black: Reformat without skipping string normalization.
This commit is contained in:
parent
fba21bb00d
commit
6f3f9bf7e4
|
@ -8,96 +8,96 @@ if MYPY:
|
||||||
|
|
||||||
whitespace_rules = [
|
whitespace_rules = [
|
||||||
# This linter should be first since bash_rules depends on it.
|
# This linter should be first since bash_rules depends on it.
|
||||||
{'pattern': r'\s+$', 'strip': '\n', 'description': 'Fix trailing whitespace'},
|
{"pattern": r"\s+$", "strip": "\n", "description": "Fix trailing whitespace"},
|
||||||
{'pattern': '\t', 'strip': '\n', 'description': 'Fix tab-based whitespace'},
|
{"pattern": "\t", "strip": "\n", "description": "Fix tab-based whitespace"},
|
||||||
] # type: List[Rule]
|
] # type: List[Rule]
|
||||||
|
|
||||||
markdown_whitespace_rules = list(
|
markdown_whitespace_rules = list(
|
||||||
[rule for rule in whitespace_rules if rule['pattern'] != r'\s+$']
|
[rule for rule in whitespace_rules if rule["pattern"] != r"\s+$"]
|
||||||
) + [
|
) + [
|
||||||
# Two spaces trailing a line with other content is okay--it's a markdown line break.
|
# Two spaces trailing a line with other content is okay--it's a markdown line break.
|
||||||
# This rule finds one space trailing a non-space, three or more trailing spaces, and
|
# This rule finds one space trailing a non-space, three or more trailing spaces, and
|
||||||
# spaces on an empty line.
|
# spaces on an empty line.
|
||||||
{
|
{
|
||||||
'pattern': r'((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)',
|
"pattern": r"((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)",
|
||||||
'strip': '\n',
|
"strip": "\n",
|
||||||
'description': 'Fix trailing whitespace',
|
"description": "Fix trailing whitespace",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'^#+[A-Za-z0-9]',
|
"pattern": r"^#+[A-Za-z0-9]",
|
||||||
'strip': '\n',
|
"strip": "\n",
|
||||||
'description': 'Missing space after # in heading',
|
"description": "Missing space after # in heading",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
python_rules = RuleList(
|
python_rules = RuleList(
|
||||||
langs=['py'],
|
langs=["py"],
|
||||||
rules=[
|
rules=[
|
||||||
{'pattern': r'".*"%\([a-z_].*\)?$', 'description': 'Missing space around "%"'},
|
{"pattern": r'".*"%\([a-z_].*\)?$', "description": 'Missing space around "%"'},
|
||||||
{'pattern': r"'.*'%\([a-z_].*\)?$", 'description': 'Missing space around "%"'},
|
{"pattern": r"'.*'%\([a-z_].*\)?$", "description": 'Missing space around "%"'},
|
||||||
# This rule is constructed with + to avoid triggering on itself
|
# This rule is constructed with + to avoid triggering on itself
|
||||||
{'pattern': r" =" + r'[^ =>~"]', 'description': 'Missing whitespace after "="'},
|
{"pattern": r" =" + r'[^ =>~"]', "description": 'Missing whitespace after "="'},
|
||||||
{'pattern': r'":\w[^"]*$', 'description': 'Missing whitespace after ":"'},
|
{"pattern": r'":\w[^"]*$', "description": 'Missing whitespace after ":"'},
|
||||||
{'pattern': r"':\w[^']*$", 'description': 'Missing whitespace after ":"'},
|
{"pattern": r"':\w[^']*$", "description": 'Missing whitespace after ":"'},
|
||||||
{'pattern': r"^\s+[#]\w", 'strip': '\n', 'description': 'Missing whitespace after "#"'},
|
{"pattern": r"^\s+[#]\w", "strip": "\n", "description": 'Missing whitespace after "#"'},
|
||||||
{
|
{
|
||||||
'pattern': r"assertEquals[(]",
|
"pattern": r"assertEquals[(]",
|
||||||
'description': 'Use assertEqual, not assertEquals (which is deprecated).',
|
"description": "Use assertEqual, not assertEquals (which is deprecated).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'self: Any',
|
"pattern": r"self: Any",
|
||||||
'description': 'you can omit Any annotation for self',
|
"description": "you can omit Any annotation for self",
|
||||||
'good_lines': ['def foo (self):'],
|
"good_lines": ["def foo (self):"],
|
||||||
'bad_lines': ['def foo(self: Any):'],
|
"bad_lines": ["def foo(self: Any):"],
|
||||||
},
|
},
|
||||||
{'pattern': r"== None", 'description': 'Use `is None` to check whether something is None'},
|
{"pattern": r"== None", "description": "Use `is None` to check whether something is None"},
|
||||||
{'pattern': r"type:[(]", 'description': 'Missing whitespace after ":" in type annotation'},
|
{"pattern": r"type:[(]", "description": 'Missing whitespace after ":" in type annotation'},
|
||||||
{'pattern': r"# type [(]", 'description': 'Missing : after type in type annotation'},
|
{"pattern": r"# type [(]", "description": "Missing : after type in type annotation"},
|
||||||
{'pattern': r"#type", 'description': 'Missing whitespace after "#" in type annotation'},
|
{"pattern": r"#type", "description": 'Missing whitespace after "#" in type annotation'},
|
||||||
{'pattern': r'if[(]', 'description': 'Missing space between if and ('},
|
{"pattern": r"if[(]", "description": "Missing space between if and ("},
|
||||||
{'pattern': r", [)]", 'description': 'Unnecessary whitespace between "," and ")"'},
|
{"pattern": r", [)]", "description": 'Unnecessary whitespace between "," and ")"'},
|
||||||
{'pattern': r"% [(]", 'description': 'Unnecessary whitespace between "%" and "("'},
|
{"pattern": r"% [(]", "description": 'Unnecessary whitespace between "%" and "("'},
|
||||||
# This next check could have false positives, but it seems pretty
|
# This next check could have false positives, but it seems pretty
|
||||||
# rare; if we find any, they can be added to the exclude list for
|
# rare; if we find any, they can be added to the exclude list for
|
||||||
# this rule.
|
# this rule.
|
||||||
{
|
{
|
||||||
'pattern': r' % [a-zA-Z0-9_.]*\)?$',
|
"pattern": r" % [a-zA-Z0-9_.]*\)?$",
|
||||||
'description': 'Used % comprehension without a tuple',
|
"description": "Used % comprehension without a tuple",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'.*%s.* % \([a-zA-Z0-9_.]*\)$',
|
"pattern": r".*%s.* % \([a-zA-Z0-9_.]*\)$",
|
||||||
'description': 'Used % comprehension without a tuple',
|
"description": "Used % comprehension without a tuple",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'__future__',
|
"pattern": r"__future__",
|
||||||
'include_only': {'zulip_bots/zulip_bots/bots/'},
|
"include_only": {"zulip_bots/zulip_bots/bots/"},
|
||||||
'description': 'Bots no longer need __future__ imports.',
|
"description": "Bots no longer need __future__ imports.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'#!/usr/bin/env python$',
|
"pattern": r"#!/usr/bin/env python$",
|
||||||
'include_only': {'zulip_bots/'},
|
"include_only": {"zulip_bots/"},
|
||||||
'description': 'Python shebangs must be python3',
|
"description": "Python shebangs must be python3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'(^|\s)open\s*\(',
|
"pattern": r"(^|\s)open\s*\(",
|
||||||
'description': 'open() should not be used in Zulip\'s bots. Use functions'
|
"description": "open() should not be used in Zulip's bots. Use functions"
|
||||||
' provided by the bots framework to access the filesystem.',
|
" provided by the bots framework to access the filesystem.",
|
||||||
'include_only': {'zulip_bots/zulip_bots/bots/'},
|
"include_only": {"zulip_bots/zulip_bots/bots/"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'pprint',
|
"pattern": r"pprint",
|
||||||
'description': 'Used pprint, which is most likely a debugging leftover. For user output, use print().',
|
"description": "Used pprint, which is most likely a debugging leftover. For user output, use print().",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'\(BotTestCase\)',
|
"pattern": r"\(BotTestCase\)",
|
||||||
'bad_lines': ['class TestSomeBot(BotTestCase):'],
|
"bad_lines": ["class TestSomeBot(BotTestCase):"],
|
||||||
'description': 'Bot test cases should directly inherit from BotTestCase *and* DefaultTests.',
|
"description": "Bot test cases should directly inherit from BotTestCase *and* DefaultTests.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'\(DefaultTests, BotTestCase\)',
|
"pattern": r"\(DefaultTests, BotTestCase\)",
|
||||||
'bad_lines': ['class TestSomeBot(DefaultTests, BotTestCase):'],
|
"bad_lines": ["class TestSomeBot(DefaultTests, BotTestCase):"],
|
||||||
'good_lines': ['class TestSomeBot(BotTestCase, DefaultTests):'],
|
"good_lines": ["class TestSomeBot(BotTestCase, DefaultTests):"],
|
||||||
'description': 'Bot test cases should inherit from BotTestCase before DefaultTests.',
|
"description": "Bot test cases should inherit from BotTestCase before DefaultTests.",
|
||||||
},
|
},
|
||||||
*whitespace_rules,
|
*whitespace_rules,
|
||||||
],
|
],
|
||||||
|
@ -105,12 +105,12 @@ python_rules = RuleList(
|
||||||
)
|
)
|
||||||
|
|
||||||
bash_rules = RuleList(
|
bash_rules = RuleList(
|
||||||
langs=['sh'],
|
langs=["sh"],
|
||||||
rules=[
|
rules=[
|
||||||
{
|
{
|
||||||
'pattern': r'#!.*sh [-xe]',
|
"pattern": r"#!.*sh [-xe]",
|
||||||
'description': 'Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches'
|
"description": "Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches"
|
||||||
' to set -x|set -e',
|
" to set -x|set -e",
|
||||||
},
|
},
|
||||||
*whitespace_rules[0:1],
|
*whitespace_rules[0:1],
|
||||||
],
|
],
|
||||||
|
@ -118,7 +118,7 @@ bash_rules = RuleList(
|
||||||
|
|
||||||
|
|
||||||
json_rules = RuleList(
|
json_rules = RuleList(
|
||||||
langs=['json'],
|
langs=["json"],
|
||||||
# Here, we don't check tab-based whitespace, because the tab-based
|
# Here, we don't check tab-based whitespace, because the tab-based
|
||||||
# whitespace rule flags a lot of third-party JSON fixtures
|
# whitespace rule flags a lot of third-party JSON fixtures
|
||||||
# under zerver/webhooks that we want preserved verbatim. So
|
# under zerver/webhooks that we want preserved verbatim. So
|
||||||
|
@ -131,21 +131,21 @@ json_rules = RuleList(
|
||||||
|
|
||||||
prose_style_rules = [
|
prose_style_rules = [
|
||||||
{
|
{
|
||||||
'pattern': r'[^\/\#\-"]([jJ]avascript)', # exclude usage in hrefs/divs
|
"pattern": r'[^\/\#\-"]([jJ]avascript)', # exclude usage in hrefs/divs
|
||||||
'description': "javascript should be spelled JavaScript",
|
"description": "javascript should be spelled JavaScript",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'''[^\/\-\."'\_\=\>]([gG]ithub)[^\.\-\_"\<]''', # exclude usage in hrefs/divs
|
"pattern": r"""[^\/\-\."'\_\=\>]([gG]ithub)[^\.\-\_"\<]""", # exclude usage in hrefs/divs
|
||||||
'description': "github should be spelled GitHub",
|
"description": "github should be spelled GitHub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'pattern': r'[oO]rganisation', # exclude usage in hrefs/divs
|
"pattern": r"[oO]rganisation", # exclude usage in hrefs/divs
|
||||||
'description': "Organization is spelled with a z",
|
"description": "Organization is spelled with a z",
|
||||||
},
|
},
|
||||||
{'pattern': r'!!! warning', 'description': "!!! warning is invalid; it's spelled '!!! warn'"},
|
{"pattern": r"!!! warning", "description": "!!! warning is invalid; it's spelled '!!! warn'"},
|
||||||
{
|
{
|
||||||
'pattern': r'[^-_]botserver(?!rc)|bot server',
|
"pattern": r"[^-_]botserver(?!rc)|bot server",
|
||||||
'description': "Use Botserver instead of botserver or Botserver.",
|
"description": "Use Botserver instead of botserver or Botserver.",
|
||||||
},
|
},
|
||||||
] # type: List[Rule]
|
] # type: List[Rule]
|
||||||
|
|
||||||
|
@ -154,13 +154,13 @@ markdown_docs_length_exclude = {
|
||||||
}
|
}
|
||||||
|
|
||||||
markdown_rules = RuleList(
|
markdown_rules = RuleList(
|
||||||
langs=['md'],
|
langs=["md"],
|
||||||
rules=[
|
rules=[
|
||||||
*markdown_whitespace_rules,
|
*markdown_whitespace_rules,
|
||||||
*prose_style_rules,
|
*prose_style_rules,
|
||||||
{
|
{
|
||||||
'pattern': r'\[(?P<url>[^\]]+)\]\((?P=url)\)',
|
"pattern": r"\[(?P<url>[^\]]+)\]\((?P=url)\)",
|
||||||
'description': 'Linkified markdown URLs should use cleaner <http://example.com> syntax.',
|
"description": "Linkified markdown URLs should use cleaner <http://example.com> syntax.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
max_length=120,
|
max_length=120,
|
||||||
|
@ -168,7 +168,7 @@ markdown_rules = RuleList(
|
||||||
)
|
)
|
||||||
|
|
||||||
txt_rules = RuleList(
|
txt_rules = RuleList(
|
||||||
langs=['txt'],
|
langs=["txt"],
|
||||||
rules=whitespace_rules,
|
rules=whitespace_rules,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
204
tools/deploy
204
tools/deploy
|
@ -11,69 +11,69 @@ from typing import Any, Callable, Dict, List
|
||||||
import requests
|
import requests
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
|
||||||
red = '\033[91m' # type: str
|
red = "\033[91m" # type: str
|
||||||
green = '\033[92m' # type: str
|
green = "\033[92m" # type: str
|
||||||
end_format = '\033[0m' # type: str
|
end_format = "\033[0m" # type: str
|
||||||
bold = '\033[1m' # type: str
|
bold = "\033[1m" # type: str
|
||||||
|
|
||||||
bots_dir = '.bots' # type: str
|
bots_dir = ".bots" # type: str
|
||||||
|
|
||||||
|
|
||||||
def pack(options: argparse.Namespace) -> None:
|
def pack(options: argparse.Namespace) -> None:
|
||||||
# Basic sanity checks for input.
|
# Basic sanity checks for input.
|
||||||
if not options.path:
|
if not options.path:
|
||||||
print('tools/deploy: Path to bot folder not specified.')
|
print("tools/deploy: Path to bot folder not specified.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not options.config:
|
if not options.config:
|
||||||
print('tools/deploy: Path to zuliprc not specified.')
|
print("tools/deploy: Path to zuliprc not specified.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not options.main:
|
if not options.main:
|
||||||
print('tools/deploy: No main bot file specified.')
|
print("tools/deploy: No main bot file specified.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not os.path.isfile(options.config):
|
if not os.path.isfile(options.config):
|
||||||
print('pack: Config file not found at path: {}.'.format(options.config))
|
print("pack: Config file not found at path: {}.".format(options.config))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not os.path.isdir(options.path):
|
if not os.path.isdir(options.path):
|
||||||
print('pack: Bot folder not found at path: {}.'.format(options.path))
|
print("pack: Bot folder not found at path: {}.".format(options.path))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
main_path = os.path.join(options.path, options.main)
|
main_path = os.path.join(options.path, options.main)
|
||||||
if not os.path.isfile(main_path):
|
if not os.path.isfile(main_path):
|
||||||
print('pack: Bot main file not found at path: {}.'.format(main_path))
|
print("pack: Bot main file not found at path: {}.".format(main_path))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Main logic for packing the bot.
|
# Main logic for packing the bot.
|
||||||
if not os.path.exists(bots_dir):
|
if not os.path.exists(bots_dir):
|
||||||
os.makedirs(bots_dir)
|
os.makedirs(bots_dir)
|
||||||
zip_file_path = os.path.join(bots_dir, options.botname + ".zip")
|
zip_file_path = os.path.join(bots_dir, options.botname + ".zip")
|
||||||
zip_file = zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED)
|
zip_file = zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_DEFLATED)
|
||||||
# Pack the complete bot folder
|
# Pack the complete bot folder
|
||||||
for root, dirs, files in os.walk(options.path):
|
for root, dirs, files in os.walk(options.path):
|
||||||
for file in files:
|
for file in files:
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
zip_file.write(file_path, os.path.relpath(file_path, options.path))
|
zip_file.write(file_path, os.path.relpath(file_path, options.path))
|
||||||
# Pack the zuliprc
|
# Pack the zuliprc
|
||||||
zip_file.write(options.config, 'zuliprc')
|
zip_file.write(options.config, "zuliprc")
|
||||||
# Pack the config file for the botfarm.
|
# Pack the config file for the botfarm.
|
||||||
bot_config = textwrap.dedent(
|
bot_config = textwrap.dedent(
|
||||||
'''\
|
"""\
|
||||||
[deploy]
|
[deploy]
|
||||||
bot={}
|
bot={}
|
||||||
zuliprc=zuliprc
|
zuliprc=zuliprc
|
||||||
'''.format(
|
""".format(
|
||||||
options.main
|
options.main
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
zip_file.writestr('config.ini', bot_config)
|
zip_file.writestr("config.ini", bot_config)
|
||||||
zip_file.close()
|
zip_file.close()
|
||||||
print('pack: Created zip file at: {}.'.format(zip_file_path))
|
print("pack: Created zip file at: {}.".format(zip_file_path))
|
||||||
|
|
||||||
|
|
||||||
def check_common_options(options: argparse.Namespace) -> None:
|
def check_common_options(options: argparse.Namespace) -> None:
|
||||||
if not options.server:
|
if not options.server:
|
||||||
print('tools/deploy: URL to Botfarm server not specified.')
|
print("tools/deploy: URL to Botfarm server not specified.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not options.token:
|
if not options.token:
|
||||||
print('tools/deploy: Botfarm deploy token not specified.')
|
print("tools/deploy: Botfarm deploy token not specified.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ def handle_common_response_without_data(
|
||||||
return handle_common_response(
|
return handle_common_response(
|
||||||
response=response,
|
response=response,
|
||||||
operation=operation,
|
operation=operation,
|
||||||
success_handler=lambda r: print('{}: {}'.format(operation, success_message)),
|
success_handler=lambda r: print("{}: {}".format(operation, success_message)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,56 +92,56 @@ def handle_common_response(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if response.status_code == requests.codes.ok:
|
if response.status_code == requests.codes.ok:
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
if response_data['status'] == 'success':
|
if response_data["status"] == "success":
|
||||||
success_handler(response_data)
|
success_handler(response_data)
|
||||||
return True
|
return True
|
||||||
elif response_data['status'] == 'error':
|
elif response_data["status"] == "error":
|
||||||
print('{}: {}'.format(operation, response_data['message']))
|
print("{}: {}".format(operation, response_data["message"]))
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
print('{}: Unexpected success response format'.format(operation))
|
print("{}: Unexpected success response format".format(operation))
|
||||||
return False
|
return False
|
||||||
if response.status_code == requests.codes.unauthorized:
|
if response.status_code == requests.codes.unauthorized:
|
||||||
print('{}: Authentication error with the server. Aborting.'.format(operation))
|
print("{}: Authentication error with the server. Aborting.".format(operation))
|
||||||
else:
|
else:
|
||||||
print('{}: Error {}. Aborting.'.format(operation, response.status_code))
|
print("{}: Error {}. Aborting.".format(operation, response.status_code))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def upload(options: argparse.Namespace) -> None:
|
def upload(options: argparse.Namespace) -> None:
|
||||||
check_common_options(options)
|
check_common_options(options)
|
||||||
file_path = os.path.join(bots_dir, options.botname + '.zip')
|
file_path = os.path.join(bots_dir, options.botname + ".zip")
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
print('upload: Could not find bot package at {}.'.format(file_path))
|
print("upload: Could not find bot package at {}.".format(file_path))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
files = {'file': open(file_path, 'rb')}
|
files = {"file": open(file_path, "rb")}
|
||||||
headers = {'key': options.token}
|
headers = {"key": options.token}
|
||||||
url = urllib.parse.urljoin(options.server, 'bots/upload')
|
url = urllib.parse.urljoin(options.server, "bots/upload")
|
||||||
response = requests.post(url, files=files, headers=headers)
|
response = requests.post(url, files=files, headers=headers)
|
||||||
result = handle_common_response_without_data(
|
result = handle_common_response_without_data(
|
||||||
response, 'upload', 'Uploaded the bot package to botfarm.'
|
response, "upload", "Uploaded the bot package to botfarm."
|
||||||
)
|
)
|
||||||
if result is False:
|
if result is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def clean(options: argparse.Namespace) -> None:
|
def clean(options: argparse.Namespace) -> None:
|
||||||
file_path = os.path.join(bots_dir, options.botname + '.zip')
|
file_path = os.path.join(bots_dir, options.botname + ".zip")
|
||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
print('clean: Removed {}.'.format(file_path))
|
print("clean: Removed {}.".format(file_path))
|
||||||
else:
|
else:
|
||||||
print('clean: File \'{}\' not found.'.format(file_path))
|
print("clean: File '{}' not found.".format(file_path))
|
||||||
|
|
||||||
|
|
||||||
def process(options: argparse.Namespace) -> None:
|
def process(options: argparse.Namespace) -> None:
|
||||||
check_common_options(options)
|
check_common_options(options)
|
||||||
headers = {'key': options.token}
|
headers = {"key": options.token}
|
||||||
url = urllib.parse.urljoin(options.server, 'bots/process')
|
url = urllib.parse.urljoin(options.server, "bots/process")
|
||||||
payload = {'name': options.botname}
|
payload = {"name": options.botname}
|
||||||
response = requests.post(url, headers=headers, json=payload)
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
result = handle_common_response_without_data(
|
result = handle_common_response_without_data(
|
||||||
response, 'process', 'The bot has been processed by the botfarm.'
|
response, "process", "The bot has been processed by the botfarm."
|
||||||
)
|
)
|
||||||
if result is False:
|
if result is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -149,12 +149,12 @@ def process(options: argparse.Namespace) -> None:
|
||||||
|
|
||||||
def start(options: argparse.Namespace) -> None:
|
def start(options: argparse.Namespace) -> None:
|
||||||
check_common_options(options)
|
check_common_options(options)
|
||||||
headers = {'key': options.token}
|
headers = {"key": options.token}
|
||||||
url = urllib.parse.urljoin(options.server, 'bots/start')
|
url = urllib.parse.urljoin(options.server, "bots/start")
|
||||||
payload = {'name': options.botname}
|
payload = {"name": options.botname}
|
||||||
response = requests.post(url, headers=headers, json=payload)
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
result = handle_common_response_without_data(
|
result = handle_common_response_without_data(
|
||||||
response, 'start', 'The bot has been started by the botfarm.'
|
response, "start", "The bot has been started by the botfarm."
|
||||||
)
|
)
|
||||||
if result is False:
|
if result is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -162,12 +162,12 @@ def start(options: argparse.Namespace) -> None:
|
||||||
|
|
||||||
def stop(options: argparse.Namespace) -> None:
|
def stop(options: argparse.Namespace) -> None:
|
||||||
check_common_options(options)
|
check_common_options(options)
|
||||||
headers = {'key': options.token}
|
headers = {"key": options.token}
|
||||||
url = urllib.parse.urljoin(options.server, 'bots/stop')
|
url = urllib.parse.urljoin(options.server, "bots/stop")
|
||||||
payload = {'name': options.botname}
|
payload = {"name": options.botname}
|
||||||
response = requests.post(url, headers=headers, json=payload)
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
result = handle_common_response_without_data(
|
result = handle_common_response_without_data(
|
||||||
response, 'stop', 'The bot has been stopped by the botfarm.'
|
response, "stop", "The bot has been stopped by the botfarm."
|
||||||
)
|
)
|
||||||
if result is False:
|
if result is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -182,27 +182,27 @@ def prepare(options: argparse.Namespace) -> None:
|
||||||
|
|
||||||
def log(options: argparse.Namespace) -> None:
|
def log(options: argparse.Namespace) -> None:
|
||||||
check_common_options(options)
|
check_common_options(options)
|
||||||
headers = {'key': options.token}
|
headers = {"key": options.token}
|
||||||
if options.lines:
|
if options.lines:
|
||||||
lines = options.lines
|
lines = options.lines
|
||||||
else:
|
else:
|
||||||
lines = None
|
lines = None
|
||||||
payload = {'name': options.botname, 'lines': lines}
|
payload = {"name": options.botname, "lines": lines}
|
||||||
url = urllib.parse.urljoin(options.server, 'bots/logs/' + options.botname)
|
url = urllib.parse.urljoin(options.server, "bots/logs/" + options.botname)
|
||||||
response = requests.get(url, json=payload, headers=headers)
|
response = requests.get(url, json=payload, headers=headers)
|
||||||
result = handle_common_response(response, 'log', lambda r: print(r['logs']['content']))
|
result = handle_common_response(response, "log", lambda r: print(r["logs"]["content"]))
|
||||||
if result is False:
|
if result is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def delete(options: argparse.Namespace) -> None:
|
def delete(options: argparse.Namespace) -> None:
|
||||||
check_common_options(options)
|
check_common_options(options)
|
||||||
headers = {'key': options.token}
|
headers = {"key": options.token}
|
||||||
url = urllib.parse.urljoin(options.server, 'bots/delete')
|
url = urllib.parse.urljoin(options.server, "bots/delete")
|
||||||
payload = {'name': options.botname}
|
payload = {"name": options.botname}
|
||||||
response = requests.post(url, headers=headers, json=payload)
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
result = handle_common_response_without_data(
|
result = handle_common_response_without_data(
|
||||||
response, 'delete', 'The bot has been removed from the botfarm.'
|
response, "delete", "The bot has been removed from the botfarm."
|
||||||
)
|
)
|
||||||
if result is False:
|
if result is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -210,15 +210,15 @@ def delete(options: argparse.Namespace) -> None:
|
||||||
|
|
||||||
def list_bots(options: argparse.Namespace) -> None:
|
def list_bots(options: argparse.Namespace) -> None:
|
||||||
check_common_options(options)
|
check_common_options(options)
|
||||||
headers = {'key': options.token}
|
headers = {"key": options.token}
|
||||||
if options.format:
|
if options.format:
|
||||||
pretty_print = True
|
pretty_print = True
|
||||||
else:
|
else:
|
||||||
pretty_print = False
|
pretty_print = False
|
||||||
url = urllib.parse.urljoin(options.server, 'bots/list')
|
url = urllib.parse.urljoin(options.server, "bots/list")
|
||||||
response = requests.get(url, headers=headers)
|
response = requests.get(url, headers=headers)
|
||||||
result = handle_common_response(
|
result = handle_common_response(
|
||||||
response, 'ls', lambda r: print_bots(r['bots']['list'], pretty_print)
|
response, "ls", lambda r: print_bots(r["bots"]["list"], pretty_print)
|
||||||
)
|
)
|
||||||
if result is False:
|
if result is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -229,36 +229,36 @@ def print_bots(bots: List[Any], pretty_print: bool) -> None:
|
||||||
print_bots_pretty(bots)
|
print_bots_pretty(bots)
|
||||||
else:
|
else:
|
||||||
for bot in bots:
|
for bot in bots:
|
||||||
print('{}\t{}\t{}\t{}'.format(bot['name'], bot['status'], bot['email'], bot['site']))
|
print("{}\t{}\t{}\t{}".format(bot["name"], bot["status"], bot["email"], bot["site"]))
|
||||||
|
|
||||||
|
|
||||||
def print_bots_pretty(bots: List[Any]) -> None:
|
def print_bots_pretty(bots: List[Any]) -> None:
|
||||||
if len(bots) == 0:
|
if len(bots) == 0:
|
||||||
print('ls: No bots found on the botfarm')
|
print("ls: No bots found on the botfarm")
|
||||||
else:
|
else:
|
||||||
print('ls: There are the following bots on the botfarm:')
|
print("ls: There are the following bots on the botfarm:")
|
||||||
name_col_len, status_col_len, email_col_len, site_col_len = 25, 15, 35, 35
|
name_col_len, status_col_len, email_col_len, site_col_len = 25, 15, 35, 35
|
||||||
row_format = '{0} {1} {2} {3}'
|
row_format = "{0} {1} {2} {3}"
|
||||||
header = row_format.format(
|
header = row_format.format(
|
||||||
'NAME'.rjust(name_col_len),
|
"NAME".rjust(name_col_len),
|
||||||
'STATUS'.rjust(status_col_len),
|
"STATUS".rjust(status_col_len),
|
||||||
'EMAIL'.rjust(email_col_len),
|
"EMAIL".rjust(email_col_len),
|
||||||
'SITE'.rjust(site_col_len),
|
"SITE".rjust(site_col_len),
|
||||||
)
|
)
|
||||||
header_bottom = row_format.format(
|
header_bottom = row_format.format(
|
||||||
'-' * name_col_len,
|
"-" * name_col_len,
|
||||||
'-' * status_col_len,
|
"-" * status_col_len,
|
||||||
'-' * email_col_len,
|
"-" * email_col_len,
|
||||||
'-' * site_col_len,
|
"-" * site_col_len,
|
||||||
)
|
)
|
||||||
print(header)
|
print(header)
|
||||||
print(header_bottom)
|
print(header_bottom)
|
||||||
for bot in bots:
|
for bot in bots:
|
||||||
row = row_format.format(
|
row = row_format.format(
|
||||||
bot['name'].rjust(name_col_len),
|
bot["name"].rjust(name_col_len),
|
||||||
bot['status'].rjust(status_col_len),
|
bot["status"].rjust(status_col_len),
|
||||||
bot['email'].rjust(email_col_len),
|
bot["email"].rjust(email_col_len),
|
||||||
bot['site'].rjust(site_col_len),
|
bot["site"].rjust(site_col_len),
|
||||||
)
|
)
|
||||||
print(row)
|
print(row)
|
||||||
|
|
||||||
|
@ -297,52 +297,52 @@ To list user's bots, use:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser(usage=usage)
|
parser = argparse.ArgumentParser(usage=usage)
|
||||||
parser.add_argument('command', help='Command to run.')
|
parser.add_argument("command", help="Command to run.")
|
||||||
parser.add_argument('botname', nargs='?', help='Name of bot to operate on.')
|
parser.add_argument("botname", nargs="?", help="Name of bot to operate on.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--server',
|
"--server",
|
||||||
'-s',
|
"-s",
|
||||||
metavar='SERVERURL',
|
metavar="SERVERURL",
|
||||||
default=os.environ.get('SERVER', ''),
|
default=os.environ.get("SERVER", ""),
|
||||||
help='Url of the Zulip Botfarm server.',
|
help="Url of the Zulip Botfarm server.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--token', '-t', default=os.environ.get('TOKEN', ''), help='Deploy Token for the Botfarm.'
|
"--token", "-t", default=os.environ.get("TOKEN", ""), help="Deploy Token for the Botfarm."
|
||||||
)
|
)
|
||||||
parser.add_argument('--path', '-p', help='Path to the bot directory.')
|
parser.add_argument("--path", "-p", help="Path to the bot directory.")
|
||||||
parser.add_argument('--config', '-c', help='Path to the zuliprc file.')
|
parser.add_argument("--config", "-c", help="Path to the zuliprc file.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--main', '-m', help='Path to the bot\'s main file, relative to the bot\'s directory.'
|
"--main", "-m", help="Path to the bot's main file, relative to the bot's directory."
|
||||||
)
|
)
|
||||||
parser.add_argument('--lines', '-l', help='Number of lines in log required.')
|
parser.add_argument("--lines", "-l", help="Number of lines in log required.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--format', '-f', action='store_true', help='Print user\'s bots in human readable format'
|
"--format", "-f", action="store_true", help="Print user's bots in human readable format"
|
||||||
)
|
)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
if not options.command:
|
if not options.command:
|
||||||
print('tools/deploy: No command specified.')
|
print("tools/deploy: No command specified.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if not options.botname and options.command not in ['ls']:
|
if not options.botname and options.command not in ["ls"]:
|
||||||
print('tools/deploy: No bot name specified. Please specify a name like \'my-custom-bot\'')
|
print("tools/deploy: No bot name specified. Please specify a name like 'my-custom-bot'")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
commands = {
|
commands = {
|
||||||
'pack': pack,
|
"pack": pack,
|
||||||
'upload': upload,
|
"upload": upload,
|
||||||
'clean': clean,
|
"clean": clean,
|
||||||
'prepare': prepare,
|
"prepare": prepare,
|
||||||
'process': process,
|
"process": process,
|
||||||
'start': start,
|
"start": start,
|
||||||
'stop': stop,
|
"stop": stop,
|
||||||
'log': log,
|
"log": log,
|
||||||
'delete': delete,
|
"delete": delete,
|
||||||
'ls': list_bots,
|
"ls": list_bots,
|
||||||
}
|
}
|
||||||
if options.command in commands:
|
if options.command in commands:
|
||||||
commands[options.command](options)
|
commands[options.command](options)
|
||||||
else:
|
else:
|
||||||
print('tools/deploy: No command \'{}\' found.'.format(options.command))
|
print("tools/deploy: No command '{}' found.".format(options.command))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -11,246 +11,246 @@ from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
|
||||||
# License: MIT
|
# License: MIT
|
||||||
# Ref: fit_commit/validators/tense.rb
|
# Ref: fit_commit/validators/tense.rb
|
||||||
WORD_SET = {
|
WORD_SET = {
|
||||||
'adds',
|
"adds",
|
||||||
'adding',
|
"adding",
|
||||||
'added',
|
"added",
|
||||||
'allows',
|
"allows",
|
||||||
'allowing',
|
"allowing",
|
||||||
'allowed',
|
"allowed",
|
||||||
'amends',
|
"amends",
|
||||||
'amending',
|
"amending",
|
||||||
'amended',
|
"amended",
|
||||||
'bumps',
|
"bumps",
|
||||||
'bumping',
|
"bumping",
|
||||||
'bumped',
|
"bumped",
|
||||||
'calculates',
|
"calculates",
|
||||||
'calculating',
|
"calculating",
|
||||||
'calculated',
|
"calculated",
|
||||||
'changes',
|
"changes",
|
||||||
'changing',
|
"changing",
|
||||||
'changed',
|
"changed",
|
||||||
'cleans',
|
"cleans",
|
||||||
'cleaning',
|
"cleaning",
|
||||||
'cleaned',
|
"cleaned",
|
||||||
'commits',
|
"commits",
|
||||||
'committing',
|
"committing",
|
||||||
'committed',
|
"committed",
|
||||||
'corrects',
|
"corrects",
|
||||||
'correcting',
|
"correcting",
|
||||||
'corrected',
|
"corrected",
|
||||||
'creates',
|
"creates",
|
||||||
'creating',
|
"creating",
|
||||||
'created',
|
"created",
|
||||||
'darkens',
|
"darkens",
|
||||||
'darkening',
|
"darkening",
|
||||||
'darkened',
|
"darkened",
|
||||||
'disables',
|
"disables",
|
||||||
'disabling',
|
"disabling",
|
||||||
'disabled',
|
"disabled",
|
||||||
'displays',
|
"displays",
|
||||||
'displaying',
|
"displaying",
|
||||||
'displayed',
|
"displayed",
|
||||||
'documents',
|
"documents",
|
||||||
'documenting',
|
"documenting",
|
||||||
'documented',
|
"documented",
|
||||||
'drys',
|
"drys",
|
||||||
'drying',
|
"drying",
|
||||||
'dryed',
|
"dryed",
|
||||||
'ends',
|
"ends",
|
||||||
'ending',
|
"ending",
|
||||||
'ended',
|
"ended",
|
||||||
'enforces',
|
"enforces",
|
||||||
'enforcing',
|
"enforcing",
|
||||||
'enforced',
|
"enforced",
|
||||||
'enqueues',
|
"enqueues",
|
||||||
'enqueuing',
|
"enqueuing",
|
||||||
'enqueued',
|
"enqueued",
|
||||||
'extracts',
|
"extracts",
|
||||||
'extracting',
|
"extracting",
|
||||||
'extracted',
|
"extracted",
|
||||||
'finishes',
|
"finishes",
|
||||||
'finishing',
|
"finishing",
|
||||||
'finished',
|
"finished",
|
||||||
'fixes',
|
"fixes",
|
||||||
'fixing',
|
"fixing",
|
||||||
'fixed',
|
"fixed",
|
||||||
'formats',
|
"formats",
|
||||||
'formatting',
|
"formatting",
|
||||||
'formatted',
|
"formatted",
|
||||||
'guards',
|
"guards",
|
||||||
'guarding',
|
"guarding",
|
||||||
'guarded',
|
"guarded",
|
||||||
'handles',
|
"handles",
|
||||||
'handling',
|
"handling",
|
||||||
'handled',
|
"handled",
|
||||||
'hides',
|
"hides",
|
||||||
'hiding',
|
"hiding",
|
||||||
'hid',
|
"hid",
|
||||||
'increases',
|
"increases",
|
||||||
'increasing',
|
"increasing",
|
||||||
'increased',
|
"increased",
|
||||||
'ignores',
|
"ignores",
|
||||||
'ignoring',
|
"ignoring",
|
||||||
'ignored',
|
"ignored",
|
||||||
'implements',
|
"implements",
|
||||||
'implementing',
|
"implementing",
|
||||||
'implemented',
|
"implemented",
|
||||||
'improves',
|
"improves",
|
||||||
'improving',
|
"improving",
|
||||||
'improved',
|
"improved",
|
||||||
'keeps',
|
"keeps",
|
||||||
'keeping',
|
"keeping",
|
||||||
'kept',
|
"kept",
|
||||||
'kills',
|
"kills",
|
||||||
'killing',
|
"killing",
|
||||||
'killed',
|
"killed",
|
||||||
'makes',
|
"makes",
|
||||||
'making',
|
"making",
|
||||||
'made',
|
"made",
|
||||||
'merges',
|
"merges",
|
||||||
'merging',
|
"merging",
|
||||||
'merged',
|
"merged",
|
||||||
'moves',
|
"moves",
|
||||||
'moving',
|
"moving",
|
||||||
'moved',
|
"moved",
|
||||||
'permits',
|
"permits",
|
||||||
'permitting',
|
"permitting",
|
||||||
'permitted',
|
"permitted",
|
||||||
'prevents',
|
"prevents",
|
||||||
'preventing',
|
"preventing",
|
||||||
'prevented',
|
"prevented",
|
||||||
'pushes',
|
"pushes",
|
||||||
'pushing',
|
"pushing",
|
||||||
'pushed',
|
"pushed",
|
||||||
'rebases',
|
"rebases",
|
||||||
'rebasing',
|
"rebasing",
|
||||||
'rebased',
|
"rebased",
|
||||||
'refactors',
|
"refactors",
|
||||||
'refactoring',
|
"refactoring",
|
||||||
'refactored',
|
"refactored",
|
||||||
'removes',
|
"removes",
|
||||||
'removing',
|
"removing",
|
||||||
'removed',
|
"removed",
|
||||||
'renames',
|
"renames",
|
||||||
'renaming',
|
"renaming",
|
||||||
'renamed',
|
"renamed",
|
||||||
'reorders',
|
"reorders",
|
||||||
'reordering',
|
"reordering",
|
||||||
'reordered',
|
"reordered",
|
||||||
'replaces',
|
"replaces",
|
||||||
'replacing',
|
"replacing",
|
||||||
'replaced',
|
"replaced",
|
||||||
'requires',
|
"requires",
|
||||||
'requiring',
|
"requiring",
|
||||||
'required',
|
"required",
|
||||||
'restores',
|
"restores",
|
||||||
'restoring',
|
"restoring",
|
||||||
'restored',
|
"restored",
|
||||||
'sends',
|
"sends",
|
||||||
'sending',
|
"sending",
|
||||||
'sent',
|
"sent",
|
||||||
'sets',
|
"sets",
|
||||||
'setting',
|
"setting",
|
||||||
'separates',
|
"separates",
|
||||||
'separating',
|
"separating",
|
||||||
'separated',
|
"separated",
|
||||||
'shows',
|
"shows",
|
||||||
'showing',
|
"showing",
|
||||||
'showed',
|
"showed",
|
||||||
'simplifies',
|
"simplifies",
|
||||||
'simplifying',
|
"simplifying",
|
||||||
'simplified',
|
"simplified",
|
||||||
'skips',
|
"skips",
|
||||||
'skipping',
|
"skipping",
|
||||||
'skipped',
|
"skipped",
|
||||||
'sorts',
|
"sorts",
|
||||||
'sorting',
|
"sorting",
|
||||||
'speeds',
|
"speeds",
|
||||||
'speeding',
|
"speeding",
|
||||||
'sped',
|
"sped",
|
||||||
'starts',
|
"starts",
|
||||||
'starting',
|
"starting",
|
||||||
'started',
|
"started",
|
||||||
'supports',
|
"supports",
|
||||||
'supporting',
|
"supporting",
|
||||||
'supported',
|
"supported",
|
||||||
'takes',
|
"takes",
|
||||||
'taking',
|
"taking",
|
||||||
'took',
|
"took",
|
||||||
'testing',
|
"testing",
|
||||||
'tested', # 'tests' excluded to reduce false negative
|
"tested", # 'tests' excluded to reduce false negative
|
||||||
'truncates',
|
"truncates",
|
||||||
'truncating',
|
"truncating",
|
||||||
'truncated',
|
"truncated",
|
||||||
'updates',
|
"updates",
|
||||||
'updating',
|
"updating",
|
||||||
'updated',
|
"updated",
|
||||||
'uses',
|
"uses",
|
||||||
'using',
|
"using",
|
||||||
'used',
|
"used",
|
||||||
}
|
}
|
||||||
|
|
||||||
imperative_forms = [
|
imperative_forms = [
|
||||||
'add',
|
"add",
|
||||||
'allow',
|
"allow",
|
||||||
'amend',
|
"amend",
|
||||||
'bump',
|
"bump",
|
||||||
'calculate',
|
"calculate",
|
||||||
'change',
|
"change",
|
||||||
'clean',
|
"clean",
|
||||||
'commit',
|
"commit",
|
||||||
'correct',
|
"correct",
|
||||||
'create',
|
"create",
|
||||||
'darken',
|
"darken",
|
||||||
'disable',
|
"disable",
|
||||||
'display',
|
"display",
|
||||||
'document',
|
"document",
|
||||||
'dry',
|
"dry",
|
||||||
'end',
|
"end",
|
||||||
'enforce',
|
"enforce",
|
||||||
'enqueue',
|
"enqueue",
|
||||||
'extract',
|
"extract",
|
||||||
'finish',
|
"finish",
|
||||||
'fix',
|
"fix",
|
||||||
'format',
|
"format",
|
||||||
'guard',
|
"guard",
|
||||||
'handle',
|
"handle",
|
||||||
'hide',
|
"hide",
|
||||||
'ignore',
|
"ignore",
|
||||||
'implement',
|
"implement",
|
||||||
'improve',
|
"improve",
|
||||||
'increase',
|
"increase",
|
||||||
'keep',
|
"keep",
|
||||||
'kill',
|
"kill",
|
||||||
'make',
|
"make",
|
||||||
'merge',
|
"merge",
|
||||||
'move',
|
"move",
|
||||||
'permit',
|
"permit",
|
||||||
'prevent',
|
"prevent",
|
||||||
'push',
|
"push",
|
||||||
'rebase',
|
"rebase",
|
||||||
'refactor',
|
"refactor",
|
||||||
'remove',
|
"remove",
|
||||||
'rename',
|
"rename",
|
||||||
'reorder',
|
"reorder",
|
||||||
'replace',
|
"replace",
|
||||||
'require',
|
"require",
|
||||||
'restore',
|
"restore",
|
||||||
'send',
|
"send",
|
||||||
'separate',
|
"separate",
|
||||||
'set',
|
"set",
|
||||||
'show',
|
"show",
|
||||||
'simplify',
|
"simplify",
|
||||||
'skip',
|
"skip",
|
||||||
'sort',
|
"sort",
|
||||||
'speed',
|
"speed",
|
||||||
'start',
|
"start",
|
||||||
'support',
|
"support",
|
||||||
'take',
|
"take",
|
||||||
'test',
|
"test",
|
||||||
'truncate',
|
"truncate",
|
||||||
'update',
|
"update",
|
||||||
'use',
|
"use",
|
||||||
]
|
]
|
||||||
imperative_forms.sort()
|
imperative_forms.sort()
|
||||||
|
|
||||||
|
@ -260,8 +260,8 @@ def head_binary_search(key: str, words: List[str]) -> str:
|
||||||
3 characters."""
|
3 characters."""
|
||||||
|
|
||||||
# Edge case: 'disable' and 'display' have the same 3 starting letters.
|
# Edge case: 'disable' and 'display' have the same 3 starting letters.
|
||||||
if key in ['displays', 'displaying', 'displayed']:
|
if key in ["displays", "displaying", "displayed"]:
|
||||||
return 'display'
|
return "display"
|
||||||
|
|
||||||
lower = 0
|
lower = 0
|
||||||
upper = len(words) - 1
|
upper = len(words) - 1
|
||||||
|
@ -292,7 +292,7 @@ class ImperativeMood(LineRule):
|
||||||
target = CommitMessageTitle
|
target = CommitMessageTitle
|
||||||
|
|
||||||
error_msg = (
|
error_msg = (
|
||||||
'The first word in commit title should be in imperative mood '
|
"The first word in commit title should be in imperative mood "
|
||||||
'("{word}" -> "{imperative}"): "{title}"'
|
'("{word}" -> "{imperative}"): "{title}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ class ImperativeMood(LineRule):
|
||||||
violations = []
|
violations = []
|
||||||
|
|
||||||
# Ignore the section tag (ie `<section tag>: <message body>.`)
|
# Ignore the section tag (ie `<section tag>: <message body>.`)
|
||||||
words = line.split(': ', 1)[-1].split()
|
words = line.split(": ", 1)[-1].split()
|
||||||
first_word = words[0].lower()
|
first_word = words[0].lower()
|
||||||
|
|
||||||
if first_word in WORD_SET:
|
if first_word in WORD_SET:
|
||||||
|
|
16
tools/lint
16
tools/lint
|
@ -9,7 +9,7 @@ from custom_check import non_py_rules, python_rules
|
||||||
|
|
||||||
EXCLUDED_FILES = [
|
EXCLUDED_FILES = [
|
||||||
# This is an external file that doesn't comply with our codestyle
|
# This is an external file that doesn't comply with our codestyle
|
||||||
'zulip/integrations/perforce/git_p4.py',
|
"zulip/integrations/perforce/git_p4.py",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,21 +21,21 @@ def run() -> None:
|
||||||
linter_config = LinterConfig(args)
|
linter_config = LinterConfig(args)
|
||||||
|
|
||||||
by_lang = linter_config.list_files(
|
by_lang = linter_config.list_files(
|
||||||
file_types=['py', 'sh', 'json', 'md', 'txt'], exclude=EXCLUDED_FILES
|
file_types=["py", "sh", "json", "md", "txt"], exclude=EXCLUDED_FILES
|
||||||
)
|
)
|
||||||
|
|
||||||
linter_config.external_linter(
|
linter_config.external_linter(
|
||||||
'mypy',
|
"mypy",
|
||||||
[sys.executable, 'tools/run-mypy'],
|
[sys.executable, "tools/run-mypy"],
|
||||||
['py'],
|
["py"],
|
||||||
pass_targets=False,
|
pass_targets=False,
|
||||||
description="Static type checker for Python (config: mypy.ini)",
|
description="Static type checker for Python (config: mypy.ini)",
|
||||||
)
|
)
|
||||||
linter_config.external_linter(
|
linter_config.external_linter(
|
||||||
'flake8', ['flake8'], ['py'], description="Standard Python linter (config: .flake8)"
|
"flake8", ["flake8"], ["py"], description="Standard Python linter (config: .flake8)"
|
||||||
)
|
)
|
||||||
linter_config.external_linter(
|
linter_config.external_linter(
|
||||||
'gitlint', ['tools/lint-commits'], description="Git Lint for commit messages"
|
"gitlint", ["tools/lint-commits"], description="Git Lint for commit messages"
|
||||||
)
|
)
|
||||||
|
|
||||||
@linter_config.lint
|
@linter_config.lint
|
||||||
|
@ -55,5 +55,5 @@ def run() -> None:
|
||||||
linter_config.do_lint()
|
linter_config.do_lint()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
|
|
|
@ -7,13 +7,13 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
ZULIP_BOTS_DIR = os.path.join(CURRENT_DIR, '..', 'zulip_bots')
|
ZULIP_BOTS_DIR = os.path.join(CURRENT_DIR, "..", "zulip_bots")
|
||||||
sys.path.append(ZULIP_BOTS_DIR)
|
sys.path.append(ZULIP_BOTS_DIR)
|
||||||
|
|
||||||
red = '\033[91m'
|
red = "\033[91m"
|
||||||
green = '\033[92m'
|
green = "\033[92m"
|
||||||
end_format = '\033[0m'
|
end_format = "\033[0m"
|
||||||
bold = '\033[1m'
|
bold = "\033[1m"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -23,25 +23,25 @@ Creates a Python virtualenv. Its Python version is equal to
|
||||||
the Python version this command is executed with."""
|
the Python version this command is executed with."""
|
||||||
parser = argparse.ArgumentParser(usage=usage)
|
parser = argparse.ArgumentParser(usage=usage)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--python-interpreter',
|
"--python-interpreter",
|
||||||
'-p',
|
"-p",
|
||||||
metavar='PATH_TO_PYTHON_INTERPRETER',
|
metavar="PATH_TO_PYTHON_INTERPRETER",
|
||||||
default=os.path.abspath(sys.executable),
|
default=os.path.abspath(sys.executable),
|
||||||
help='Path to the Python interpreter to use when provisioning.',
|
help="Path to the Python interpreter to use when provisioning.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--force', '-f', action='store_true', help='create venv even with outdated Python version.'
|
"--force", "-f", action="store_true", help="create venv even with outdated Python version."
|
||||||
)
|
)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
base_dir = os.path.abspath(os.path.join(__file__, '..', '..'))
|
base_dir = os.path.abspath(os.path.join(__file__, "..", ".."))
|
||||||
py_version_output = subprocess.check_output(
|
py_version_output = subprocess.check_output(
|
||||||
[options.python_interpreter, '--version'], stderr=subprocess.STDOUT, universal_newlines=True
|
[options.python_interpreter, "--version"], stderr=subprocess.STDOUT, universal_newlines=True
|
||||||
)
|
)
|
||||||
# The output has the format "Python 1.2.3"
|
# The output has the format "Python 1.2.3"
|
||||||
py_version_list = py_version_output.split()[1].split('.')
|
py_version_list = py_version_output.split()[1].split(".")
|
||||||
py_version = tuple(int(num) for num in py_version_list[0:2])
|
py_version = tuple(int(num) for num in py_version_list[0:2])
|
||||||
venv_name = 'zulip-api-py{}-venv'.format(py_version[0])
|
venv_name = "zulip-api-py{}-venv".format(py_version[0])
|
||||||
|
|
||||||
if py_version <= (3, 1) and (not options.force):
|
if py_version <= (3, 1) and (not options.force):
|
||||||
print(
|
print(
|
||||||
|
@ -53,7 +53,7 @@ the Python version this command is executed with."""
|
||||||
venv_dir = os.path.join(base_dir, venv_name)
|
venv_dir = os.path.join(base_dir, venv_name)
|
||||||
if not os.path.isdir(venv_dir):
|
if not os.path.isdir(venv_dir):
|
||||||
try:
|
try:
|
||||||
return_code = subprocess.call([options.python_interpreter, '-m', 'venv', venv_dir])
|
return_code = subprocess.call([options.python_interpreter, "-m", "venv", venv_dir])
|
||||||
except OSError:
|
except OSError:
|
||||||
print(
|
print(
|
||||||
"{red}Installation with venv failed. Probable errors are: "
|
"{red}Installation with venv failed. Probable errors are: "
|
||||||
|
@ -77,34 +77,34 @@ the Python version this command is executed with."""
|
||||||
else:
|
else:
|
||||||
print("Virtualenv already exists.")
|
print("Virtualenv already exists.")
|
||||||
|
|
||||||
if os.path.isdir(os.path.join(venv_dir, 'Scripts')):
|
if os.path.isdir(os.path.join(venv_dir, "Scripts")):
|
||||||
# POSIX compatibility layer and Linux environment emulation for Windows
|
# POSIX compatibility layer and Linux environment emulation for Windows
|
||||||
# venv uses /Scripts instead of /bin on Windows cmd and Power Shell.
|
# venv uses /Scripts instead of /bin on Windows cmd and Power Shell.
|
||||||
# Read https://docs.python.org/3/library/venv.html
|
# Read https://docs.python.org/3/library/venv.html
|
||||||
venv_exec_dir = 'Scripts'
|
venv_exec_dir = "Scripts"
|
||||||
else:
|
else:
|
||||||
venv_exec_dir = 'bin'
|
venv_exec_dir = "bin"
|
||||||
|
|
||||||
# On OS X, ensure we use the virtualenv version of the python binary for
|
# On OS X, ensure we use the virtualenv version of the python binary for
|
||||||
# future subprocesses instead of the version that this script was launched with. See
|
# future subprocesses instead of the version that this script was launched with. See
|
||||||
# https://stackoverflow.com/questions/26323852/whats-the-meaning-of-pyvenv-launcher-environment-variable
|
# https://stackoverflow.com/questions/26323852/whats-the-meaning-of-pyvenv-launcher-environment-variable
|
||||||
if '__PYVENV_LAUNCHER__' in os.environ:
|
if "__PYVENV_LAUNCHER__" in os.environ:
|
||||||
del os.environ['__PYVENV_LAUNCHER__']
|
del os.environ["__PYVENV_LAUNCHER__"]
|
||||||
|
|
||||||
# In order to install all required packages for the venv, `pip` needs to be executed by
|
# In order to install all required packages for the venv, `pip` needs to be executed by
|
||||||
# the venv's Python interpreter. `--prefix venv_dir` ensures that all modules are installed
|
# the venv's Python interpreter. `--prefix venv_dir` ensures that all modules are installed
|
||||||
# in the right place.
|
# in the right place.
|
||||||
def install_dependencies(requirements_filename):
|
def install_dependencies(requirements_filename):
|
||||||
pip_path = os.path.join(venv_dir, venv_exec_dir, 'pip')
|
pip_path = os.path.join(venv_dir, venv_exec_dir, "pip")
|
||||||
# We first install a modern version of pip that supports --prefix
|
# We first install a modern version of pip that supports --prefix
|
||||||
subprocess.call([pip_path, 'install', 'pip>=9.0'])
|
subprocess.call([pip_path, "install", "pip>=9.0"])
|
||||||
if subprocess.call(
|
if subprocess.call(
|
||||||
[
|
[
|
||||||
pip_path,
|
pip_path,
|
||||||
'install',
|
"install",
|
||||||
'--prefix',
|
"--prefix",
|
||||||
venv_dir,
|
venv_dir,
|
||||||
'-r',
|
"-r",
|
||||||
os.path.join(base_dir, requirements_filename),
|
os.path.join(base_dir, requirements_filename),
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
|
@ -114,7 +114,7 @@ the Python version this command is executed with."""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
install_dependencies('requirements.txt')
|
install_dependencies("requirements.txt")
|
||||||
|
|
||||||
# Install all requirements for all bots. get_bot_paths()
|
# Install all requirements for all bots. get_bot_paths()
|
||||||
# has requirements that must be satisfied prior to calling
|
# has requirements that must be satisfied prior to calling
|
||||||
|
@ -127,15 +127,15 @@ the Python version this command is executed with."""
|
||||||
relative_path = os.path.join(*path_split)
|
relative_path = os.path.join(*path_split)
|
||||||
install_dependencies(relative_path)
|
install_dependencies(relative_path)
|
||||||
|
|
||||||
print(green + 'Success!' + end_format)
|
print(green + "Success!" + end_format)
|
||||||
|
|
||||||
activate_command = os.path.join(base_dir, venv_dir, venv_exec_dir, 'activate')
|
activate_command = os.path.join(base_dir, venv_dir, venv_exec_dir, "activate")
|
||||||
# We make the path look like a Unix path, because most Windows users
|
# We make the path look like a Unix path, because most Windows users
|
||||||
# are likely to be running in a bash shell.
|
# are likely to be running in a bash shell.
|
||||||
activate_command = activate_command.replace(os.sep, '/')
|
activate_command = activate_command.replace(os.sep, "/")
|
||||||
print('\nRun the following to enter into the virtualenv:\n')
|
print("\nRun the following to enter into the virtualenv:\n")
|
||||||
print(bold + ' source ' + activate_command + end_format + "\n")
|
print(bold + " source " + activate_command + end_format + "\n")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -25,7 +25,7 @@ def cd(newdir):
|
||||||
|
|
||||||
|
|
||||||
def _generate_dist(dist_type, setup_file, package_name, setup_args):
|
def _generate_dist(dist_type, setup_file, package_name, setup_args):
|
||||||
message = 'Generating {dist_type} for {package_name}.'.format(
|
message = "Generating {dist_type} for {package_name}.".format(
|
||||||
dist_type=dist_type,
|
dist_type=dist_type,
|
||||||
package_name=package_name,
|
package_name=package_name,
|
||||||
)
|
)
|
||||||
|
@ -35,7 +35,7 @@ def _generate_dist(dist_type, setup_file, package_name, setup_args):
|
||||||
with cd(setup_dir):
|
with cd(setup_dir):
|
||||||
setuptools.sandbox.run_setup(setup_file, setup_args)
|
setuptools.sandbox.run_setup(setup_file, setup_args)
|
||||||
|
|
||||||
message = '{dist_type} for {package_name} generated under {dir}.\n'.format(
|
message = "{dist_type} for {package_name} generated under {dir}.\n".format(
|
||||||
dist_type=dist_type,
|
dist_type=dist_type,
|
||||||
package_name=package_name,
|
package_name=package_name,
|
||||||
dir=setup_dir,
|
dir=setup_dir,
|
||||||
|
@ -45,13 +45,13 @@ def _generate_dist(dist_type, setup_file, package_name, setup_args):
|
||||||
|
|
||||||
def generate_bdist_wheel(setup_file, package_name, universal=False):
|
def generate_bdist_wheel(setup_file, package_name, universal=False):
|
||||||
if universal:
|
if universal:
|
||||||
_generate_dist('bdist_wheel', setup_file, package_name, ['bdist_wheel', '--universal'])
|
_generate_dist("bdist_wheel", setup_file, package_name, ["bdist_wheel", "--universal"])
|
||||||
else:
|
else:
|
||||||
_generate_dist('bdist_wheel', setup_file, package_name, ['bdist_wheel'])
|
_generate_dist("bdist_wheel", setup_file, package_name, ["bdist_wheel"])
|
||||||
|
|
||||||
|
|
||||||
def twine_upload(dist_dirs):
|
def twine_upload(dist_dirs):
|
||||||
message = 'Uploading distributions under the following directories:'
|
message = "Uploading distributions under the following directories:"
|
||||||
print(crayons.green(message, bold=True))
|
print(crayons.green(message, bold=True))
|
||||||
for dist_dir in dist_dirs:
|
for dist_dir in dist_dirs:
|
||||||
print(crayons.yellow(dist_dir))
|
print(crayons.yellow(dist_dir))
|
||||||
|
@ -59,14 +59,14 @@ def twine_upload(dist_dirs):
|
||||||
|
|
||||||
|
|
||||||
def cleanup(package_dir):
|
def cleanup(package_dir):
|
||||||
build_dir = os.path.join(package_dir, 'build')
|
build_dir = os.path.join(package_dir, "build")
|
||||||
temp_dir = os.path.join(package_dir, 'temp')
|
temp_dir = os.path.join(package_dir, "temp")
|
||||||
dist_dir = os.path.join(package_dir, 'dist')
|
dist_dir = os.path.join(package_dir, "dist")
|
||||||
egg_info = os.path.join(package_dir, '{}.egg-info'.format(os.path.basename(package_dir)))
|
egg_info = os.path.join(package_dir, "{}.egg-info".format(os.path.basename(package_dir)))
|
||||||
|
|
||||||
def _rm_if_it_exists(directory):
|
def _rm_if_it_exists(directory):
|
||||||
if os.path.isdir(directory):
|
if os.path.isdir(directory):
|
||||||
print(crayons.green('Removing {}/*'.format(directory), bold=True))
|
print(crayons.green("Removing {}/*".format(directory), bold=True))
|
||||||
shutil.rmtree(directory)
|
shutil.rmtree(directory)
|
||||||
|
|
||||||
_rm_if_it_exists(build_dir)
|
_rm_if_it_exists(build_dir)
|
||||||
|
@ -77,11 +77,11 @@ def cleanup(package_dir):
|
||||||
|
|
||||||
def set_variable(fp, variable, value):
|
def set_variable(fp, variable, value):
|
||||||
fh, temp_abs_path = tempfile.mkstemp()
|
fh, temp_abs_path = tempfile.mkstemp()
|
||||||
with os.fdopen(fh, 'w') as new_file, open(fp) as old_file:
|
with os.fdopen(fh, "w") as new_file, open(fp) as old_file:
|
||||||
for line in old_file:
|
for line in old_file:
|
||||||
if line.startswith(variable):
|
if line.startswith(variable):
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
template = '{variable} = {value}\n'
|
template = "{variable} = {value}\n"
|
||||||
else:
|
else:
|
||||||
template = '{variable} = "{value}"\n'
|
template = '{variable} = "{value}"\n'
|
||||||
new_file.write(template.format(variable=variable, value=value))
|
new_file.write(template.format(variable=variable, value=value))
|
||||||
|
@ -91,22 +91,22 @@ def set_variable(fp, variable, value):
|
||||||
os.remove(fp)
|
os.remove(fp)
|
||||||
shutil.move(temp_abs_path, fp)
|
shutil.move(temp_abs_path, fp)
|
||||||
|
|
||||||
message = 'Set {variable} in {fp} to {value}.'.format(fp=fp, variable=variable, value=value)
|
message = "Set {variable} in {fp} to {value}.".format(fp=fp, variable=variable, value=value)
|
||||||
print(crayons.white(message, bold=True))
|
print(crayons.white(message, bold=True))
|
||||||
|
|
||||||
|
|
||||||
def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag):
|
def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag):
|
||||||
common = os.path.join(zulip_repo_dir, 'requirements', 'common.in')
|
common = os.path.join(zulip_repo_dir, "requirements", "common.in")
|
||||||
prod = os.path.join(zulip_repo_dir, 'requirements', 'prod.txt')
|
prod = os.path.join(zulip_repo_dir, "requirements", "prod.txt")
|
||||||
dev = os.path.join(zulip_repo_dir, 'requirements', 'dev.txt')
|
dev = os.path.join(zulip_repo_dir, "requirements", "dev.txt")
|
||||||
|
|
||||||
def _edit_reqs_file(reqs, zulip_bots_line, zulip_line):
|
def _edit_reqs_file(reqs, zulip_bots_line, zulip_line):
|
||||||
fh, temp_abs_path = tempfile.mkstemp()
|
fh, temp_abs_path = tempfile.mkstemp()
|
||||||
with os.fdopen(fh, 'w') as new_file, open(reqs) as old_file:
|
with os.fdopen(fh, "w") as new_file, open(reqs) as old_file:
|
||||||
for line in old_file:
|
for line in old_file:
|
||||||
if 'python-zulip-api' in line and 'zulip==' in line:
|
if "python-zulip-api" in line and "zulip==" in line:
|
||||||
new_file.write(zulip_line)
|
new_file.write(zulip_line)
|
||||||
elif 'python-zulip-api' in line and 'zulip_bots' in line:
|
elif "python-zulip-api" in line and "zulip_bots" in line:
|
||||||
new_file.write(zulip_bots_line)
|
new_file.write(zulip_bots_line)
|
||||||
else:
|
else:
|
||||||
new_file.write(line)
|
new_file.write(line)
|
||||||
|
@ -114,10 +114,10 @@ def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag):
|
||||||
os.remove(reqs)
|
os.remove(reqs)
|
||||||
shutil.move(temp_abs_path, reqs)
|
shutil.move(temp_abs_path, reqs)
|
||||||
|
|
||||||
url_zulip = 'git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}_git&subdirectory={name}\n'
|
url_zulip = "git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}_git&subdirectory={name}\n"
|
||||||
url_zulip_bots = 'git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}+git&subdirectory={name}\n'
|
url_zulip_bots = "git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}+git&subdirectory={name}\n"
|
||||||
zulip_bots_line = url_zulip_bots.format(tag=hash_or_tag, name='zulip_bots', version=version)
|
zulip_bots_line = url_zulip_bots.format(tag=hash_or_tag, name="zulip_bots", version=version)
|
||||||
zulip_line = url_zulip.format(tag=hash_or_tag, name='zulip', version=version)
|
zulip_line = url_zulip.format(tag=hash_or_tag, name="zulip", version=version)
|
||||||
|
|
||||||
_edit_reqs_file(prod, zulip_bots_line, zulip_line)
|
_edit_reqs_file(prod, zulip_bots_line, zulip_line)
|
||||||
_edit_reqs_file(dev, zulip_bots_line, zulip_line)
|
_edit_reqs_file(dev, zulip_bots_line, zulip_line)
|
||||||
|
@ -127,11 +127,11 @@ def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag):
|
||||||
|
|
||||||
_edit_reqs_file(
|
_edit_reqs_file(
|
||||||
common,
|
common,
|
||||||
editable_zulip_bots.format(tag=hash_or_tag, name='zulip_bots', version=version),
|
editable_zulip_bots.format(tag=hash_or_tag, name="zulip_bots", version=version),
|
||||||
editable_zulip.format(tag=hash_or_tag, name='zulip', version=version),
|
editable_zulip.format(tag=hash_or_tag, name="zulip", version=version),
|
||||||
)
|
)
|
||||||
|
|
||||||
message = 'Updated zulip API package requirements in the main repo.'
|
message = "Updated zulip API package requirements in the main repo."
|
||||||
print(crayons.white(message, bold=True))
|
print(crayons.white(message, bold=True))
|
||||||
|
|
||||||
|
|
||||||
|
@ -177,39 +177,39 @@ And you're done! Congrats!
|
||||||
parser = argparse.ArgumentParser(usage=usage)
|
parser = argparse.ArgumentParser(usage=usage)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--cleanup',
|
"--cleanup",
|
||||||
'-c',
|
"-c",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help='Remove build directories (dist/, build/, egg-info/, etc).',
|
help="Remove build directories (dist/, build/, egg-info/, etc).",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--build',
|
"--build",
|
||||||
'-b',
|
"-b",
|
||||||
metavar='VERSION_NUM',
|
metavar="VERSION_NUM",
|
||||||
help=(
|
help=(
|
||||||
'Build sdists and wheels for all packages with the'
|
"Build sdists and wheels for all packages with the"
|
||||||
'specified version number.'
|
"specified version number."
|
||||||
' sdists and wheels are stored in <package_name>/dist/*.'
|
" sdists and wheels are stored in <package_name>/dist/*."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--release',
|
"--release",
|
||||||
'-r',
|
"-r",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help='Upload the packages to PyPA using twine.',
|
help="Upload the packages to PyPA using twine.",
|
||||||
)
|
)
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest='subcommand')
|
subparsers = parser.add_subparsers(dest="subcommand")
|
||||||
parser_main_repo = subparsers.add_parser(
|
parser_main_repo = subparsers.add_parser(
|
||||||
'update-main-repo', help='Update the zulip/requirements/* in the main zulip repo.'
|
"update-main-repo", help="Update the zulip/requirements/* in the main zulip repo."
|
||||||
)
|
)
|
||||||
parser_main_repo.add_argument('repo', metavar='PATH_TO_ZULIP_DIR')
|
parser_main_repo.add_argument("repo", metavar="PATH_TO_ZULIP_DIR")
|
||||||
parser_main_repo.add_argument('version', metavar='version number of the packages')
|
parser_main_repo.add_argument("version", metavar="version number of the packages")
|
||||||
parser_main_repo.add_argument('--hash', metavar='COMMIT_HASH')
|
parser_main_repo.add_argument("--hash", metavar="COMMIT_HASH")
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
@ -217,7 +217,7 @@ And you're done! Congrats!
|
||||||
def main():
|
def main():
|
||||||
options = parse_args()
|
options = parse_args()
|
||||||
|
|
||||||
glob_pattern = os.path.join(REPO_DIR, '*', 'setup.py')
|
glob_pattern = os.path.join(REPO_DIR, "*", "setup.py")
|
||||||
setup_py_files = glob.glob(glob_pattern)
|
setup_py_files = glob.glob(glob_pattern)
|
||||||
|
|
||||||
if options.cleanup:
|
if options.cleanup:
|
||||||
|
@ -230,30 +230,30 @@ def main():
|
||||||
for package_dir in package_dirs:
|
for package_dir in package_dirs:
|
||||||
cleanup(package_dir)
|
cleanup(package_dir)
|
||||||
|
|
||||||
zulip_init = os.path.join(REPO_DIR, 'zulip', 'zulip', '__init__.py')
|
zulip_init = os.path.join(REPO_DIR, "zulip", "zulip", "__init__.py")
|
||||||
set_variable(zulip_init, '__version__', options.build)
|
set_variable(zulip_init, "__version__", options.build)
|
||||||
bots_setup = os.path.join(REPO_DIR, 'zulip_bots', 'setup.py')
|
bots_setup = os.path.join(REPO_DIR, "zulip_bots", "setup.py")
|
||||||
set_variable(bots_setup, 'ZULIP_BOTS_VERSION', options.build)
|
set_variable(bots_setup, "ZULIP_BOTS_VERSION", options.build)
|
||||||
set_variable(bots_setup, 'IS_PYPA_PACKAGE', True)
|
set_variable(bots_setup, "IS_PYPA_PACKAGE", True)
|
||||||
botserver_setup = os.path.join(REPO_DIR, 'zulip_botserver', 'setup.py')
|
botserver_setup = os.path.join(REPO_DIR, "zulip_botserver", "setup.py")
|
||||||
set_variable(botserver_setup, 'ZULIP_BOTSERVER_VERSION', options.build)
|
set_variable(botserver_setup, "ZULIP_BOTSERVER_VERSION", options.build)
|
||||||
|
|
||||||
for setup_file in setup_py_files:
|
for setup_file in setup_py_files:
|
||||||
package_name = os.path.basename(os.path.dirname(setup_file))
|
package_name = os.path.basename(os.path.dirname(setup_file))
|
||||||
generate_bdist_wheel(setup_file, package_name)
|
generate_bdist_wheel(setup_file, package_name)
|
||||||
|
|
||||||
set_variable(bots_setup, 'IS_PYPA_PACKAGE', False)
|
set_variable(bots_setup, "IS_PYPA_PACKAGE", False)
|
||||||
|
|
||||||
if options.release:
|
if options.release:
|
||||||
dist_dirs = glob.glob(os.path.join(REPO_DIR, '*', 'dist', '*'))
|
dist_dirs = glob.glob(os.path.join(REPO_DIR, "*", "dist", "*"))
|
||||||
twine_upload(dist_dirs)
|
twine_upload(dist_dirs)
|
||||||
|
|
||||||
if options.subcommand == 'update-main-repo':
|
if options.subcommand == "update-main-repo":
|
||||||
if options.hash:
|
if options.hash:
|
||||||
update_requirements_in_zulip_repo(options.repo, options.version, options.hash)
|
update_requirements_in_zulip_repo(options.repo, options.version, options.hash)
|
||||||
else:
|
else:
|
||||||
update_requirements_in_zulip_repo(options.repo, options.version, options.version)
|
update_requirements_in_zulip_repo(options.repo, options.version, options.version)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
38
tools/review
38
tools/review
|
@ -5,63 +5,63 @@ import sys
|
||||||
|
|
||||||
|
|
||||||
def exit(message: str) -> None:
|
def exit(message: str) -> None:
|
||||||
print('PROBLEM!')
|
print("PROBLEM!")
|
||||||
print(message)
|
print(message)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def run(command: str) -> None:
|
def run(command: str) -> None:
|
||||||
print('\n>>> ' + command)
|
print("\n>>> " + command)
|
||||||
subprocess.check_call(command.split())
|
subprocess.check_call(command.split())
|
||||||
|
|
||||||
|
|
||||||
def check_output(command: str) -> str:
|
def check_output(command: str) -> str:
|
||||||
return subprocess.check_output(command.split()).decode('ascii')
|
return subprocess.check_output(command.split()).decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
def get_git_branch() -> str:
|
def get_git_branch() -> str:
|
||||||
command = 'git rev-parse --abbrev-ref HEAD'
|
command = "git rev-parse --abbrev-ref HEAD"
|
||||||
output = check_output(command)
|
output = check_output(command)
|
||||||
return output.strip()
|
return output.strip()
|
||||||
|
|
||||||
|
|
||||||
def check_git_pristine() -> None:
|
def check_git_pristine() -> None:
|
||||||
command = 'git status --porcelain'
|
command = "git status --porcelain"
|
||||||
output = check_output(command)
|
output = check_output(command)
|
||||||
if output.strip():
|
if output.strip():
|
||||||
exit('Git is not pristine:\n' + output)
|
exit("Git is not pristine:\n" + output)
|
||||||
|
|
||||||
|
|
||||||
def ensure_on_clean_master() -> None:
|
def ensure_on_clean_master() -> None:
|
||||||
branch = get_git_branch()
|
branch = get_git_branch()
|
||||||
if branch != 'master':
|
if branch != "master":
|
||||||
exit('You are still on a feature branch: %s' % (branch,))
|
exit("You are still on a feature branch: %s" % (branch,))
|
||||||
check_git_pristine()
|
check_git_pristine()
|
||||||
run('git fetch upstream master')
|
run("git fetch upstream master")
|
||||||
run('git rebase upstream/master')
|
run("git rebase upstream/master")
|
||||||
|
|
||||||
|
|
||||||
def create_pull_branch(pull_id: int) -> None:
|
def create_pull_branch(pull_id: int) -> None:
|
||||||
run('git fetch upstream pull/%d/head' % (pull_id,))
|
run("git fetch upstream pull/%d/head" % (pull_id,))
|
||||||
run('git checkout -B review-%s FETCH_HEAD' % (pull_id,))
|
run("git checkout -B review-%s FETCH_HEAD" % (pull_id,))
|
||||||
run('git rebase upstream/master')
|
run("git rebase upstream/master")
|
||||||
run('git log upstream/master.. --oneline')
|
run("git log upstream/master.. --oneline")
|
||||||
run('git diff upstream/master.. --name-status')
|
run("git diff upstream/master.. --name-status")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print('PR: %d' % (pull_id,))
|
print("PR: %d" % (pull_id,))
|
||||||
print(subprocess.check_output(['git', 'log', 'HEAD~..', '--pretty=format:Author: %an']))
|
print(subprocess.check_output(["git", "log", "HEAD~..", "--pretty=format:Author: %an"]))
|
||||||
|
|
||||||
|
|
||||||
def review_pr() -> None:
|
def review_pr() -> None:
|
||||||
try:
|
try:
|
||||||
pull_id = int(sys.argv[1])
|
pull_id = int(sys.argv[1])
|
||||||
except Exception:
|
except Exception:
|
||||||
exit('please provide an integer pull request id')
|
exit("please provide an integer pull request id")
|
||||||
|
|
||||||
ensure_on_clean_master()
|
ensure_on_clean_master()
|
||||||
create_pull_branch(pull_id)
|
create_pull_branch(pull_id)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
review_pr()
|
review_pr()
|
||||||
|
|
|
@ -104,54 +104,54 @@ force_include = [
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.")
|
parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'targets',
|
"targets",
|
||||||
nargs='*',
|
nargs="*",
|
||||||
default=[],
|
default=[],
|
||||||
help="""files and directories to include in the result.
|
help="""files and directories to include in the result.
|
||||||
If this is not specified, the current directory is used""",
|
If this is not specified, the current directory is used""",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-m', '--modified', action='store_true', default=False, help='list only modified files'
|
"-m", "--modified", action="store_true", default=False, help="list only modified files"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-a',
|
"-a",
|
||||||
'--all',
|
"--all",
|
||||||
dest='all',
|
dest="all",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="""run mypy on all python files, ignoring the exclude list.
|
help="""run mypy on all python files, ignoring the exclude list.
|
||||||
This is useful if you have to find out which files fail mypy check.""",
|
This is useful if you have to find out which files fail mypy check.""",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--no-disallow-untyped-defs',
|
"--no-disallow-untyped-defs",
|
||||||
dest='disallow_untyped_defs',
|
dest="disallow_untyped_defs",
|
||||||
action='store_false',
|
action="store_false",
|
||||||
default=True,
|
default=True,
|
||||||
help="""Don't throw errors when functions are not annotated""",
|
help="""Don't throw errors when functions are not annotated""",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--scripts-only',
|
"--scripts-only",
|
||||||
dest='scripts_only',
|
dest="scripts_only",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="""Only type check extensionless python scripts""",
|
help="""Only type check extensionless python scripts""",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--warn-unused-ignores',
|
"--warn-unused-ignores",
|
||||||
dest='warn_unused_ignores',
|
dest="warn_unused_ignores",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="""Use the --warn-unused-ignores flag with mypy""",
|
help="""Use the --warn-unused-ignores flag with mypy""",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--no-ignore-missing-imports',
|
"--no-ignore-missing-imports",
|
||||||
dest='ignore_missing_imports',
|
dest="ignore_missing_imports",
|
||||||
action='store_false',
|
action="store_false",
|
||||||
default=True,
|
default=True,
|
||||||
help="""Don't use the --ignore-missing-imports flag with mypy""",
|
help="""Don't use the --ignore-missing-imports flag with mypy""",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--quick', action='store_true', default=False, help="""Use the --quick flag with mypy"""
|
"--quick", action="store_true", default=False, help="""Use the --quick flag with mypy"""
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -163,10 +163,10 @@ files_dict = cast(
|
||||||
Dict[str, List[str]],
|
Dict[str, List[str]],
|
||||||
lister.list_files(
|
lister.list_files(
|
||||||
targets=args.targets,
|
targets=args.targets,
|
||||||
ftypes=['py', 'pyi'],
|
ftypes=["py", "pyi"],
|
||||||
use_shebang=True,
|
use_shebang=True,
|
||||||
modified_only=args.modified,
|
modified_only=args.modified,
|
||||||
exclude=exclude + ['stubs'],
|
exclude=exclude + ["stubs"],
|
||||||
group_by_ftype=True,
|
group_by_ftype=True,
|
||||||
extless_only=args.scripts_only,
|
extless_only=args.scripts_only,
|
||||||
),
|
),
|
||||||
|
@ -174,18 +174,18 @@ files_dict = cast(
|
||||||
|
|
||||||
for inpath in force_include:
|
for inpath in force_include:
|
||||||
try:
|
try:
|
||||||
ext = os.path.splitext(inpath)[1].split('.')[1]
|
ext = os.path.splitext(inpath)[1].split(".")[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
ext = 'py' # type: str
|
ext = "py" # type: str
|
||||||
files_dict[ext].append(inpath)
|
files_dict[ext].append(inpath)
|
||||||
|
|
||||||
pyi_files = set(files_dict['pyi'])
|
pyi_files = set(files_dict["pyi"])
|
||||||
python_files = [
|
python_files = [
|
||||||
fpath for fpath in files_dict['py'] if not fpath.endswith('.py') or fpath + 'i' not in pyi_files
|
fpath for fpath in files_dict["py"] if not fpath.endswith(".py") or fpath + "i" not in pyi_files
|
||||||
]
|
]
|
||||||
|
|
||||||
repo_python_files = OrderedDict(
|
repo_python_files = OrderedDict(
|
||||||
[('zulip', []), ('zulip_bots', []), ('zulip_botserver', []), ('tools', [])]
|
[("zulip", []), ("zulip_bots", []), ("zulip_botserver", []), ("tools", [])]
|
||||||
)
|
)
|
||||||
for file_path in python_files:
|
for file_path in python_files:
|
||||||
repo = PurePath(file_path).parts[0]
|
repo = PurePath(file_path).parts[0]
|
||||||
|
|
|
@ -13,45 +13,45 @@ os.chdir(os.path.dirname(TOOLS_DIR))
|
||||||
def handle_input_and_run_tests_for_package(package_name, path_list):
|
def handle_input_and_run_tests_for_package(package_name, path_list):
|
||||||
parser = argparse.ArgumentParser(description="Run tests for {}.".format(package_name))
|
parser = argparse.ArgumentParser(description="Run tests for {}.".format(package_name))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--coverage',
|
"--coverage",
|
||||||
nargs='?',
|
nargs="?",
|
||||||
const=True,
|
const=True,
|
||||||
default=False,
|
default=False,
|
||||||
help='compute test coverage (--coverage combine to combine with previous reports)',
|
help="compute test coverage (--coverage combine to combine with previous reports)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--pytest', '-p', default=False, action='store_true', help="run tests with pytest"
|
"--pytest", "-p", default=False, action="store_true", help="run tests with pytest"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--verbose',
|
"--verbose",
|
||||||
'-v',
|
"-v",
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='show verbose output (with pytest)',
|
help="show verbose output (with pytest)",
|
||||||
)
|
)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
test_session_title = ' Running tests for {} '.format(package_name)
|
test_session_title = " Running tests for {} ".format(package_name)
|
||||||
header = test_session_title.center(shutil.get_terminal_size().columns, '#')
|
header = test_session_title.center(shutil.get_terminal_size().columns, "#")
|
||||||
print(header)
|
print(header)
|
||||||
|
|
||||||
if options.coverage:
|
if options.coverage:
|
||||||
import coverage
|
import coverage
|
||||||
|
|
||||||
cov = coverage.Coverage(config_file="tools/.coveragerc")
|
cov = coverage.Coverage(config_file="tools/.coveragerc")
|
||||||
if options.coverage == 'combine':
|
if options.coverage == "combine":
|
||||||
cov.load()
|
cov.load()
|
||||||
cov.start()
|
cov.start()
|
||||||
|
|
||||||
if options.pytest:
|
if options.pytest:
|
||||||
location_to_run_in = os.path.join(TOOLS_DIR, '..', *path_list)
|
location_to_run_in = os.path.join(TOOLS_DIR, "..", *path_list)
|
||||||
paths_to_test = ['.']
|
paths_to_test = ["."]
|
||||||
pytest_options = [
|
pytest_options = [
|
||||||
'-s', # show output from tests; this hides the progress bar though
|
"-s", # show output from tests; this hides the progress bar though
|
||||||
'-x', # stop on first test failure
|
"-x", # stop on first test failure
|
||||||
'--ff', # runs last failure first
|
"--ff", # runs last failure first
|
||||||
]
|
]
|
||||||
pytest_options += ['-v'] if options.verbose else []
|
pytest_options += ["-v"] if options.verbose else []
|
||||||
os.chdir(location_to_run_in)
|
os.chdir(location_to_run_in)
|
||||||
result = pytest.main(paths_to_test + pytest_options)
|
result = pytest.main(paths_to_test + pytest_options)
|
||||||
if result != 0:
|
if result != 0:
|
||||||
|
|
|
@ -32,35 +32,35 @@ the tests for xkcd and wikipedia bots):
|
||||||
parser = argparse.ArgumentParser(description=description)
|
parser = argparse.ArgumentParser(description=description)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'bots_to_test',
|
"bots_to_test",
|
||||||
metavar='bot',
|
metavar="bot",
|
||||||
nargs='*',
|
nargs="*",
|
||||||
default=[],
|
default=[],
|
||||||
help='specific bots to test (default is all)',
|
help="specific bots to test (default is all)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--coverage',
|
"--coverage",
|
||||||
nargs='?',
|
nargs="?",
|
||||||
const=True,
|
const=True,
|
||||||
default=False,
|
default=False,
|
||||||
help='compute test coverage (--coverage combine to combine with previous reports)',
|
help="compute test coverage (--coverage combine to combine with previous reports)",
|
||||||
)
|
)
|
||||||
parser.add_argument('--exclude', metavar='bot', nargs='*', default=[], help='bot(s) to exclude')
|
parser.add_argument("--exclude", metavar="bot", nargs="*", default=[], help="bot(s) to exclude")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--error-on-no-init',
|
"--error-on-no-init",
|
||||||
default=False,
|
default=False,
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="whether to exit if a bot has tests which won't run due to no __init__.py",
|
help="whether to exit if a bot has tests which won't run due to no __init__.py",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--pytest', '-p', default=False, action='store_true', help="run tests with pytest"
|
"--pytest", "-p", default=False, action="store_true", help="run tests with pytest"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--verbose',
|
"--verbose",
|
||||||
'-v',
|
"-v",
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='show verbose output (with pytest)',
|
help="show verbose output (with pytest)",
|
||||||
)
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
@ -69,8 +69,8 @@ def main():
|
||||||
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
|
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
os.chdir(os.path.dirname(TOOLS_DIR))
|
os.chdir(os.path.dirname(TOOLS_DIR))
|
||||||
sys.path.insert(0, TOOLS_DIR)
|
sys.path.insert(0, TOOLS_DIR)
|
||||||
bots_dir = os.path.join(TOOLS_DIR, '..', 'zulip_bots/zulip_bots/bots')
|
bots_dir = os.path.join(TOOLS_DIR, "..", "zulip_bots/zulip_bots/bots")
|
||||||
glob_pattern = bots_dir + '/*/test_*.py'
|
glob_pattern = bots_dir + "/*/test_*.py"
|
||||||
test_modules = glob.glob(glob_pattern)
|
test_modules = glob.glob(glob_pattern)
|
||||||
|
|
||||||
# get only the names of bots that have tests
|
# get only the names of bots that have tests
|
||||||
|
@ -82,7 +82,7 @@ def main():
|
||||||
import coverage
|
import coverage
|
||||||
|
|
||||||
cov = coverage.Coverage(config_file="tools/.coveragerc")
|
cov = coverage.Coverage(config_file="tools/.coveragerc")
|
||||||
if options.coverage == 'combine':
|
if options.coverage == "combine":
|
||||||
cov.load()
|
cov.load()
|
||||||
cov.start()
|
cov.start()
|
||||||
|
|
||||||
|
@ -96,14 +96,14 @@ def main():
|
||||||
bots_to_test = {bot for bot in specified_bots if bot not in options.exclude}
|
bots_to_test = {bot for bot in specified_bots if bot not in options.exclude}
|
||||||
|
|
||||||
if options.pytest:
|
if options.pytest:
|
||||||
excluded_bots = ['merels']
|
excluded_bots = ["merels"]
|
||||||
pytest_bots_to_test = sorted([bot for bot in bots_to_test if bot not in excluded_bots])
|
pytest_bots_to_test = sorted([bot for bot in bots_to_test if bot not in excluded_bots])
|
||||||
pytest_options = [
|
pytest_options = [
|
||||||
'-s', # show output from tests; this hides the progress bar though
|
"-s", # show output from tests; this hides the progress bar though
|
||||||
'-x', # stop on first test failure
|
"-x", # stop on first test failure
|
||||||
'--ff', # runs last failure first
|
"--ff", # runs last failure first
|
||||||
]
|
]
|
||||||
pytest_options += ['-v'] if options.verbose else []
|
pytest_options += ["-v"] if options.verbose else []
|
||||||
os.chdir(bots_dir)
|
os.chdir(bots_dir)
|
||||||
result = pytest.main(pytest_bots_to_test + pytest_options)
|
result = pytest.main(pytest_bots_to_test + pytest_options)
|
||||||
if result != 0:
|
if result != 0:
|
||||||
|
@ -142,5 +142,5 @@ def main():
|
||||||
print("HTML report saved under directory 'htmlcov'.")
|
print("HTML report saved under directory 'htmlcov'.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
|
|
||||||
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
handle_input_and_run_tests_for_package('Botserver', ['zulip_botserver'])
|
handle_input_and_run_tests_for_package("Botserver", ["zulip_botserver"])
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
|
|
||||||
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
handle_input_and_run_tests_for_package('Bot library', ['zulip_bots', 'zulip_bots', 'tests'])
|
handle_input_and_run_tests_for_package("Bot library", ["zulip_bots", "zulip_bots", "tests"])
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
|
|
||||||
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
handle_input_and_run_tests_for_package('API', ['zulip'])
|
handle_input_and_run_tests_for_package("API", ["zulip"])
|
||||||
|
|
|
@ -71,10 +71,10 @@ if __name__ == "__main__":
|
||||||
all topics within the stream are mirrored as-is without
|
all topics within the stream are mirrored as-is without
|
||||||
translation.
|
translation.
|
||||||
"""
|
"""
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(usage=usage)
|
parser = argparse.ArgumentParser(usage=usage)
|
||||||
parser.add_argument('--stream', action='store_true', help="", default=False)
|
parser.add_argument("--stream", action="store_true", help="", default=False)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
options = interrealm_bridge_config.config
|
options = interrealm_bridge_config.config
|
||||||
|
|
|
@ -29,13 +29,13 @@ if __name__ == "__main__":
|
||||||
parser = zulip.add_default_arguments(
|
parser = zulip.add_default_arguments(
|
||||||
argparse.ArgumentParser(usage=usage), allow_provisioning=True
|
argparse.ArgumentParser(usage=usage), allow_provisioning=True
|
||||||
)
|
)
|
||||||
parser.add_argument('--irc-server', default=None)
|
parser.add_argument("--irc-server", default=None)
|
||||||
parser.add_argument('--port', default=6667)
|
parser.add_argument("--port", default=6667)
|
||||||
parser.add_argument('--nick-prefix', default=None)
|
parser.add_argument("--nick-prefix", default=None)
|
||||||
parser.add_argument('--channel', default=None)
|
parser.add_argument("--channel", default=None)
|
||||||
parser.add_argument('--stream', default="general")
|
parser.add_argument("--stream", default="general")
|
||||||
parser.add_argument('--topic', default="IRC")
|
parser.add_argument("--topic", default="IRC")
|
||||||
parser.add_argument('--nickserv-pw', default='')
|
parser.add_argument("--nickserv-pw", default="")
|
||||||
|
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
# Setting the client to irc_mirror is critical for this to work
|
# Setting the client to irc_mirror is critical for this to work
|
||||||
|
|
|
@ -18,7 +18,7 @@ class IRCBot(irc.bot.SingleServerIRCBot):
|
||||||
channel: irc.bot.Channel,
|
channel: irc.bot.Channel,
|
||||||
nickname: str,
|
nickname: str,
|
||||||
server: str,
|
server: str,
|
||||||
nickserv_password: str = '',
|
nickserv_password: str = "",
|
||||||
port: int = 6667,
|
port: int = 6667,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.channel = channel # type: irc.bot.Channel
|
self.channel = channel # type: irc.bot.Channel
|
||||||
|
@ -61,8 +61,8 @@ class IRCBot(irc.bot.SingleServerIRCBot):
|
||||||
|
|
||||||
def on_welcome(self, c: ServerConnection, e: Event) -> None:
|
def on_welcome(self, c: ServerConnection, e: Event) -> None:
|
||||||
if len(self.nickserv_password) > 0:
|
if len(self.nickserv_password) > 0:
|
||||||
msg = 'identify %s' % (self.nickserv_password,)
|
msg = "identify %s" % (self.nickserv_password,)
|
||||||
c.privmsg('NickServ', msg)
|
c.privmsg("NickServ", msg)
|
||||||
c.join(self.channel)
|
c.join(self.channel)
|
||||||
|
|
||||||
def forward_to_irc(msg: Dict[str, Any]) -> None:
|
def forward_to_irc(msg: Dict[str, Any]) -> None:
|
||||||
|
|
|
@ -17,8 +17,8 @@ from requests.exceptions import MissingSchema
|
||||||
|
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
GENERAL_NETWORK_USERNAME_REGEX = '@_?[a-zA-Z0-9]+_([a-zA-Z0-9-_]+):[a-zA-Z0-9.]+'
|
GENERAL_NETWORK_USERNAME_REGEX = "@_?[a-zA-Z0-9]+_([a-zA-Z0-9-_]+):[a-zA-Z0-9.]+"
|
||||||
MATRIX_USERNAME_REGEX = '@([a-zA-Z0-9-_]+):matrix.org'
|
MATRIX_USERNAME_REGEX = "@([a-zA-Z0-9-_]+):matrix.org"
|
||||||
|
|
||||||
# change these templates to change the format of displayed message
|
# change these templates to change the format of displayed message
|
||||||
ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}"
|
ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}"
|
||||||
|
@ -77,10 +77,10 @@ def matrix_to_zulip(
|
||||||
"""
|
"""
|
||||||
content = get_message_content_from_event(event, no_noise)
|
content = get_message_content_from_event(event, no_noise)
|
||||||
|
|
||||||
zulip_bot_user = '@%s:matrix.org' % (matrix_config['username'],)
|
zulip_bot_user = "@%s:matrix.org" % (matrix_config["username"],)
|
||||||
# We do this to identify the messages generated from Zulip -> Matrix
|
# We do this to identify the messages generated from Zulip -> Matrix
|
||||||
# and we make sure we don't forward it again to the Zulip stream.
|
# and we make sure we don't forward it again to the Zulip stream.
|
||||||
not_from_zulip_bot = event['sender'] != zulip_bot_user
|
not_from_zulip_bot = event["sender"] != zulip_bot_user
|
||||||
|
|
||||||
if not_from_zulip_bot and content:
|
if not_from_zulip_bot and content:
|
||||||
try:
|
try:
|
||||||
|
@ -95,31 +95,31 @@ def matrix_to_zulip(
|
||||||
except Exception as exception: # XXX This should be more specific
|
except Exception as exception: # XXX This should be more specific
|
||||||
# Generally raised when user is forbidden
|
# Generally raised when user is forbidden
|
||||||
raise Bridge_ZulipFatalException(exception)
|
raise Bridge_ZulipFatalException(exception)
|
||||||
if result['result'] != 'success':
|
if result["result"] != "success":
|
||||||
# Generally raised when API key is invalid
|
# Generally raised when API key is invalid
|
||||||
raise Bridge_ZulipFatalException(result['msg'])
|
raise Bridge_ZulipFatalException(result["msg"])
|
||||||
|
|
||||||
return _matrix_to_zulip
|
return _matrix_to_zulip
|
||||||
|
|
||||||
|
|
||||||
def get_message_content_from_event(event: Dict[str, Any], no_noise: bool) -> Optional[str]:
|
def get_message_content_from_event(event: Dict[str, Any], no_noise: bool) -> Optional[str]:
|
||||||
irc_nick = shorten_irc_nick(event['sender'])
|
irc_nick = shorten_irc_nick(event["sender"])
|
||||||
if event['type'] == "m.room.member":
|
if event["type"] == "m.room.member":
|
||||||
if no_noise:
|
if no_noise:
|
||||||
return None
|
return None
|
||||||
# Join and leave events can be noisy. They are ignored by default.
|
# Join and leave events can be noisy. They are ignored by default.
|
||||||
# To enable these events pass `no_noise` as `False` as the script argument
|
# To enable these events pass `no_noise` as `False` as the script argument
|
||||||
if event['membership'] == "join":
|
if event["membership"] == "join":
|
||||||
content = ZULIP_MESSAGE_TEMPLATE.format(username=irc_nick, message="joined")
|
content = ZULIP_MESSAGE_TEMPLATE.format(username=irc_nick, message="joined")
|
||||||
elif event['membership'] == "leave":
|
elif event["membership"] == "leave":
|
||||||
content = ZULIP_MESSAGE_TEMPLATE.format(username=irc_nick, message="quit")
|
content = ZULIP_MESSAGE_TEMPLATE.format(username=irc_nick, message="quit")
|
||||||
elif event['type'] == "m.room.message":
|
elif event["type"] == "m.room.message":
|
||||||
if event['content']['msgtype'] == "m.text" or event['content']['msgtype'] == "m.emote":
|
if event["content"]["msgtype"] == "m.text" or event["content"]["msgtype"] == "m.emote":
|
||||||
content = ZULIP_MESSAGE_TEMPLATE.format(
|
content = ZULIP_MESSAGE_TEMPLATE.format(
|
||||||
username=irc_nick, message=event['content']['body']
|
username=irc_nick, message=event["content"]["body"]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
content = event['type']
|
content = event["type"]
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ def zulip_to_matrix(config: Dict[str, Any], room: Any) -> Callable[[Dict[str, An
|
||||||
"""
|
"""
|
||||||
message_valid = check_zulip_message_validity(msg, config)
|
message_valid = check_zulip_message_validity(msg, config)
|
||||||
if message_valid:
|
if message_valid:
|
||||||
matrix_username = msg["sender_full_name"].replace(' ', '')
|
matrix_username = msg["sender_full_name"].replace(" ", "")
|
||||||
matrix_text = MATRIX_MESSAGE_TEMPLATE.format(
|
matrix_text = MATRIX_MESSAGE_TEMPLATE.format(
|
||||||
username=matrix_username, message=msg["content"]
|
username=matrix_username, message=msg["content"]
|
||||||
)
|
)
|
||||||
|
@ -186,25 +186,25 @@ def generate_parser() -> argparse.ArgumentParser:
|
||||||
description=description, formatter_class=argparse.RawTextHelpFormatter
|
description=description, formatter_class=argparse.RawTextHelpFormatter
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-c', '--config', required=False, help="Path to the config file for the bridge."
|
"-c", "--config", required=False, help="Path to the config file for the bridge."
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--write-sample-config',
|
"--write-sample-config",
|
||||||
metavar='PATH',
|
metavar="PATH",
|
||||||
dest='sample_config',
|
dest="sample_config",
|
||||||
help="Generate a configuration template at the specified location.",
|
help="Generate a configuration template at the specified location.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--from-zuliprc',
|
"--from-zuliprc",
|
||||||
metavar='ZULIPRC',
|
metavar="ZULIPRC",
|
||||||
dest='zuliprc',
|
dest="zuliprc",
|
||||||
help="Optional path to zuliprc file for bot, when using --write-sample-config",
|
help="Optional path to zuliprc file for bot, when using --write-sample-config",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--show-join-leave',
|
"--show-join-leave",
|
||||||
dest='no_noise',
|
dest="no_noise",
|
||||||
default=True,
|
default=True,
|
||||||
action='store_false',
|
action="store_false",
|
||||||
help="Enable IRC join/leave events.",
|
help="Enable IRC join/leave events.",
|
||||||
)
|
)
|
||||||
return parser
|
return parser
|
||||||
|
@ -218,7 +218,7 @@ def read_configuration(config_file: str) -> Dict[str, Dict[str, str]]:
|
||||||
except configparser.Error as exception:
|
except configparser.Error as exception:
|
||||||
raise Bridge_ConfigException(str(exception))
|
raise Bridge_ConfigException(str(exception))
|
||||||
|
|
||||||
if set(config.sections()) != {'matrix', 'zulip'}:
|
if set(config.sections()) != {"matrix", "zulip"}:
|
||||||
raise Bridge_ConfigException("Please ensure the configuration has zulip & matrix sections.")
|
raise Bridge_ConfigException("Please ensure the configuration has zulip & matrix sections.")
|
||||||
|
|
||||||
# TODO Could add more checks for configuration content here
|
# TODO Could add more checks for configuration content here
|
||||||
|
@ -235,25 +235,25 @@ def write_sample_config(target_path: str, zuliprc: Optional[str]) -> None:
|
||||||
sample_dict = OrderedDict(
|
sample_dict = OrderedDict(
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
'matrix',
|
"matrix",
|
||||||
OrderedDict(
|
OrderedDict(
|
||||||
(
|
(
|
||||||
('host', 'https://matrix.org'),
|
("host", "https://matrix.org"),
|
||||||
('username', 'username'),
|
("username", "username"),
|
||||||
('password', 'password'),
|
("password", "password"),
|
||||||
('room_id', '#zulip:matrix.org'),
|
("room_id", "#zulip:matrix.org"),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
'zulip',
|
"zulip",
|
||||||
OrderedDict(
|
OrderedDict(
|
||||||
(
|
(
|
||||||
('email', 'glitch-bot@chat.zulip.org'),
|
("email", "glitch-bot@chat.zulip.org"),
|
||||||
('api_key', 'aPiKeY'),
|
("api_key", "aPiKeY"),
|
||||||
('site', 'https://chat.zulip.org'),
|
("site", "https://chat.zulip.org"),
|
||||||
('stream', 'test here'),
|
("stream", "test here"),
|
||||||
('topic', 'matrix'),
|
("topic", "matrix"),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -272,13 +272,13 @@ def write_sample_config(target_path: str, zuliprc: Optional[str]) -> None:
|
||||||
|
|
||||||
# Can add more checks for validity of zuliprc file here
|
# Can add more checks for validity of zuliprc file here
|
||||||
|
|
||||||
sample_dict['zulip']['email'] = zuliprc_config['api']['email']
|
sample_dict["zulip"]["email"] = zuliprc_config["api"]["email"]
|
||||||
sample_dict['zulip']['site'] = zuliprc_config['api']['site']
|
sample_dict["zulip"]["site"] = zuliprc_config["api"]["site"]
|
||||||
sample_dict['zulip']['api_key'] = zuliprc_config['api']['key']
|
sample_dict["zulip"]["api_key"] = zuliprc_config["api"]["key"]
|
||||||
|
|
||||||
sample = configparser.ConfigParser()
|
sample = configparser.ConfigParser()
|
||||||
sample.read_dict(sample_dict)
|
sample.read_dict(sample_dict)
|
||||||
with open(target_path, 'w') as target:
|
with open(target_path, "w") as target:
|
||||||
sample.write(target)
|
sample.write(target)
|
||||||
|
|
||||||
|
|
||||||
|
@ -357,5 +357,5 @@ def main() -> None:
|
||||||
backoff.fail()
|
backoff.fail()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -59,7 +59,7 @@ class MatrixBridgeScriptTests(TestCase):
|
||||||
usage = "usage: {} [-h]".format(script_file)
|
usage = "usage: {} [-h]".format(script_file)
|
||||||
description = "Script to bridge"
|
description = "Script to bridge"
|
||||||
self.assertIn(usage, output_lines[0])
|
self.assertIn(usage, output_lines[0])
|
||||||
blank_lines = [num for num, line in enumerate(output_lines) if line == '']
|
blank_lines = [num for num, line in enumerate(output_lines) if line == ""]
|
||||||
# There should be blank lines in the output
|
# There should be blank lines in the output
|
||||||
self.assertTrue(blank_lines)
|
self.assertTrue(blank_lines)
|
||||||
# There should be finite output
|
# There should be finite output
|
||||||
|
@ -79,9 +79,9 @@ class MatrixBridgeScriptTests(TestCase):
|
||||||
def test_write_sample_config_from_zuliprc(self) -> None:
|
def test_write_sample_config_from_zuliprc(self) -> None:
|
||||||
zuliprc_template = ["[api]", "email={email}", "key={key}", "site={site}"]
|
zuliprc_template = ["[api]", "email={email}", "key={key}", "site={site}"]
|
||||||
zulip_params = {
|
zulip_params = {
|
||||||
'email': 'foo@bar',
|
"email": "foo@bar",
|
||||||
'key': 'some_api_key',
|
"key": "some_api_key",
|
||||||
'site': 'https://some.chat.serverplace',
|
"site": "https://some.chat.serverplace",
|
||||||
}
|
}
|
||||||
with new_temp_dir() as tempdir:
|
with new_temp_dir() as tempdir:
|
||||||
path = os.path.join(tempdir, sample_config_path)
|
path = os.path.join(tempdir, sample_config_path)
|
||||||
|
@ -103,9 +103,9 @@ class MatrixBridgeScriptTests(TestCase):
|
||||||
with open(path) as sample_file:
|
with open(path) as sample_file:
|
||||||
sample_lines = [line.strip() for line in sample_file.readlines()]
|
sample_lines = [line.strip() for line in sample_file.readlines()]
|
||||||
expected_lines = sample_config_text.split("\n")
|
expected_lines = sample_config_text.split("\n")
|
||||||
expected_lines[7] = 'email = {}'.format(zulip_params['email'])
|
expected_lines[7] = "email = {}".format(zulip_params["email"])
|
||||||
expected_lines[8] = 'api_key = {}'.format(zulip_params['key'])
|
expected_lines[8] = "api_key = {}".format(zulip_params["key"])
|
||||||
expected_lines[9] = 'site = {}'.format(zulip_params['site'])
|
expected_lines[9] = "site = {}".format(zulip_params["site"])
|
||||||
self.assertEqual(sample_lines, expected_lines[:-1])
|
self.assertEqual(sample_lines, expected_lines[:-1])
|
||||||
|
|
||||||
def test_detect_zuliprc_does_not_exist(self) -> None:
|
def test_detect_zuliprc_does_not_exist(self) -> None:
|
||||||
|
@ -131,31 +131,31 @@ class MatrixBridgeZulipToMatrixTests(TestCase):
|
||||||
valid_msg = dict(
|
valid_msg = dict(
|
||||||
sender_email="John@Smith.smith", # must not be equal to config:email
|
sender_email="John@Smith.smith", # must not be equal to config:email
|
||||||
type="stream", # Can only mirror Zulip streams
|
type="stream", # Can only mirror Zulip streams
|
||||||
display_recipient=valid_zulip_config['stream'],
|
display_recipient=valid_zulip_config["stream"],
|
||||||
subject=valid_zulip_config['topic'],
|
subject=valid_zulip_config["topic"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_zulip_message_validity_success(self) -> None:
|
def test_zulip_message_validity_success(self) -> None:
|
||||||
zulip_config = self.valid_zulip_config
|
zulip_config = self.valid_zulip_config
|
||||||
msg = self.valid_msg
|
msg = self.valid_msg
|
||||||
# Ensure the test inputs are valid for success
|
# Ensure the test inputs are valid for success
|
||||||
assert msg['sender_email'] != zulip_config['email']
|
assert msg["sender_email"] != zulip_config["email"]
|
||||||
|
|
||||||
self.assertTrue(check_zulip_message_validity(msg, zulip_config))
|
self.assertTrue(check_zulip_message_validity(msg, zulip_config))
|
||||||
|
|
||||||
def test_zulip_message_validity_failure(self) -> None:
|
def test_zulip_message_validity_failure(self) -> None:
|
||||||
zulip_config = self.valid_zulip_config
|
zulip_config = self.valid_zulip_config
|
||||||
|
|
||||||
msg_wrong_stream = dict(self.valid_msg, display_recipient='foo')
|
msg_wrong_stream = dict(self.valid_msg, display_recipient="foo")
|
||||||
self.assertFalse(check_zulip_message_validity(msg_wrong_stream, zulip_config))
|
self.assertFalse(check_zulip_message_validity(msg_wrong_stream, zulip_config))
|
||||||
|
|
||||||
msg_wrong_topic = dict(self.valid_msg, subject='foo')
|
msg_wrong_topic = dict(self.valid_msg, subject="foo")
|
||||||
self.assertFalse(check_zulip_message_validity(msg_wrong_topic, zulip_config))
|
self.assertFalse(check_zulip_message_validity(msg_wrong_topic, zulip_config))
|
||||||
|
|
||||||
msg_not_stream = dict(self.valid_msg, type="private")
|
msg_not_stream = dict(self.valid_msg, type="private")
|
||||||
self.assertFalse(check_zulip_message_validity(msg_not_stream, zulip_config))
|
self.assertFalse(check_zulip_message_validity(msg_not_stream, zulip_config))
|
||||||
|
|
||||||
msg_from_bot = dict(self.valid_msg, sender_email=zulip_config['email'])
|
msg_from_bot = dict(self.valid_msg, sender_email=zulip_config["email"])
|
||||||
self.assertFalse(check_zulip_message_validity(msg_from_bot, zulip_config))
|
self.assertFalse(check_zulip_message_validity(msg_from_bot, zulip_config))
|
||||||
|
|
||||||
def test_zulip_to_matrix(self) -> None:
|
def test_zulip_to_matrix(self) -> None:
|
||||||
|
@ -166,14 +166,14 @@ class MatrixBridgeZulipToMatrixTests(TestCase):
|
||||||
msg = dict(self.valid_msg, sender_full_name="John Smith")
|
msg = dict(self.valid_msg, sender_full_name="John Smith")
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
'hi': '{} hi',
|
"hi": "{} hi",
|
||||||
'*hi*': '{} *hi*',
|
"*hi*": "{} *hi*",
|
||||||
'**hi**': '{} **hi**',
|
"**hi**": "{} **hi**",
|
||||||
}
|
}
|
||||||
|
|
||||||
for content in expected:
|
for content in expected:
|
||||||
send_msg(dict(msg, content=content))
|
send_msg(dict(msg, content=content))
|
||||||
|
|
||||||
for (method, params, _), expect in zip(room.method_calls, expected.values()):
|
for (method, params, _), expect in zip(room.method_calls, expected.values()):
|
||||||
self.assertEqual(method, 'send_text')
|
self.assertEqual(method, "send_text")
|
||||||
self.assertEqual(params[0], expect.format('<JohnSmith>'))
|
self.assertEqual(params[0], expect.format("<JohnSmith>"))
|
||||||
|
|
|
@ -55,17 +55,17 @@ class SlackBridge:
|
||||||
self.slack_webclient = slack_sdk.WebClient(token=self.slack_config["token"])
|
self.slack_webclient = slack_sdk.WebClient(token=self.slack_config["token"])
|
||||||
|
|
||||||
def wrap_slack_mention_with_bracket(self, zulip_msg: Dict[str, Any]) -> None:
|
def wrap_slack_mention_with_bracket(self, zulip_msg: Dict[str, Any]) -> None:
|
||||||
words = zulip_msg["content"].split(' ')
|
words = zulip_msg["content"].split(" ")
|
||||||
for w in words:
|
for w in words:
|
||||||
if w.startswith('@'):
|
if w.startswith("@"):
|
||||||
zulip_msg["content"] = zulip_msg["content"].replace(w, '<' + w + '>')
|
zulip_msg["content"] = zulip_msg["content"].replace(w, "<" + w + ">")
|
||||||
|
|
||||||
def replace_slack_id_with_name(self, msg: Dict[str, Any]) -> None:
|
def replace_slack_id_with_name(self, msg: Dict[str, Any]) -> None:
|
||||||
words = msg['text'].split(' ')
|
words = msg["text"].split(" ")
|
||||||
for w in words:
|
for w in words:
|
||||||
if w.startswith('<@') and w.endswith('>'):
|
if w.startswith("<@") and w.endswith(">"):
|
||||||
_id = w[2:-1]
|
_id = w[2:-1]
|
||||||
msg['text'] = msg['text'].replace(_id, self.slack_id_to_name[_id])
|
msg["text"] = msg["text"].replace(_id, self.slack_id_to_name[_id])
|
||||||
|
|
||||||
def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]:
|
def zulip_to_slack(self) -> Callable[[Dict[str, Any]], None]:
|
||||||
def _zulip_to_slack(msg: Dict[str, Any]) -> None:
|
def _zulip_to_slack(msg: Dict[str, Any]) -> None:
|
||||||
|
@ -83,25 +83,25 @@ class SlackBridge:
|
||||||
return _zulip_to_slack
|
return _zulip_to_slack
|
||||||
|
|
||||||
def run_slack_listener(self) -> None:
|
def run_slack_listener(self) -> None:
|
||||||
members = self.slack_webclient.users_list()['members']
|
members = self.slack_webclient.users_list()["members"]
|
||||||
# See also https://api.slack.com/changelog/2017-09-the-one-about-usernames
|
# See also https://api.slack.com/changelog/2017-09-the-one-about-usernames
|
||||||
self.slack_id_to_name = {
|
self.slack_id_to_name = {
|
||||||
u["id"]: u["profile"].get("display_name", u["profile"]["real_name"]) for u in members
|
u["id"]: u["profile"].get("display_name", u["profile"]["real_name"]) for u in members
|
||||||
}
|
}
|
||||||
self.slack_name_to_id = {v: k for k, v in self.slack_id_to_name.items()}
|
self.slack_name_to_id = {v: k for k, v in self.slack_id_to_name.items()}
|
||||||
|
|
||||||
@RTMClient.run_on(event='message')
|
@RTMClient.run_on(event="message")
|
||||||
def slack_to_zulip(**payload: Any) -> None:
|
def slack_to_zulip(**payload: Any) -> None:
|
||||||
msg = payload['data']
|
msg = payload["data"]
|
||||||
if msg['channel'] != self.channel:
|
if msg["channel"] != self.channel:
|
||||||
return
|
return
|
||||||
user_id = msg['user']
|
user_id = msg["user"]
|
||||||
user = self.slack_id_to_name[user_id]
|
user = self.slack_id_to_name[user_id]
|
||||||
from_bot = user == self.slack_config['username']
|
from_bot = user == self.slack_config["username"]
|
||||||
if from_bot:
|
if from_bot:
|
||||||
return
|
return
|
||||||
self.replace_slack_id_with_name(msg)
|
self.replace_slack_id_with_name(msg)
|
||||||
content = ZULIP_MESSAGE_TEMPLATE.format(username=user, message=msg['text'])
|
content = ZULIP_MESSAGE_TEMPLATE.format(username=user, message=msg["text"])
|
||||||
msg_data = dict(
|
msg_data = dict(
|
||||||
type="stream", to=self.zulip_stream, subject=self.zulip_subject, content=content
|
type="stream", to=self.zulip_stream, subject=self.zulip_subject, content=content
|
||||||
)
|
)
|
||||||
|
@ -117,7 +117,7 @@ if __name__ == "__main__":
|
||||||
the first realm to a channel in a Slack workspace.
|
the first realm to a channel in a Slack workspace.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
parser = argparse.ArgumentParser(usage=usage)
|
parser = argparse.ArgumentParser(usage=usage)
|
||||||
|
|
||||||
print("Starting slack mirroring bot")
|
print("Starting slack mirroring bot")
|
||||||
|
|
|
@ -44,7 +44,7 @@ client = zulip.Client(
|
||||||
user_agent = "Codebase To Zulip Mirroring script (zulip-devel@googlegroups.com)"
|
user_agent = "Codebase To Zulip Mirroring script (zulip-devel@googlegroups.com)"
|
||||||
|
|
||||||
# find some form of JSON loader/dumper, with a preference order for speed.
|
# find some form of JSON loader/dumper, with a preference order for speed.
|
||||||
json_implementations = ['ujson', 'cjson', 'simplejson', 'json']
|
json_implementations = ["ujson", "cjson", "simplejson", "json"]
|
||||||
|
|
||||||
while len(json_implementations):
|
while len(json_implementations):
|
||||||
try:
|
try:
|
||||||
|
@ -58,7 +58,7 @@ def make_api_call(path: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
"https://api3.codebasehq.com/%s" % (path,),
|
"https://api3.codebasehq.com/%s" % (path,),
|
||||||
auth=(config.CODEBASE_API_USERNAME, config.CODEBASE_API_KEY),
|
auth=(config.CODEBASE_API_USERNAME, config.CODEBASE_API_KEY),
|
||||||
params={'raw': 'True'},
|
params={"raw": "True"},
|
||||||
headers={
|
headers={
|
||||||
"User-Agent": user_agent,
|
"User-Agent": user_agent,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -86,36 +86,36 @@ def make_url(path: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def handle_event(event: Dict[str, Any]) -> None:
|
def handle_event(event: Dict[str, Any]) -> None:
|
||||||
event = event['event']
|
event = event["event"]
|
||||||
event_type = event['type']
|
event_type = event["type"]
|
||||||
actor_name = event['actor_name']
|
actor_name = event["actor_name"]
|
||||||
|
|
||||||
raw_props = event.get('raw_properties', {})
|
raw_props = event.get("raw_properties", {})
|
||||||
|
|
||||||
project_link = raw_props.get('project_permalink')
|
project_link = raw_props.get("project_permalink")
|
||||||
|
|
||||||
subject = None
|
subject = None
|
||||||
content = None
|
content = None
|
||||||
if event_type == 'repository_creation':
|
if event_type == "repository_creation":
|
||||||
stream = config.ZULIP_COMMITS_STREAM_NAME
|
stream = config.ZULIP_COMMITS_STREAM_NAME
|
||||||
|
|
||||||
project_name = raw_props.get('name')
|
project_name = raw_props.get("name")
|
||||||
project_repo_type = raw_props.get('scm_type')
|
project_repo_type = raw_props.get("scm_type")
|
||||||
|
|
||||||
url = make_url("projects/%s" % (project_link,))
|
url = make_url("projects/%s" % (project_link,))
|
||||||
scm = "of type %s" % (project_repo_type,) if project_repo_type else ""
|
scm = "of type %s" % (project_repo_type,) if project_repo_type else ""
|
||||||
|
|
||||||
subject = "Repository %s Created" % (project_name,)
|
subject = "Repository %s Created" % (project_name,)
|
||||||
content = "%s created a new repository %s [%s](%s)" % (actor_name, scm, project_name, url)
|
content = "%s created a new repository %s [%s](%s)" % (actor_name, scm, project_name, url)
|
||||||
elif event_type == 'push':
|
elif event_type == "push":
|
||||||
stream = config.ZULIP_COMMITS_STREAM_NAME
|
stream = config.ZULIP_COMMITS_STREAM_NAME
|
||||||
|
|
||||||
num_commits = raw_props.get('commits_count')
|
num_commits = raw_props.get("commits_count")
|
||||||
branch = raw_props.get('ref_name')
|
branch = raw_props.get("ref_name")
|
||||||
project = raw_props.get('project_name')
|
project = raw_props.get("project_name")
|
||||||
repo_link = raw_props.get('repository_permalink')
|
repo_link = raw_props.get("repository_permalink")
|
||||||
deleted_ref = raw_props.get('deleted_ref')
|
deleted_ref = raw_props.get("deleted_ref")
|
||||||
new_ref = raw_props.get('new_ref')
|
new_ref = raw_props.get("new_ref")
|
||||||
|
|
||||||
subject = "Push to %s on %s" % (branch, project)
|
subject = "Push to %s on %s" % (branch, project)
|
||||||
|
|
||||||
|
@ -130,20 +130,20 @@ def handle_event(event: Dict[str, Any]) -> None:
|
||||||
branch,
|
branch,
|
||||||
project,
|
project,
|
||||||
)
|
)
|
||||||
for commit in raw_props.get('commits'):
|
for commit in raw_props.get("commits"):
|
||||||
ref = commit.get('ref')
|
ref = commit.get("ref")
|
||||||
url = make_url(
|
url = make_url(
|
||||||
"projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, ref)
|
"projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, ref)
|
||||||
)
|
)
|
||||||
message = commit.get('message')
|
message = commit.get("message")
|
||||||
content += "* [%s](%s): %s\n" % (ref, url, message)
|
content += "* [%s](%s): %s\n" % (ref, url, message)
|
||||||
elif event_type == 'ticketing_ticket':
|
elif event_type == "ticketing_ticket":
|
||||||
stream = config.ZULIP_TICKETS_STREAM_NAME
|
stream = config.ZULIP_TICKETS_STREAM_NAME
|
||||||
|
|
||||||
num = raw_props.get('number')
|
num = raw_props.get("number")
|
||||||
name = raw_props.get('subject')
|
name = raw_props.get("subject")
|
||||||
assignee = raw_props.get('assignee')
|
assignee = raw_props.get("assignee")
|
||||||
priority = raw_props.get('priority')
|
priority = raw_props.get("priority")
|
||||||
url = make_url("projects/%s/tickets/%s" % (project_link, num))
|
url = make_url("projects/%s/tickets/%s" % (project_link, num))
|
||||||
|
|
||||||
if assignee is None:
|
if assignee is None:
|
||||||
|
@ -153,13 +153,13 @@ def handle_event(event: Dict[str, Any]) -> None:
|
||||||
"""%s created a new ticket [#%s](%s) priority **%s** assigned to %s:\n\n~~~ quote\n %s"""
|
"""%s created a new ticket [#%s](%s) priority **%s** assigned to %s:\n\n~~~ quote\n %s"""
|
||||||
% (actor_name, num, url, priority, assignee, name)
|
% (actor_name, num, url, priority, assignee, name)
|
||||||
)
|
)
|
||||||
elif event_type == 'ticketing_note':
|
elif event_type == "ticketing_note":
|
||||||
stream = config.ZULIP_TICKETS_STREAM_NAME
|
stream = config.ZULIP_TICKETS_STREAM_NAME
|
||||||
|
|
||||||
num = raw_props.get('number')
|
num = raw_props.get("number")
|
||||||
name = raw_props.get('subject')
|
name = raw_props.get("subject")
|
||||||
body = raw_props.get('content')
|
body = raw_props.get("content")
|
||||||
changes = raw_props.get('changes')
|
changes = raw_props.get("changes")
|
||||||
|
|
||||||
url = make_url("projects/%s/tickets/%s" % (project_link, num))
|
url = make_url("projects/%s/tickets/%s" % (project_link, num))
|
||||||
subject = "#%s: %s" % (num, name)
|
subject = "#%s: %s" % (num, name)
|
||||||
|
@ -173,33 +173,33 @@ def handle_event(event: Dict[str, Any]) -> None:
|
||||||
body,
|
body,
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'status_id' in changes:
|
if "status_id" in changes:
|
||||||
status_change = changes.get('status_id')
|
status_change = changes.get("status_id")
|
||||||
content += "Status changed from **%s** to **%s**\n\n" % (
|
content += "Status changed from **%s** to **%s**\n\n" % (
|
||||||
status_change[0],
|
status_change[0],
|
||||||
status_change[1],
|
status_change[1],
|
||||||
)
|
)
|
||||||
elif event_type == 'ticketing_milestone':
|
elif event_type == "ticketing_milestone":
|
||||||
stream = config.ZULIP_TICKETS_STREAM_NAME
|
stream = config.ZULIP_TICKETS_STREAM_NAME
|
||||||
|
|
||||||
name = raw_props.get('name')
|
name = raw_props.get("name")
|
||||||
identifier = raw_props.get('identifier')
|
identifier = raw_props.get("identifier")
|
||||||
url = make_url("projects/%s/milestone/%s" % (project_link, identifier))
|
url = make_url("projects/%s/milestone/%s" % (project_link, identifier))
|
||||||
|
|
||||||
subject = name
|
subject = name
|
||||||
content = "%s created a new milestone [%s](%s)" % (actor_name, name, url)
|
content = "%s created a new milestone [%s](%s)" % (actor_name, name, url)
|
||||||
elif event_type == 'comment':
|
elif event_type == "comment":
|
||||||
stream = config.ZULIP_COMMITS_STREAM_NAME
|
stream = config.ZULIP_COMMITS_STREAM_NAME
|
||||||
|
|
||||||
comment = raw_props.get('content')
|
comment = raw_props.get("content")
|
||||||
commit = raw_props.get('commit_ref')
|
commit = raw_props.get("commit_ref")
|
||||||
|
|
||||||
# If there's a commit id, it's a comment to a commit
|
# If there's a commit id, it's a comment to a commit
|
||||||
if commit:
|
if commit:
|
||||||
repo_link = raw_props.get('repository_permalink')
|
repo_link = raw_props.get("repository_permalink")
|
||||||
|
|
||||||
url = make_url(
|
url = make_url(
|
||||||
'projects/%s/repositories/%s/commit/%s' % (project_link, repo_link, commit)
|
"projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, commit)
|
||||||
)
|
)
|
||||||
|
|
||||||
subject = "%s commented on %s" % (actor_name, commit)
|
subject = "%s commented on %s" % (actor_name, commit)
|
||||||
|
@ -223,14 +223,14 @@ def handle_event(event: Dict[str, Any]) -> None:
|
||||||
else:
|
else:
|
||||||
content = "%s posted:\n\n~~~ quote\n%s\n~~~" % (actor_name, comment_content)
|
content = "%s posted:\n\n~~~ quote\n%s\n~~~" % (actor_name, comment_content)
|
||||||
|
|
||||||
elif event_type == 'deployment':
|
elif event_type == "deployment":
|
||||||
stream = config.ZULIP_COMMITS_STREAM_NAME
|
stream = config.ZULIP_COMMITS_STREAM_NAME
|
||||||
|
|
||||||
start_ref = raw_props.get('start_ref')
|
start_ref = raw_props.get("start_ref")
|
||||||
end_ref = raw_props.get('end_ref')
|
end_ref = raw_props.get("end_ref")
|
||||||
environment = raw_props.get('environment')
|
environment = raw_props.get("environment")
|
||||||
servers = raw_props.get('servers')
|
servers = raw_props.get("servers")
|
||||||
repo_link = raw_props.get('repository_permalink')
|
repo_link = raw_props.get("repository_permalink")
|
||||||
|
|
||||||
start_ref_url = make_url(
|
start_ref_url = make_url(
|
||||||
"projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, start_ref)
|
"projects/%s/repositories/%s/commit/%s" % (project_link, repo_link, start_ref)
|
||||||
|
@ -259,30 +259,30 @@ def handle_event(event: Dict[str, Any]) -> None:
|
||||||
", ".join(["`%s`" % (server,) for server in servers])
|
", ".join(["`%s`" % (server,) for server in servers])
|
||||||
)
|
)
|
||||||
|
|
||||||
elif event_type == 'named_tree':
|
elif event_type == "named_tree":
|
||||||
# Docs say named_tree type used for new/deleting branches and tags,
|
# Docs say named_tree type used for new/deleting branches and tags,
|
||||||
# but experimental testing showed that they were all sent as 'push' events
|
# but experimental testing showed that they were all sent as 'push' events
|
||||||
pass
|
pass
|
||||||
elif event_type == 'wiki_page':
|
elif event_type == "wiki_page":
|
||||||
logging.warn("Wiki page notifications not yet implemented")
|
logging.warn("Wiki page notifications not yet implemented")
|
||||||
elif event_type == 'sprint_creation':
|
elif event_type == "sprint_creation":
|
||||||
logging.warn("Sprint notifications not yet implemented")
|
logging.warn("Sprint notifications not yet implemented")
|
||||||
elif event_type == 'sprint_ended':
|
elif event_type == "sprint_ended":
|
||||||
logging.warn("Sprint notifications not yet implemented")
|
logging.warn("Sprint notifications not yet implemented")
|
||||||
else:
|
else:
|
||||||
logging.info("Unknown event type %s, ignoring!" % (event_type,))
|
logging.info("Unknown event type %s, ignoring!" % (event_type,))
|
||||||
|
|
||||||
if subject and content:
|
if subject and content:
|
||||||
if len(subject) > 60:
|
if len(subject) > 60:
|
||||||
subject = subject[:57].rstrip() + '...'
|
subject = subject[:57].rstrip() + "..."
|
||||||
|
|
||||||
res = client.send_message(
|
res = client.send_message(
|
||||||
{"type": "stream", "to": stream, "subject": subject, "content": content}
|
{"type": "stream", "to": stream, "subject": subject, "content": content}
|
||||||
)
|
)
|
||||||
if res['result'] == 'success':
|
if res["result"] == "success":
|
||||||
logging.info("Successfully sent Zulip with id: %s" % (res['id'],))
|
logging.info("Successfully sent Zulip with id: %s" % (res["id"],))
|
||||||
else:
|
else:
|
||||||
logging.warn("Failed to send Zulip: %s %s" % (res['result'], res['msg']))
|
logging.warn("Failed to send Zulip: %s %s" % (res["result"], res["msg"]))
|
||||||
|
|
||||||
|
|
||||||
# the main run loop for this mirror script
|
# the main run loop for this mirror script
|
||||||
|
@ -295,7 +295,7 @@ def run_mirror() -> None:
|
||||||
try:
|
try:
|
||||||
with open(config.RESUME_FILE) as f:
|
with open(config.RESUME_FILE) as f:
|
||||||
timestamp = f.read()
|
timestamp = f.read()
|
||||||
if timestamp == '':
|
if timestamp == "":
|
||||||
since = default_since()
|
since = default_since()
|
||||||
else:
|
else:
|
||||||
since = datetime.fromtimestamp(float(timestamp), tz=pytz.utc)
|
since = datetime.fromtimestamp(float(timestamp), tz=pytz.utc)
|
||||||
|
@ -310,7 +310,7 @@ def run_mirror() -> None:
|
||||||
if events is not None:
|
if events is not None:
|
||||||
sleepInterval = 1
|
sleepInterval = 1
|
||||||
for event in events[::-1]:
|
for event in events[::-1]:
|
||||||
timestamp = event.get('event', {}).get('timestamp', '')
|
timestamp = event.get("event", {}).get("timestamp", "")
|
||||||
event_date = dateutil.parser.parse(timestamp)
|
event_date = dateutil.parser.parse(timestamp)
|
||||||
if event_date > since:
|
if event_date > since:
|
||||||
handle_event(event)
|
handle_event(event)
|
||||||
|
@ -322,7 +322,7 @@ def run_mirror() -> None:
|
||||||
time.sleep(sleepInterval)
|
time.sleep(sleepInterval)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
open(config.RESUME_FILE, 'w').write(since.strftime("%s"))
|
open(config.RESUME_FILE, "w").write(since.strftime("%s"))
|
||||||
logging.info("Shutting down Codebase mirror")
|
logging.info("Shutting down Codebase mirror")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -43,19 +43,19 @@ def git_repository_name() -> Text:
|
||||||
|
|
||||||
def git_commit_range(oldrev: str, newrev: str) -> str:
|
def git_commit_range(oldrev: str, newrev: str) -> str:
|
||||||
log_cmd = ["git", "log", "--reverse", "--pretty=%aE %H %s", "%s..%s" % (oldrev, newrev)]
|
log_cmd = ["git", "log", "--reverse", "--pretty=%aE %H %s", "%s..%s" % (oldrev, newrev)]
|
||||||
commits = ''
|
commits = ""
|
||||||
for ln in subprocess.check_output(log_cmd, universal_newlines=True).splitlines():
|
for ln in subprocess.check_output(log_cmd, universal_newlines=True).splitlines():
|
||||||
author_email, commit_id, subject = ln.split(None, 2)
|
author_email, commit_id, subject = ln.split(None, 2)
|
||||||
if hasattr(config, "format_commit_message"):
|
if hasattr(config, "format_commit_message"):
|
||||||
commits += config.format_commit_message(author_email, subject, commit_id)
|
commits += config.format_commit_message(author_email, subject, commit_id)
|
||||||
else:
|
else:
|
||||||
commits += '!avatar(%s) %s\n' % (author_email, subject)
|
commits += "!avatar(%s) %s\n" % (author_email, subject)
|
||||||
return commits
|
return commits
|
||||||
|
|
||||||
|
|
||||||
def send_bot_message(oldrev: str, newrev: str, refname: str) -> None:
|
def send_bot_message(oldrev: str, newrev: str, refname: str) -> None:
|
||||||
repo_name = git_repository_name()
|
repo_name = git_repository_name()
|
||||||
branch = refname.replace('refs/heads/', '')
|
branch = refname.replace("refs/heads/", "")
|
||||||
destination = config.commit_notice_destination(repo_name, branch, newrev)
|
destination = config.commit_notice_destination(repo_name, branch, newrev)
|
||||||
if destination is None:
|
if destination is None:
|
||||||
# Don't forward the notice anywhere
|
# Don't forward the notice anywhere
|
||||||
|
@ -65,30 +65,30 @@ def send_bot_message(oldrev: str, newrev: str, refname: str) -> None:
|
||||||
old_head = oldrev[:12]
|
old_head = oldrev[:12]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
oldrev == '0000000000000000000000000000000000000000'
|
oldrev == "0000000000000000000000000000000000000000"
|
||||||
or newrev == '0000000000000000000000000000000000000000'
|
or newrev == "0000000000000000000000000000000000000000"
|
||||||
):
|
):
|
||||||
# New branch pushed or old branch removed
|
# New branch pushed or old branch removed
|
||||||
added = ''
|
added = ""
|
||||||
removed = ''
|
removed = ""
|
||||||
else:
|
else:
|
||||||
added = git_commit_range(oldrev, newrev)
|
added = git_commit_range(oldrev, newrev)
|
||||||
removed = git_commit_range(newrev, oldrev)
|
removed = git_commit_range(newrev, oldrev)
|
||||||
|
|
||||||
if oldrev == '0000000000000000000000000000000000000000':
|
if oldrev == "0000000000000000000000000000000000000000":
|
||||||
message = '`%s` was pushed to new branch `%s`' % (new_head, branch)
|
message = "`%s` was pushed to new branch `%s`" % (new_head, branch)
|
||||||
elif newrev == '0000000000000000000000000000000000000000':
|
elif newrev == "0000000000000000000000000000000000000000":
|
||||||
message = 'branch `%s` was removed (was `%s`)' % (branch, old_head)
|
message = "branch `%s` was removed (was `%s`)" % (branch, old_head)
|
||||||
elif removed:
|
elif removed:
|
||||||
message = '`%s` was pushed to `%s`, **REMOVING**:\n\n%s' % (new_head, branch, removed)
|
message = "`%s` was pushed to `%s`, **REMOVING**:\n\n%s" % (new_head, branch, removed)
|
||||||
if added:
|
if added:
|
||||||
message += '\n**and adding**:\n\n' + added
|
message += "\n**and adding**:\n\n" + added
|
||||||
message += '\n**A HISTORY REWRITE HAS OCCURRED!**'
|
message += "\n**A HISTORY REWRITE HAS OCCURRED!**"
|
||||||
message += '\n@everyone: Please check your local branches to deal with this.'
|
message += "\n@everyone: Please check your local branches to deal with this."
|
||||||
elif added:
|
elif added:
|
||||||
message = '`%s` was deployed to `%s` with:\n\n%s' % (new_head, branch, added)
|
message = "`%s` was deployed to `%s` with:\n\n%s" % (new_head, branch, added)
|
||||||
else:
|
else:
|
||||||
message = '`%s` was pushed to `%s`... but nothing changed?' % (new_head, branch)
|
message = "`%s` was pushed to `%s`... but nothing changed?" % (new_head, branch)
|
||||||
|
|
||||||
message_data = {
|
message_data = {
|
||||||
"type": "stream",
|
"type": "stream",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from typing import Dict, Optional, Text
|
from typing import Dict, Optional, Text
|
||||||
|
|
||||||
# Name of the stream to send notifications to, default is "commits"
|
# Name of the stream to send notifications to, default is "commits"
|
||||||
STREAM_NAME = 'commits'
|
STREAM_NAME = "commits"
|
||||||
|
|
||||||
# Change these values to configure authentication for the plugin
|
# Change these values to configure authentication for the plugin
|
||||||
ZULIP_USER = "git-bot@example.com"
|
ZULIP_USER = "git-bot@example.com"
|
||||||
|
@ -37,7 +37,7 @@ def commit_notice_destination(repo: Text, branch: Text, commit: Text) -> Optiona
|
||||||
#
|
#
|
||||||
# return '!avatar(%s) [%s](https://example.com/commits/%s)\n' % (author, subject, commit_id)
|
# return '!avatar(%s) [%s](https://example.com/commits/%s)\n' % (author, subject, commit_id)
|
||||||
def format_commit_message(author: Text, subject: Text, commit_id: Text) -> Text:
|
def format_commit_message(author: Text, subject: Text, commit_id: Text) -> Text:
|
||||||
return '!avatar(%s) %s\n' % (author, subject)
|
return "!avatar(%s) %s\n" % (author, subject)
|
||||||
|
|
||||||
|
|
||||||
## If properly installed, the Zulip API should be in your import
|
## If properly installed, the Zulip API should be in your import
|
||||||
|
|
|
@ -18,12 +18,12 @@ except ImportError:
|
||||||
# at zulip/bots/gcal/
|
# at zulip/bots/gcal/
|
||||||
# NOTE: When adding more scopes, add them after the previous one in the same field, with a space
|
# NOTE: When adding more scopes, add them after the previous one in the same field, with a space
|
||||||
# seperating them.
|
# seperating them.
|
||||||
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
|
SCOPES = "https://www.googleapis.com/auth/calendar.readonly"
|
||||||
# This file contains the information that google uses to figure out which application is requesting
|
# This file contains the information that google uses to figure out which application is requesting
|
||||||
# this client's data.
|
# this client's data.
|
||||||
CLIENT_SECRET_FILE = 'client_secret.json'
|
CLIENT_SECRET_FILE = "client_secret.json"
|
||||||
APPLICATION_NAME = 'Zulip Calendar Bot'
|
APPLICATION_NAME = "Zulip Calendar Bot"
|
||||||
HOME_DIR = os.path.expanduser('~')
|
HOME_DIR = os.path.expanduser("~")
|
||||||
|
|
||||||
|
|
||||||
def get_credentials() -> client.Credentials:
|
def get_credentials() -> client.Credentials:
|
||||||
|
@ -36,7 +36,7 @@ def get_credentials() -> client.Credentials:
|
||||||
Credentials, the obtained credential.
|
Credentials, the obtained credential.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
credential_path = os.path.join(HOME_DIR, 'google-credentials.json')
|
credential_path = os.path.join(HOME_DIR, "google-credentials.json")
|
||||||
|
|
||||||
store = Storage(credential_path)
|
store = Storage(credential_path)
|
||||||
credentials = store.get()
|
credentials = store.get()
|
||||||
|
@ -50,7 +50,7 @@ def get_credentials() -> client.Credentials:
|
||||||
credentials = tools.run_flow(flow, store, flags)
|
credentials = tools.run_flow(flow, store, flags)
|
||||||
else: # Needed only for compatibility with Python 2.6
|
else: # Needed only for compatibility with Python 2.6
|
||||||
credentials = tools.run(flow, store)
|
credentials = tools.run(flow, store)
|
||||||
print('Storing credentials to ' + credential_path)
|
print("Storing credentials to " + credential_path)
|
||||||
|
|
||||||
|
|
||||||
get_credentials()
|
get_credentials()
|
||||||
|
|
|
@ -20,15 +20,15 @@ from oauth2client.file import Storage
|
||||||
try:
|
try:
|
||||||
from googleapiclient import discovery
|
from googleapiclient import discovery
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logging.exception('Install google-api-python-client')
|
logging.exception("Install google-api-python-client")
|
||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../'))
|
sys.path.append(os.path.join(os.path.dirname(__file__), "../../"))
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
|
SCOPES = "https://www.googleapis.com/auth/calendar.readonly"
|
||||||
CLIENT_SECRET_FILE = 'client_secret.json'
|
CLIENT_SECRET_FILE = "client_secret.json"
|
||||||
APPLICATION_NAME = 'Zulip'
|
APPLICATION_NAME = "Zulip"
|
||||||
HOME_DIR = os.path.expanduser('~')
|
HOME_DIR = os.path.expanduser("~")
|
||||||
|
|
||||||
# Our cached view of the calendar, updated periodically.
|
# Our cached view of the calendar, updated periodically.
|
||||||
events = [] # type: List[Tuple[int, datetime.datetime, str]]
|
events = [] # type: List[Tuple[int, datetime.datetime, str]]
|
||||||
|
@ -61,28 +61,28 @@ google-calendar --calendar calendarID@example.calendar.google.com
|
||||||
|
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--interval',
|
"--interval",
|
||||||
dest='interval',
|
dest="interval",
|
||||||
default=30,
|
default=30,
|
||||||
type=int,
|
type=int,
|
||||||
action='store',
|
action="store",
|
||||||
help='Minutes before event for reminder [default: 30]',
|
help="Minutes before event for reminder [default: 30]",
|
||||||
metavar='MINUTES',
|
metavar="MINUTES",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--calendar',
|
"--calendar",
|
||||||
dest='calendarID',
|
dest="calendarID",
|
||||||
default='primary',
|
default="primary",
|
||||||
type=str,
|
type=str,
|
||||||
action='store',
|
action="store",
|
||||||
help='Calendar ID for the calendar you want to receive reminders from.',
|
help="Calendar ID for the calendar you want to receive reminders from.",
|
||||||
)
|
)
|
||||||
|
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
if not (options.zulip_email):
|
if not (options.zulip_email):
|
||||||
parser.error('You must specify --user')
|
parser.error("You must specify --user")
|
||||||
|
|
||||||
zulip_client = zulip.init_from_options(options)
|
zulip_client = zulip.init_from_options(options)
|
||||||
|
|
||||||
|
@ -98,14 +98,14 @@ def get_credentials() -> client.Credentials:
|
||||||
Credentials, the obtained credential.
|
Credentials, the obtained credential.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
credential_path = os.path.join(HOME_DIR, 'google-credentials.json')
|
credential_path = os.path.join(HOME_DIR, "google-credentials.json")
|
||||||
|
|
||||||
store = Storage(credential_path)
|
store = Storage(credential_path)
|
||||||
credentials = store.get()
|
credentials = store.get()
|
||||||
|
|
||||||
return credentials
|
return credentials
|
||||||
except client.Error:
|
except client.Error:
|
||||||
logging.exception('Error while trying to open the `google-credentials.json` file.')
|
logging.exception("Error while trying to open the `google-credentials.json` file.")
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.error("Run the get-google-credentials script from this directory first.")
|
logging.error("Run the get-google-credentials script from this directory first.")
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ def populate_events() -> Optional[None]:
|
||||||
|
|
||||||
credentials = get_credentials()
|
credentials = get_credentials()
|
||||||
creds = credentials.authorize(httplib2.Http())
|
creds = credentials.authorize(httplib2.Http())
|
||||||
service = discovery.build('calendar', 'v3', http=creds)
|
service = discovery.build("calendar", "v3", http=creds)
|
||||||
|
|
||||||
now = datetime.datetime.now(pytz.utc).isoformat()
|
now = datetime.datetime.now(pytz.utc).isoformat()
|
||||||
feed = (
|
feed = (
|
||||||
|
@ -125,7 +125,7 @@ def populate_events() -> Optional[None]:
|
||||||
timeMin=now,
|
timeMin=now,
|
||||||
maxResults=5,
|
maxResults=5,
|
||||||
singleEvents=True,
|
singleEvents=True,
|
||||||
orderBy='startTime',
|
orderBy="startTime",
|
||||||
)
|
)
|
||||||
.execute()
|
.execute()
|
||||||
)
|
)
|
||||||
|
@ -174,10 +174,10 @@ def send_reminders() -> Optional[None]:
|
||||||
key = (id, start)
|
key = (id, start)
|
||||||
if key not in sent:
|
if key not in sent:
|
||||||
if start.hour == 0 and start.minute == 0:
|
if start.hour == 0 and start.minute == 0:
|
||||||
line = '%s is today.' % (summary,)
|
line = "%s is today." % (summary,)
|
||||||
else:
|
else:
|
||||||
line = '%s starts at %s' % (summary, start.strftime('%H:%M'))
|
line = "%s starts at %s" % (summary, start.strftime("%H:%M"))
|
||||||
print('Sending reminder:', line)
|
print("Sending reminder:", line)
|
||||||
messages.append(line)
|
messages.append(line)
|
||||||
keys.add(key)
|
keys.add(key)
|
||||||
|
|
||||||
|
@ -185,12 +185,12 @@ def send_reminders() -> Optional[None]:
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(messages) == 1:
|
if len(messages) == 1:
|
||||||
message = 'Reminder: ' + messages[0]
|
message = "Reminder: " + messages[0]
|
||||||
else:
|
else:
|
||||||
message = 'Reminder:\n\n' + '\n'.join('* ' + m for m in messages)
|
message = "Reminder:\n\n" + "\n".join("* " + m for m in messages)
|
||||||
|
|
||||||
zulip_client.send_message(
|
zulip_client.send_message(
|
||||||
dict(type='private', to=options.zulip_email, sender=options.zulip_email, content=message)
|
dict(type="private", to=options.zulip_email, sender=options.zulip_email, content=message)
|
||||||
)
|
)
|
||||||
|
|
||||||
sent.update(keys)
|
sent.update(keys)
|
||||||
|
|
|
@ -94,7 +94,7 @@ def send_zulip(
|
||||||
def get_config(ui: ui, item: str) -> str:
|
def get_config(ui: ui, item: str) -> str:
|
||||||
try:
|
try:
|
||||||
# config returns configuration value.
|
# config returns configuration value.
|
||||||
return ui.config('zulip', item)
|
return ui.config("zulip", item)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
ui.warn("Zulip: Could not find required item {} in hg config.".format(item))
|
ui.warn("Zulip: Could not find required item {} in hg config.".format(item))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
@ -62,7 +62,7 @@ def stream_to_room(stream: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def jid_to_zulip(jid: JID) -> str:
|
def jid_to_zulip(jid: JID) -> str:
|
||||||
suffix = ''
|
suffix = ""
|
||||||
if not jid.username.endswith("-bot"):
|
if not jid.username.endswith("-bot"):
|
||||||
suffix = options.zulip_email_suffix
|
suffix = options.zulip_email_suffix
|
||||||
return "%s%s@%s" % (jid.username, suffix, options.zulip_domain)
|
return "%s%s@%s" % (jid.username, suffix, options.zulip_domain)
|
||||||
|
@ -94,10 +94,10 @@ class JabberToZulipBot(ClientXMPP):
|
||||||
self.zulip = None
|
self.zulip = None
|
||||||
self.use_ipv6 = False
|
self.use_ipv6 = False
|
||||||
|
|
||||||
self.register_plugin('xep_0045') # Jabber chatrooms
|
self.register_plugin("xep_0045") # Jabber chatrooms
|
||||||
self.register_plugin('xep_0199') # XMPP Ping
|
self.register_plugin("xep_0199") # XMPP Ping
|
||||||
|
|
||||||
def set_zulip_client(self, zulipToJabberClient: 'ZulipToJabberBot') -> None:
|
def set_zulip_client(self, zulipToJabberClient: "ZulipToJabberBot") -> None:
|
||||||
self.zulipToJabber = zulipToJabberClient
|
self.zulipToJabber = zulipToJabberClient
|
||||||
|
|
||||||
def session_start(self, event: Dict[str, Any]) -> None:
|
def session_start(self, event: Dict[str, Any]) -> None:
|
||||||
|
@ -112,7 +112,7 @@ class JabberToZulipBot(ClientXMPP):
|
||||||
logging.debug("Joining " + room)
|
logging.debug("Joining " + room)
|
||||||
self.rooms.add(room)
|
self.rooms.add(room)
|
||||||
muc_jid = JID(local=room, domain=options.conference_domain)
|
muc_jid = JID(local=room, domain=options.conference_domain)
|
||||||
xep0045 = self.plugin['xep_0045']
|
xep0045 = self.plugin["xep_0045"]
|
||||||
try:
|
try:
|
||||||
xep0045.joinMUC(muc_jid, self.nick, wait=True)
|
xep0045.joinMUC(muc_jid, self.nick, wait=True)
|
||||||
except InvalidJID:
|
except InvalidJID:
|
||||||
|
@ -137,7 +137,7 @@ class JabberToZulipBot(ClientXMPP):
|
||||||
logging.debug("Leaving " + room)
|
logging.debug("Leaving " + room)
|
||||||
self.rooms.remove(room)
|
self.rooms.remove(room)
|
||||||
muc_jid = JID(local=room, domain=options.conference_domain)
|
muc_jid = JID(local=room, domain=options.conference_domain)
|
||||||
self.plugin['xep_0045'].leaveMUC(muc_jid, self.nick)
|
self.plugin["xep_0045"].leaveMUC(muc_jid, self.nick)
|
||||||
|
|
||||||
def message(self, msg: JabberMessage) -> Any:
|
def message(self, msg: JabberMessage) -> Any:
|
||||||
try:
|
try:
|
||||||
|
@ -152,7 +152,7 @@ class JabberToZulipBot(ClientXMPP):
|
||||||
logging.exception("Error forwarding Jabber => Zulip")
|
logging.exception("Error forwarding Jabber => Zulip")
|
||||||
|
|
||||||
def private(self, msg: JabberMessage) -> None:
|
def private(self, msg: JabberMessage) -> None:
|
||||||
if options.mode == 'public' or msg['thread'] == '\u1FFFE':
|
if options.mode == "public" or msg["thread"] == "\u1FFFE":
|
||||||
return
|
return
|
||||||
sender = jid_to_zulip(msg["from"])
|
sender = jid_to_zulip(msg["from"])
|
||||||
recipient = jid_to_zulip(msg["to"])
|
recipient = jid_to_zulip(msg["to"])
|
||||||
|
@ -168,13 +168,13 @@ class JabberToZulipBot(ClientXMPP):
|
||||||
logging.error(str(ret))
|
logging.error(str(ret))
|
||||||
|
|
||||||
def group(self, msg: JabberMessage) -> None:
|
def group(self, msg: JabberMessage) -> None:
|
||||||
if options.mode == 'personal' or msg["thread"] == '\u1FFFE':
|
if options.mode == "personal" or msg["thread"] == "\u1FFFE":
|
||||||
return
|
return
|
||||||
|
|
||||||
subject = msg["subject"]
|
subject = msg["subject"]
|
||||||
if len(subject) == 0:
|
if len(subject) == 0:
|
||||||
subject = "(no topic)"
|
subject = "(no topic)"
|
||||||
stream = room_to_stream(msg['from'].local)
|
stream = room_to_stream(msg["from"].local)
|
||||||
sender_nick = msg.get_mucnick()
|
sender_nick = msg.get_mucnick()
|
||||||
if not sender_nick:
|
if not sender_nick:
|
||||||
# Messages from the room itself have no nickname. We should not try
|
# Messages from the room itself have no nickname. We should not try
|
||||||
|
@ -195,9 +195,9 @@ class JabberToZulipBot(ClientXMPP):
|
||||||
logging.error(str(ret))
|
logging.error(str(ret))
|
||||||
|
|
||||||
def nickname_to_jid(self, room: str, nick: str) -> JID:
|
def nickname_to_jid(self, room: str, nick: str) -> JID:
|
||||||
jid = self.plugin['xep_0045'].getJidProperty(room, nick, "jid")
|
jid = self.plugin["xep_0045"].getJidProperty(room, nick, "jid")
|
||||||
if jid is None or jid == '':
|
if jid is None or jid == "":
|
||||||
return JID(local=nick.replace(' ', ''), domain=self.boundjid.domain)
|
return JID(local=nick.replace(" ", ""), domain=self.boundjid.domain)
|
||||||
else:
|
else:
|
||||||
return jid
|
return jid
|
||||||
|
|
||||||
|
@ -211,59 +211,59 @@ class ZulipToJabberBot:
|
||||||
self.jabber = client
|
self.jabber = client
|
||||||
|
|
||||||
def process_event(self, event: Dict[str, Any]) -> None:
|
def process_event(self, event: Dict[str, Any]) -> None:
|
||||||
if event['type'] == 'message':
|
if event["type"] == "message":
|
||||||
message = event["message"]
|
message = event["message"]
|
||||||
if message['sender_email'] != self.client.email:
|
if message["sender_email"] != self.client.email:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if message['type'] == 'stream':
|
if message["type"] == "stream":
|
||||||
self.stream_message(message)
|
self.stream_message(message)
|
||||||
elif message['type'] == 'private':
|
elif message["type"] == "private":
|
||||||
self.private_message(message)
|
self.private_message(message)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Exception forwarding Zulip => Jabber")
|
logging.exception("Exception forwarding Zulip => Jabber")
|
||||||
elif event['type'] == 'subscription':
|
elif event["type"] == "subscription":
|
||||||
self.process_subscription(event)
|
self.process_subscription(event)
|
||||||
|
|
||||||
def stream_message(self, msg: Dict[str, str]) -> None:
|
def stream_message(self, msg: Dict[str, str]) -> None:
|
||||||
assert self.jabber is not None
|
assert self.jabber is not None
|
||||||
stream = msg['display_recipient']
|
stream = msg["display_recipient"]
|
||||||
if not stream.endswith("/xmpp"):
|
if not stream.endswith("/xmpp"):
|
||||||
return
|
return
|
||||||
|
|
||||||
room = stream_to_room(stream)
|
room = stream_to_room(stream)
|
||||||
jabber_recipient = JID(local=room, domain=options.conference_domain)
|
jabber_recipient = JID(local=room, domain=options.conference_domain)
|
||||||
outgoing = self.jabber.make_message(
|
outgoing = self.jabber.make_message(
|
||||||
mto=jabber_recipient, mbody=msg['content'], mtype='groupchat'
|
mto=jabber_recipient, mbody=msg["content"], mtype="groupchat"
|
||||||
)
|
)
|
||||||
outgoing['thread'] = '\u1FFFE'
|
outgoing["thread"] = "\u1FFFE"
|
||||||
outgoing.send()
|
outgoing.send()
|
||||||
|
|
||||||
def private_message(self, msg: Dict[str, Any]) -> None:
|
def private_message(self, msg: Dict[str, Any]) -> None:
|
||||||
assert self.jabber is not None
|
assert self.jabber is not None
|
||||||
for recipient in msg['display_recipient']:
|
for recipient in msg["display_recipient"]:
|
||||||
if recipient["email"] == self.client.email:
|
if recipient["email"] == self.client.email:
|
||||||
continue
|
continue
|
||||||
if not recipient["is_mirror_dummy"]:
|
if not recipient["is_mirror_dummy"]:
|
||||||
continue
|
continue
|
||||||
recip_email = recipient['email']
|
recip_email = recipient["email"]
|
||||||
jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain)
|
jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain)
|
||||||
outgoing = self.jabber.make_message(
|
outgoing = self.jabber.make_message(
|
||||||
mto=jabber_recipient, mbody=msg['content'], mtype='chat'
|
mto=jabber_recipient, mbody=msg["content"], mtype="chat"
|
||||||
)
|
)
|
||||||
outgoing['thread'] = '\u1FFFE'
|
outgoing["thread"] = "\u1FFFE"
|
||||||
outgoing.send()
|
outgoing.send()
|
||||||
|
|
||||||
def process_subscription(self, event: Dict[str, Any]) -> None:
|
def process_subscription(self, event: Dict[str, Any]) -> None:
|
||||||
assert self.jabber is not None
|
assert self.jabber is not None
|
||||||
if event['op'] == 'add':
|
if event["op"] == "add":
|
||||||
streams = [s['name'].lower() for s in event['subscriptions']]
|
streams = [s["name"].lower() for s in event["subscriptions"]]
|
||||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||||
for stream in streams:
|
for stream in streams:
|
||||||
self.jabber.join_muc(stream_to_room(stream))
|
self.jabber.join_muc(stream_to_room(stream))
|
||||||
if event['op'] == 'remove':
|
if event["op"] == "remove":
|
||||||
streams = [s['name'].lower() for s in event['subscriptions']]
|
streams = [s["name"].lower() for s in event["subscriptions"]]
|
||||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||||
for stream in streams:
|
for stream in streams:
|
||||||
self.jabber.leave_muc(stream_to_room(stream))
|
self.jabber.leave_muc(stream_to_room(stream))
|
||||||
|
@ -277,14 +277,14 @@ def get_rooms(zulipToJabber: ZulipToJabberBot) -> List[str]:
|
||||||
sys.exit("Could not get initial list of Zulip %s" % (key,))
|
sys.exit("Could not get initial list of Zulip %s" % (key,))
|
||||||
return ret[key]
|
return ret[key]
|
||||||
|
|
||||||
if options.mode == 'public':
|
if options.mode == "public":
|
||||||
stream_infos = get_stream_infos("streams", zulipToJabber.client.get_streams)
|
stream_infos = get_stream_infos("streams", zulipToJabber.client.get_streams)
|
||||||
else:
|
else:
|
||||||
stream_infos = get_stream_infos("subscriptions", zulipToJabber.client.get_subscriptions)
|
stream_infos = get_stream_infos("subscriptions", zulipToJabber.client.get_subscriptions)
|
||||||
|
|
||||||
rooms = [] # type: List[str]
|
rooms = [] # type: List[str]
|
||||||
for stream_info in stream_infos:
|
for stream_info in stream_infos:
|
||||||
stream = stream_info['name']
|
stream = stream_info["name"]
|
||||||
if stream.endswith("/xmpp"):
|
if stream.endswith("/xmpp"):
|
||||||
rooms.append(stream_to_room(stream))
|
rooms.append(stream_to_room(stream))
|
||||||
return rooms
|
return rooms
|
||||||
|
@ -295,20 +295,20 @@ def config_error(msg: str) -> None:
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
parser = optparse.OptionParser(
|
parser = optparse.OptionParser(
|
||||||
epilog='''Most general and Jabber configuration options may also be specified in the
|
epilog="""Most general and Jabber configuration options may also be specified in the
|
||||||
zulip configuration file under the jabber_mirror section (exceptions are noted
|
zulip configuration file under the jabber_mirror section (exceptions are noted
|
||||||
in their help sections). Keys have the same name as options with hyphens
|
in their help sections). Keys have the same name as options with hyphens
|
||||||
replaced with underscores. Zulip configuration options go in the api section,
|
replaced with underscores. Zulip configuration options go in the api section,
|
||||||
as normal.'''.replace(
|
as normal.""".replace(
|
||||||
"\n", " "
|
"\n", " "
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--mode',
|
"--mode",
|
||||||
default=None,
|
default=None,
|
||||||
action='store',
|
action="store",
|
||||||
help='''Which mode to run in. Valid options are "personal" and "public". In
|
help='''Which mode to run in. Valid options are "personal" and "public". In
|
||||||
"personal" mode, the mirror uses an individual users' credentials and mirrors
|
"personal" mode, the mirror uses an individual users' credentials and mirrors
|
||||||
all messages they send on Zulip to Jabber and all private Jabber messages to
|
all messages they send on Zulip to Jabber and all private Jabber messages to
|
||||||
|
@ -319,33 +319,33 @@ user and mirrors messages sent to Jabber rooms to Zulip. Defaults to
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--zulip-email-suffix',
|
"--zulip-email-suffix",
|
||||||
default=None,
|
default=None,
|
||||||
action='store',
|
action="store",
|
||||||
help='''Add the specified suffix to the local part of email addresses constructed
|
help="""Add the specified suffix to the local part of email addresses constructed
|
||||||
from JIDs and nicks before sending requests to the Zulip server, and remove the
|
from JIDs and nicks before sending requests to the Zulip server, and remove the
|
||||||
suffix before sending requests to the Jabber server. For example, specifying
|
suffix before sending requests to the Jabber server. For example, specifying
|
||||||
"+foo" will cause messages that are sent to the "bar" room by nickname "qux" to
|
"+foo" will cause messages that are sent to the "bar" room by nickname "qux" to
|
||||||
be mirrored to the "bar/xmpp" stream in Zulip by user "qux+foo@example.com". This
|
be mirrored to the "bar/xmpp" stream in Zulip by user "qux+foo@example.com". This
|
||||||
option does not affect login credentials.'''.replace(
|
option does not affect login credentials.""".replace(
|
||||||
"\n", " "
|
"\n", " "
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'-d',
|
"-d",
|
||||||
'--debug',
|
"--debug",
|
||||||
help='set logging to DEBUG. Can not be set via config file.',
|
help="set logging to DEBUG. Can not be set via config file.",
|
||||||
action='store_const',
|
action="store_const",
|
||||||
dest='log_level',
|
dest="log_level",
|
||||||
const=logging.DEBUG,
|
const=logging.DEBUG,
|
||||||
default=logging.INFO,
|
default=logging.INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
jabber_group = optparse.OptionGroup(parser, "Jabber configuration")
|
jabber_group = optparse.OptionGroup(parser, "Jabber configuration")
|
||||||
jabber_group.add_option(
|
jabber_group.add_option(
|
||||||
'--jid',
|
"--jid",
|
||||||
default=None,
|
default=None,
|
||||||
action='store',
|
action="store",
|
||||||
help="Your Jabber JID. If a resource is specified, "
|
help="Your Jabber JID. If a resource is specified, "
|
||||||
"it will be used as the nickname when joining MUCs. "
|
"it will be used as the nickname when joining MUCs. "
|
||||||
"Specifying the nickname is mostly useful if you want "
|
"Specifying the nickname is mostly useful if you want "
|
||||||
|
@ -353,27 +353,27 @@ option does not affect login credentials.'''.replace(
|
||||||
"from a dedicated account.",
|
"from a dedicated account.",
|
||||||
)
|
)
|
||||||
jabber_group.add_option(
|
jabber_group.add_option(
|
||||||
'--jabber-password', default=None, action='store', help="Your Jabber password"
|
"--jabber-password", default=None, action="store", help="Your Jabber password"
|
||||||
)
|
)
|
||||||
jabber_group.add_option(
|
jabber_group.add_option(
|
||||||
'--conference-domain',
|
"--conference-domain",
|
||||||
default=None,
|
default=None,
|
||||||
action='store',
|
action="store",
|
||||||
help="Your Jabber conference domain (E.g. conference.jabber.example.com). "
|
help="Your Jabber conference domain (E.g. conference.jabber.example.com). "
|
||||||
"If not specifed, \"conference.\" will be prepended to your JID's domain.",
|
'If not specifed, "conference." will be prepended to your JID\'s domain.',
|
||||||
)
|
)
|
||||||
jabber_group.add_option('--no-use-tls', default=None, action='store_true')
|
jabber_group.add_option("--no-use-tls", default=None, action="store_true")
|
||||||
jabber_group.add_option(
|
jabber_group.add_option(
|
||||||
'--jabber-server-address',
|
"--jabber-server-address",
|
||||||
default=None,
|
default=None,
|
||||||
action='store',
|
action="store",
|
||||||
help="The hostname of your Jabber server. This is only needed if "
|
help="The hostname of your Jabber server. This is only needed if "
|
||||||
"your server is missing SRV records",
|
"your server is missing SRV records",
|
||||||
)
|
)
|
||||||
jabber_group.add_option(
|
jabber_group.add_option(
|
||||||
'--jabber-server-port',
|
"--jabber-server-port",
|
||||||
default='5222',
|
default="5222",
|
||||||
action='store',
|
action="store",
|
||||||
help="The port of your Jabber server. This is only needed if "
|
help="The port of your Jabber server. This is only needed if "
|
||||||
"your server is missing SRV records",
|
"your server is missing SRV records",
|
||||||
)
|
)
|
||||||
|
@ -382,7 +382,7 @@ option does not affect login credentials.'''.replace(
|
||||||
parser.add_option_group(zulip.generate_option_group(parser, "zulip-"))
|
parser.add_option_group(zulip.generate_option_group(parser, "zulip-"))
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
logging.basicConfig(level=options.log_level, format='%(levelname)-8s %(message)s')
|
logging.basicConfig(level=options.log_level, format="%(levelname)-8s %(message)s")
|
||||||
|
|
||||||
if options.zulip_config_file is None:
|
if options.zulip_config_file is None:
|
||||||
default_config_file = zulip.get_default_config_filename()
|
default_config_file = zulip.get_default_config_filename()
|
||||||
|
@ -422,9 +422,9 @@ option does not affect login credentials.'''.replace(
|
||||||
options.mode = "personal"
|
options.mode = "personal"
|
||||||
|
|
||||||
if options.zulip_email_suffix is None:
|
if options.zulip_email_suffix is None:
|
||||||
options.zulip_email_suffix = ''
|
options.zulip_email_suffix = ""
|
||||||
|
|
||||||
if options.mode not in ('public', 'personal'):
|
if options.mode not in ("public", "personal"):
|
||||||
config_error("Bad value for --mode: must be one of 'public' or 'personal'")
|
config_error("Bad value for --mode: must be one of 'public' or 'personal'")
|
||||||
|
|
||||||
if None in (options.jid, options.jabber_password):
|
if None in (options.jid, options.jabber_password):
|
||||||
|
@ -437,7 +437,7 @@ option does not affect login credentials.'''.replace(
|
||||||
zulip.init_from_options(options, "JabberMirror/" + __version__)
|
zulip.init_from_options(options, "JabberMirror/" + __version__)
|
||||||
)
|
)
|
||||||
# This won't work for open realms that don't have a consistent domain
|
# This won't work for open realms that don't have a consistent domain
|
||||||
options.zulip_domain = zulipToJabber.client.email.partition('@')[-1]
|
options.zulip_domain = zulipToJabber.client.email.partition("@")[-1]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jid = JID(options.jid)
|
jid = JID(options.jid)
|
||||||
|
@ -460,10 +460,10 @@ option does not affect login credentials.'''.replace(
|
||||||
zulipToJabber.set_jabber_client(xmpp)
|
zulipToJabber.set_jabber_client(xmpp)
|
||||||
|
|
||||||
xmpp.process(block=False)
|
xmpp.process(block=False)
|
||||||
if options.mode == 'public':
|
if options.mode == "public":
|
||||||
event_types = ['stream']
|
event_types = ["stream"]
|
||||||
else:
|
else:
|
||||||
event_types = ['message', 'subscription']
|
event_types = ["message", "subscription"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info("Connecting to Zulip.")
|
logging.info("Connecting to Zulip.")
|
||||||
|
|
|
@ -96,7 +96,7 @@ def process_logs() -> None:
|
||||||
# immediately after rotation, this tool won't notice.
|
# immediately after rotation, this tool won't notice.
|
||||||
file_data["last"] = 1
|
file_data["last"] = 1
|
||||||
output = subprocess.check_output(["tail", "-n+%s" % (file_data["last"],), log_file])
|
output = subprocess.check_output(["tail", "-n+%s" % (file_data["last"],), log_file])
|
||||||
new_lines = output.decode('utf-8', errors='replace').split('\n')[:-1]
|
new_lines = output.decode("utf-8", errors="replace").split("\n")[:-1]
|
||||||
if len(new_lines) > 0:
|
if len(new_lines) > 0:
|
||||||
process_lines(new_lines, log_file)
|
process_lines(new_lines, log_file)
|
||||||
file_data["last"] += len(new_lines)
|
file_data["last"] += len(new_lines)
|
||||||
|
|
|
@ -9,19 +9,19 @@ VERSION = "0.9"
|
||||||
# In Nagios, "output" means "first line of output", and "long
|
# In Nagios, "output" means "first line of output", and "long
|
||||||
# output" means "other lines of output".
|
# output" means "other lines of output".
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser()) # type: argparse.ArgumentParser
|
parser = zulip.add_default_arguments(argparse.ArgumentParser()) # type: argparse.ArgumentParser
|
||||||
parser.add_argument('--output', default='')
|
parser.add_argument("--output", default="")
|
||||||
parser.add_argument('--long-output', default='')
|
parser.add_argument("--long-output", default="")
|
||||||
parser.add_argument('--stream', default='nagios')
|
parser.add_argument("--stream", default="nagios")
|
||||||
parser.add_argument('--config', default='/etc/nagios3/zuliprc')
|
parser.add_argument("--config", default="/etc/nagios3/zuliprc")
|
||||||
for opt in ('type', 'host', 'service', 'state'):
|
for opt in ("type", "host", "service", "state"):
|
||||||
parser.add_argument('--' + opt)
|
parser.add_argument("--" + opt)
|
||||||
opts = parser.parse_args()
|
opts = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.Client(
|
client = zulip.Client(
|
||||||
config_file=opts.config, client="ZulipNagios/" + VERSION
|
config_file=opts.config, client="ZulipNagios/" + VERSION
|
||||||
) # type: zulip.Client
|
) # type: zulip.Client
|
||||||
|
|
||||||
msg = dict(type='stream', to=opts.stream) # type: Dict[str, Any]
|
msg = dict(type="stream", to=opts.stream) # type: Dict[str, Any]
|
||||||
|
|
||||||
# Set a subject based on the host or service in question. This enables
|
# Set a subject based on the host or service in question. This enables
|
||||||
# threaded discussion of multiple concurrent issues, and provides useful
|
# threaded discussion of multiple concurrent issues, and provides useful
|
||||||
|
@ -30,24 +30,24 @@ msg = dict(type='stream', to=opts.stream) # type: Dict[str, Any]
|
||||||
# We send PROBLEM and RECOVERY messages to the same subject.
|
# We send PROBLEM and RECOVERY messages to the same subject.
|
||||||
if opts.service is None:
|
if opts.service is None:
|
||||||
# Host notification
|
# Host notification
|
||||||
thing = 'host' # type: Text
|
thing = "host" # type: Text
|
||||||
msg['subject'] = 'host %s' % (opts.host,)
|
msg["subject"] = "host %s" % (opts.host,)
|
||||||
else:
|
else:
|
||||||
# Service notification
|
# Service notification
|
||||||
thing = 'service'
|
thing = "service"
|
||||||
msg['subject'] = 'service %s on %s' % (opts.service, opts.host)
|
msg["subject"] = "service %s on %s" % (opts.service, opts.host)
|
||||||
|
|
||||||
if len(msg['subject']) > 60:
|
if len(msg["subject"]) > 60:
|
||||||
msg['subject'] = msg['subject'][0:57].rstrip() + "..."
|
msg["subject"] = msg["subject"][0:57].rstrip() + "..."
|
||||||
# e.g. **PROBLEM**: service is CRITICAL
|
# e.g. **PROBLEM**: service is CRITICAL
|
||||||
msg['content'] = '**%s**: %s is %s' % (opts.type, thing, opts.state)
|
msg["content"] = "**%s**: %s is %s" % (opts.type, thing, opts.state)
|
||||||
|
|
||||||
# The "long output" can contain newlines represented by "\n" escape sequences.
|
# The "long output" can contain newlines represented by "\n" escape sequences.
|
||||||
# The Nagios mail command uses /usr/bin/printf "%b" to expand these.
|
# The Nagios mail command uses /usr/bin/printf "%b" to expand these.
|
||||||
# We will be more conservative and handle just this one escape sequence.
|
# We will be more conservative and handle just this one escape sequence.
|
||||||
output = (opts.output + '\n' + opts.long_output.replace(r'\n', '\n')).strip() # type: Text
|
output = (opts.output + "\n" + opts.long_output.replace(r"\n", "\n")).strip() # type: Text
|
||||||
if output:
|
if output:
|
||||||
# Put any command output in a code block.
|
# Put any command output in a code block.
|
||||||
msg['content'] += '\n\n~~~~\n' + output + "\n~~~~\n"
|
msg["content"] += "\n\n~~~~\n" + output + "\n~~~~\n"
|
||||||
|
|
||||||
client.send_message(msg)
|
client.send_message(msg)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from typing import Dict
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
import zulip_openshift_config as config
|
import zulip_openshift_config as config
|
||||||
|
|
||||||
VERSION = '0.1'
|
VERSION = "0.1"
|
||||||
|
|
||||||
if config.ZULIP_API_PATH is not None:
|
if config.ZULIP_API_PATH is not None:
|
||||||
sys.path.append(config.ZULIP_API_PATH)
|
sys.path.append(config.ZULIP_API_PATH)
|
||||||
|
@ -21,7 +21,7 @@ client = zulip.Client(
|
||||||
email=config.ZULIP_USER,
|
email=config.ZULIP_USER,
|
||||||
site=config.ZULIP_SITE,
|
site=config.ZULIP_SITE,
|
||||||
api_key=config.ZULIP_API_KEY,
|
api_key=config.ZULIP_API_KEY,
|
||||||
client='ZulipOpenShift/' + VERSION,
|
client="ZulipOpenShift/" + VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,19 +29,19 @@ def get_deployment_details() -> Dict[str, str]:
|
||||||
# "gear deployments" output example:
|
# "gear deployments" output example:
|
||||||
# Activation time - Deployment ID - Git Ref - Git SHA1
|
# Activation time - Deployment ID - Git Ref - Git SHA1
|
||||||
# 2017-01-07 15:40:30 -0500 - 9e2b7143 - master - b9ce57c - ACTIVE
|
# 2017-01-07 15:40:30 -0500 - 9e2b7143 - master - b9ce57c - ACTIVE
|
||||||
dep = subprocess.check_output(['gear', 'deployments'], universal_newlines=True).splitlines()[1]
|
dep = subprocess.check_output(["gear", "deployments"], universal_newlines=True).splitlines()[1]
|
||||||
splits = dep.split(' - ')
|
splits = dep.split(" - ")
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
app_name=os.environ['OPENSHIFT_APP_NAME'],
|
app_name=os.environ["OPENSHIFT_APP_NAME"],
|
||||||
url=os.environ['OPENSHIFT_APP_DNS'],
|
url=os.environ["OPENSHIFT_APP_DNS"],
|
||||||
branch=splits[2],
|
branch=splits[2],
|
||||||
commit_id=splits[3],
|
commit_id=splits[3],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_bot_message(deployment: Dict[str, str]) -> None:
|
def send_bot_message(deployment: Dict[str, str]) -> None:
|
||||||
destination = config.deployment_notice_destination(deployment['branch'])
|
destination = config.deployment_notice_destination(deployment["branch"])
|
||||||
if destination is None:
|
if destination is None:
|
||||||
# No message should be sent
|
# No message should be sent
|
||||||
return
|
return
|
||||||
|
@ -49,10 +49,10 @@ def send_bot_message(deployment: Dict[str, str]) -> None:
|
||||||
|
|
||||||
client.send_message(
|
client.send_message(
|
||||||
{
|
{
|
||||||
'type': 'stream',
|
"type": "stream",
|
||||||
'to': destination['stream'],
|
"to": destination["stream"],
|
||||||
'subject': destination['subject'],
|
"subject": destination["subject"],
|
||||||
'content': message,
|
"content": message,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
from typing import Dict, Optional, Text
|
from typing import Dict, Optional, Text
|
||||||
|
|
||||||
# Change these values to configure authentication for the plugin
|
# Change these values to configure authentication for the plugin
|
||||||
ZULIP_USER = 'openshift-bot@example.com'
|
ZULIP_USER = "openshift-bot@example.com"
|
||||||
ZULIP_API_KEY = '0123456789abcdef0123456789abcdef'
|
ZULIP_API_KEY = "0123456789abcdef0123456789abcdef"
|
||||||
|
|
||||||
# deployment_notice_destination() lets you customize where deployment notices
|
# deployment_notice_destination() lets you customize where deployment notices
|
||||||
# are sent to with the full power of a Python function.
|
# are sent to with the full power of a Python function.
|
||||||
|
@ -20,8 +20,8 @@ ZULIP_API_KEY = '0123456789abcdef0123456789abcdef'
|
||||||
# * topic "master"
|
# * topic "master"
|
||||||
# And similarly for branch "test-post-receive" (for use when testing).
|
# And similarly for branch "test-post-receive" (for use when testing).
|
||||||
def deployment_notice_destination(branch: str) -> Optional[Dict[str, Text]]:
|
def deployment_notice_destination(branch: str) -> Optional[Dict[str, Text]]:
|
||||||
if branch in ['master', 'test-post-receive']:
|
if branch in ["master", "test-post-receive"]:
|
||||||
return dict(stream='deployments', subject='%s' % (branch,))
|
return dict(stream="deployments", subject="%s" % (branch,))
|
||||||
|
|
||||||
# Return None for cases where you don't want a notice sent
|
# Return None for cases where you don't want a notice sent
|
||||||
return None
|
return None
|
||||||
|
@ -39,14 +39,14 @@ def deployment_notice_destination(branch: str) -> Optional[Dict[str, Text]]:
|
||||||
# * dep_id = deployment id
|
# * dep_id = deployment id
|
||||||
# * dep_time = deployment timestamp
|
# * dep_time = deployment timestamp
|
||||||
def format_deployment_message(
|
def format_deployment_message(
|
||||||
app_name: str = '',
|
app_name: str = "",
|
||||||
url: str = '',
|
url: str = "",
|
||||||
branch: str = '',
|
branch: str = "",
|
||||||
commit_id: str = '',
|
commit_id: str = "",
|
||||||
dep_id: str = '',
|
dep_id: str = "",
|
||||||
dep_time: str = '',
|
dep_time: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
return 'Deployed commit `%s` (%s) in [%s](%s)' % (commit_id, branch, app_name, url)
|
return "Deployed commit `%s` (%s) in [%s](%s)" % (commit_id, branch, app_name, url)
|
||||||
|
|
||||||
|
|
||||||
## If properly installed, the Zulip API should be in your import
|
## If properly installed, the Zulip API should be in your import
|
||||||
|
@ -54,4 +54,4 @@ def format_deployment_message(
|
||||||
ZULIP_API_PATH = None # type: Optional[str]
|
ZULIP_API_PATH = None # type: Optional[str]
|
||||||
|
|
||||||
# Set this to your Zulip server's API URI
|
# Set this to your Zulip server's API URI
|
||||||
ZULIP_SITE = 'https://zulip.example.com'
|
ZULIP_SITE = "https://zulip.example.com"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
'''Zulip notification change-commit hook.
|
"""Zulip notification change-commit hook.
|
||||||
|
|
||||||
In Perforce, The "change-commit" trigger is fired after a metadata has been
|
In Perforce, The "change-commit" trigger is fired after a metadata has been
|
||||||
created, files have been transferred, and the changelist committed to the depot
|
created, files have been transferred, and the changelist committed to the depot
|
||||||
|
@ -12,7 +12,7 @@ This specific trigger expects command-line arguments in the form:
|
||||||
For example:
|
For example:
|
||||||
1234 //depot/security/src/
|
1234 //depot/security/src/
|
||||||
|
|
||||||
'''
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
@ -43,11 +43,11 @@ try:
|
||||||
changelist = int(sys.argv[1]) # type: int
|
changelist = int(sys.argv[1]) # type: int
|
||||||
changeroot = sys.argv[2] # type: str
|
changeroot = sys.argv[2] # type: str
|
||||||
except IndexError:
|
except IndexError:
|
||||||
print("Wrong number of arguments.\n\n", end=' ', file=sys.stderr)
|
print("Wrong number of arguments.\n\n", end=" ", file=sys.stderr)
|
||||||
print(__doc__, file=sys.stderr)
|
print(__doc__, file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("First argument must be an integer.\n\n", end=' ', file=sys.stderr)
|
print("First argument must be an integer.\n\n", end=" ", file=sys.stderr)
|
||||||
print(__doc__, file=sys.stderr)
|
print(__doc__, file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ if hasattr(config, "P4_WEB"):
|
||||||
|
|
||||||
if p4web is not None:
|
if p4web is not None:
|
||||||
# linkify the change number
|
# linkify the change number
|
||||||
change = '[{change}]({p4web}/{change}?ac=10)'.format(p4web=p4web, change=change)
|
change = "[{change}]({p4web}/{change}?ac=10)".format(p4web=p4web, change=change)
|
||||||
|
|
||||||
message = """**{user}** committed revision @{change} to `{path}`.
|
message = """**{user}** committed revision @{change} to `{path}`.
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ P4_WEB: Optional[str] = None
|
||||||
# * stream "depot_subdirectory-commits"
|
# * stream "depot_subdirectory-commits"
|
||||||
# * subject "change_root"
|
# * subject "change_root"
|
||||||
def commit_notice_destination(path: Text, changelist: int) -> Optional[Dict[Text, Text]]:
|
def commit_notice_destination(path: Text, changelist: int) -> Optional[Dict[Text, Text]]:
|
||||||
dirs = path.split('/')
|
dirs = path.split("/")
|
||||||
if len(dirs) >= 4 and dirs[3] not in ("*", "..."):
|
if len(dirs) >= 4 and dirs[3] not in ("*", "..."):
|
||||||
directory = dirs[3]
|
directory = dirs[3]
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -21,7 +21,7 @@ import feedparser
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
VERSION = "0.9" # type: str
|
VERSION = "0.9" # type: str
|
||||||
RSS_DATA_DIR = os.path.expanduser(os.path.join('~', '.cache', 'zulip-rss')) # type: str
|
RSS_DATA_DIR = os.path.expanduser(os.path.join("~", ".cache", "zulip-rss")) # type: str
|
||||||
OLDNESS_THRESHOLD = 30 # type: int
|
OLDNESS_THRESHOLD = 30 # type: int
|
||||||
|
|
||||||
usage = """Usage: Send summaries of RSS entries for your favorite feeds to Zulip.
|
usage = """Usage: Send summaries of RSS entries for your favorite feeds to Zulip.
|
||||||
|
@ -52,38 +52,38 @@ parser = zulip.add_default_arguments(
|
||||||
argparse.ArgumentParser(usage)
|
argparse.ArgumentParser(usage)
|
||||||
) # type: argparse.ArgumentParser
|
) # type: argparse.ArgumentParser
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--stream',
|
"--stream",
|
||||||
dest='stream',
|
dest="stream",
|
||||||
help='The stream to which to send RSS messages.',
|
help="The stream to which to send RSS messages.",
|
||||||
default="rss",
|
default="rss",
|
||||||
action='store',
|
action="store",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--data-dir',
|
"--data-dir",
|
||||||
dest='data_dir',
|
dest="data_dir",
|
||||||
help='The directory where feed metadata is stored',
|
help="The directory where feed metadata is stored",
|
||||||
default=os.path.join(RSS_DATA_DIR),
|
default=os.path.join(RSS_DATA_DIR),
|
||||||
action='store',
|
action="store",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--feed-file',
|
"--feed-file",
|
||||||
dest='feed_file',
|
dest="feed_file",
|
||||||
help='The file containing a list of RSS feed URLs to follow, one URL per line',
|
help="The file containing a list of RSS feed URLs to follow, one URL per line",
|
||||||
default=os.path.join(RSS_DATA_DIR, "rss-feeds"),
|
default=os.path.join(RSS_DATA_DIR, "rss-feeds"),
|
||||||
action='store',
|
action="store",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--unwrap',
|
"--unwrap",
|
||||||
dest='unwrap',
|
dest="unwrap",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Convert word-wrapped paragraphs into single lines',
|
help="Convert word-wrapped paragraphs into single lines",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--math',
|
"--math",
|
||||||
dest='math',
|
dest="math",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Convert $ to $$ (for KaTeX processing)',
|
help="Convert $ to $$ (for KaTeX processing)",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class MLStripper(HTMLParser):
|
||||||
self.fed.append(data)
|
self.fed.append(data)
|
||||||
|
|
||||||
def get_data(self) -> str:
|
def get_data(self) -> str:
|
||||||
return ''.join(self.fed)
|
return "".join(self.fed)
|
||||||
|
|
||||||
|
|
||||||
def strip_tags(html: str) -> str:
|
def strip_tags(html: str) -> str:
|
||||||
|
@ -155,13 +155,13 @@ def compute_entry_hash(entry: Dict[str, Any]) -> str:
|
||||||
def unwrap_text(body: str) -> str:
|
def unwrap_text(body: str) -> str:
|
||||||
# Replace \n by space if it is preceded and followed by a non-\n.
|
# Replace \n by space if it is preceded and followed by a non-\n.
|
||||||
# Example: '\na\nb\nc\n\nd\n' -> '\na b c\n\nd\n'
|
# Example: '\na\nb\nc\n\nd\n' -> '\na b c\n\nd\n'
|
||||||
return re.sub('(?<=[^\n])\n(?=[^\n])', ' ', body)
|
return re.sub("(?<=[^\n])\n(?=[^\n])", " ", body)
|
||||||
|
|
||||||
|
|
||||||
def elide_subject(subject: str) -> str:
|
def elide_subject(subject: str) -> str:
|
||||||
MAX_TOPIC_LENGTH = 60
|
MAX_TOPIC_LENGTH = 60
|
||||||
if len(subject) > MAX_TOPIC_LENGTH:
|
if len(subject) > MAX_TOPIC_LENGTH:
|
||||||
subject = subject[: MAX_TOPIC_LENGTH - 3].rstrip() + '...'
|
subject = subject[: MAX_TOPIC_LENGTH - 3].rstrip() + "..."
|
||||||
return subject
|
return subject
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ def send_zulip(entry: Any, feed_name: str) -> Dict[str, Any]:
|
||||||
) # type: str
|
) # type: str
|
||||||
|
|
||||||
if opts.math:
|
if opts.math:
|
||||||
content = content.replace('$', '$$')
|
content = content.replace("$", "$$")
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
"type": "stream",
|
"type": "stream",
|
||||||
|
|
|
@ -43,7 +43,7 @@ entry = svn.log(path, revision_end=pysvn.Revision(pysvn.opt_revision_kind.number
|
||||||
0
|
0
|
||||||
] # type: Dict[Text, Any]
|
] # type: Dict[Text, Any]
|
||||||
message = "**{}** committed revision r{} to `{}`.\n\n> {}".format(
|
message = "**{}** committed revision r{} to `{}`.\n\n> {}".format(
|
||||||
entry['author'], rev, path.split('/')[-1], entry['revprops']['svn:log']
|
entry["author"], rev, path.split("/")[-1], entry["revprops"]["svn:log"]
|
||||||
) # type: Text
|
) # type: Text
|
||||||
|
|
||||||
destination = config.commit_notice_destination(path, rev) # type: Optional[Dict[Text, Text]]
|
destination = config.commit_notice_destination(path, rev) # type: Optional[Dict[Text, Text]]
|
||||||
|
|
|
@ -19,7 +19,7 @@ ZULIP_API_KEY = "0123456789abcdef0123456789abcdef"
|
||||||
# * stream "commits"
|
# * stream "commits"
|
||||||
# * topic "branch_name"
|
# * topic "branch_name"
|
||||||
def commit_notice_destination(path: Text, commit: Text) -> Optional[Dict[Text, Text]]:
|
def commit_notice_destination(path: Text, commit: Text) -> Optional[Dict[Text, Text]]:
|
||||||
repo = path.split('/')[-1]
|
repo = path.split("/")[-1]
|
||||||
if repo not in ["evil-master-plan", "my-super-secret-repository"]:
|
if repo not in ["evil-master-plan", "my-super-secret-repository"]:
|
||||||
return dict(stream="commits", subject="%s" % (repo,))
|
return dict(stream="commits", subject="%s" % (repo,))
|
||||||
|
|
||||||
|
|
|
@ -100,24 +100,24 @@ class ZulipPlugin(Component):
|
||||||
|
|
||||||
content = "%s updated %s" % (author, markdown_ticket_url(ticket))
|
content = "%s updated %s" % (author, markdown_ticket_url(ticket))
|
||||||
if comment:
|
if comment:
|
||||||
content += ' with comment: %s\n\n' % (markdown_block(comment),)
|
content += " with comment: %s\n\n" % (markdown_block(comment),)
|
||||||
else:
|
else:
|
||||||
content += ":\n\n"
|
content += ":\n\n"
|
||||||
field_changes = []
|
field_changes = []
|
||||||
for key, value in old_values.items():
|
for key, value in old_values.items():
|
||||||
if key == "description":
|
if key == "description":
|
||||||
content += '- Changed %s from %s\n\nto %s' % (
|
content += "- Changed %s from %s\n\nto %s" % (
|
||||||
key,
|
key,
|
||||||
markdown_block(value),
|
markdown_block(value),
|
||||||
markdown_block(ticket.values.get(key)),
|
markdown_block(ticket.values.get(key)),
|
||||||
)
|
)
|
||||||
elif old_values.get(key) == "":
|
elif old_values.get(key) == "":
|
||||||
field_changes.append('%s: => **%s**' % (key, ticket.values.get(key)))
|
field_changes.append("%s: => **%s**" % (key, ticket.values.get(key)))
|
||||||
elif ticket.values.get(key) == "":
|
elif ticket.values.get(key) == "":
|
||||||
field_changes.append('%s: **%s** => ""' % (key, old_values.get(key)))
|
field_changes.append('%s: **%s** => ""' % (key, old_values.get(key)))
|
||||||
else:
|
else:
|
||||||
field_changes.append(
|
field_changes.append(
|
||||||
'%s: **%s** => **%s**' % (key, old_values.get(key), ticket.values.get(key))
|
"%s: **%s** => **%s**" % (key, old_values.get(key), ticket.values.get(key))
|
||||||
)
|
)
|
||||||
content += ", ".join(field_changes)
|
content += ", ".join(field_changes)
|
||||||
|
|
||||||
|
|
|
@ -25,22 +25,22 @@ def get_model_id(options):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trello_api_url = 'https://api.trello.com/1/board/{}'.format(options.trello_board_id)
|
trello_api_url = "https://api.trello.com/1/board/{}".format(options.trello_board_id)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'key': options.trello_api_key,
|
"key": options.trello_api_key,
|
||||||
'token': options.trello_token,
|
"token": options.trello_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
trello_response = requests.get(trello_api_url, params=params)
|
trello_response = requests.get(trello_api_url, params=params)
|
||||||
|
|
||||||
if trello_response.status_code != 200:
|
if trello_response.status_code != 200:
|
||||||
print('Error: Can\'t get the idModel. Please check the configuration')
|
print("Error: Can't get the idModel. Please check the configuration")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
board_info_json = trello_response.json()
|
board_info_json = trello_response.json()
|
||||||
|
|
||||||
return board_info_json['id']
|
return board_info_json["id"]
|
||||||
|
|
||||||
|
|
||||||
def get_webhook_id(options, id_model):
|
def get_webhook_id(options, id_model):
|
||||||
|
@ -55,27 +55,27 @@ def get_webhook_id(options, id_model):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trello_api_url = 'https://api.trello.com/1/webhooks/'
|
trello_api_url = "https://api.trello.com/1/webhooks/"
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'key': options.trello_api_key,
|
"key": options.trello_api_key,
|
||||||
'token': options.trello_token,
|
"token": options.trello_token,
|
||||||
'description': 'Webhook for Zulip integration (From Trello {} to Zulip)'.format(
|
"description": "Webhook for Zulip integration (From Trello {} to Zulip)".format(
|
||||||
options.trello_board_name,
|
options.trello_board_name,
|
||||||
),
|
),
|
||||||
'callbackURL': options.zulip_webhook_url,
|
"callbackURL": options.zulip_webhook_url,
|
||||||
'idModel': id_model,
|
"idModel": id_model,
|
||||||
}
|
}
|
||||||
|
|
||||||
trello_response = requests.post(trello_api_url, data=data)
|
trello_response = requests.post(trello_api_url, data=data)
|
||||||
|
|
||||||
if trello_response.status_code != 200:
|
if trello_response.status_code != 200:
|
||||||
print('Error: Can\'t create the Webhook:', trello_response.text)
|
print("Error: Can't create the Webhook:", trello_response.text)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
webhook_info_json = trello_response.json()
|
webhook_info_json = trello_response.json()
|
||||||
|
|
||||||
return webhook_info_json['id']
|
return webhook_info_json["id"]
|
||||||
|
|
||||||
|
|
||||||
def create_webhook(options):
|
def create_webhook(options):
|
||||||
|
@ -88,20 +88,20 @@ def create_webhook(options):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# first, we need to get the idModel
|
# first, we need to get the idModel
|
||||||
print('Getting Trello idModel for the {} board...'.format(options.trello_board_name))
|
print("Getting Trello idModel for the {} board...".format(options.trello_board_name))
|
||||||
|
|
||||||
id_model = get_model_id(options)
|
id_model = get_model_id(options)
|
||||||
|
|
||||||
if id_model:
|
if id_model:
|
||||||
print('Success! The idModel is', id_model)
|
print("Success! The idModel is", id_model)
|
||||||
|
|
||||||
id_webhook = get_webhook_id(options, id_model)
|
id_webhook = get_webhook_id(options, id_model)
|
||||||
|
|
||||||
if id_webhook:
|
if id_webhook:
|
||||||
print('Success! The webhook ID is', id_webhook)
|
print("Success! The webhook ID is", id_webhook)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
'Success! The webhook for the {} Trello board was successfully created.'.format(
|
"Success! The webhook for the {} Trello board was successfully created.".format(
|
||||||
options.trello_board_name
|
options.trello_board_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -118,36 +118,36 @@ at <https://zulip.com/integrations/doc/trello>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description=description)
|
parser = argparse.ArgumentParser(description=description)
|
||||||
parser.add_argument('--trello-board-name', required=True, help='The Trello board name.')
|
parser.add_argument("--trello-board-name", required=True, help="The Trello board name.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--trello-board-id',
|
"--trello-board-id",
|
||||||
required=True,
|
required=True,
|
||||||
help=('The Trello board short ID. Can usually be found ' 'in the URL of the Trello board.'),
|
help=("The Trello board short ID. Can usually be found " "in the URL of the Trello board."),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--trello-api-key',
|
"--trello-api-key",
|
||||||
required=True,
|
required=True,
|
||||||
help=(
|
help=(
|
||||||
'Visit https://trello.com/1/appkey/generate to generate '
|
"Visit https://trello.com/1/appkey/generate to generate "
|
||||||
'an APPLICATION_KEY (need to be logged into Trello).'
|
"an APPLICATION_KEY (need to be logged into Trello)."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--trello-token',
|
"--trello-token",
|
||||||
required=True,
|
required=True,
|
||||||
help=(
|
help=(
|
||||||
'Visit https://trello.com/1/appkey/generate and under '
|
"Visit https://trello.com/1/appkey/generate and under "
|
||||||
'`Developer API Keys`, click on `Token` and generate '
|
"`Developer API Keys`, click on `Token` and generate "
|
||||||
'a Trello access token.'
|
"a Trello access token."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--zulip-webhook-url', required=True, help='The webhook URL that Trello will query.'
|
"--zulip-webhook-url", required=True, help="The webhook URL that Trello will query."
|
||||||
)
|
)
|
||||||
|
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
create_webhook(options)
|
create_webhook(options)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -69,32 +69,32 @@ access token" as well. Fill in the values displayed.
|
||||||
|
|
||||||
|
|
||||||
def write_config(config: ConfigParser, configfile_path: str) -> None:
|
def write_config(config: ConfigParser, configfile_path: str) -> None:
|
||||||
with open(configfile_path, 'w') as configfile:
|
with open(configfile_path, "w") as configfile:
|
||||||
config.write(configfile)
|
config.write(configfile)
|
||||||
|
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser("Fetch tweets from Twitter."))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser("Fetch tweets from Twitter."))
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--instructions',
|
"--instructions",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Show instructions for the twitter bot setup and exit',
|
help="Show instructions for the twitter bot setup and exit",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--limit-tweets', default=15, type=int, help='Maximum number of tweets to send at once'
|
"--limit-tweets", default=15, type=int, help="Maximum number of tweets to send at once"
|
||||||
)
|
)
|
||||||
parser.add_argument('--search', dest='search_terms', help='Terms to search on', action='store')
|
parser.add_argument("--search", dest="search_terms", help="Terms to search on", action="store")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--stream',
|
"--stream",
|
||||||
dest='stream',
|
dest="stream",
|
||||||
help='The stream to which to send tweets',
|
help="The stream to which to send tweets",
|
||||||
default="twitter",
|
default="twitter",
|
||||||
action='store',
|
action="store",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--twitter-name', dest='twitter_name', help='Twitter username to poll new tweets from"'
|
"--twitter-name", dest="twitter_name", help='Twitter username to poll new tweets from"'
|
||||||
)
|
)
|
||||||
parser.add_argument('--excluded-terms', dest='excluded_terms', help='Terms to exclude tweets on')
|
parser.add_argument("--excluded-terms", dest="excluded_terms", help="Terms to exclude tweets on")
|
||||||
parser.add_argument('--excluded-users', dest='excluded_users', help='Users to exclude tweets on')
|
parser.add_argument("--excluded-users", dest="excluded_users", help="Users to exclude tweets on")
|
||||||
|
|
||||||
opts = parser.parse_args()
|
opts = parser.parse_args()
|
||||||
|
|
||||||
|
@ -103,15 +103,15 @@ if opts.instructions:
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
if all([opts.search_terms, opts.twitter_name]):
|
if all([opts.search_terms, opts.twitter_name]):
|
||||||
parser.error('You must only specify either a search term or a username.')
|
parser.error("You must only specify either a search term or a username.")
|
||||||
if opts.search_terms:
|
if opts.search_terms:
|
||||||
client_type = 'ZulipTwitterSearch/'
|
client_type = "ZulipTwitterSearch/"
|
||||||
CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitterrc_fetchsearch")
|
CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitterrc_fetchsearch")
|
||||||
elif opts.twitter_name:
|
elif opts.twitter_name:
|
||||||
client_type = 'ZulipTwitter/'
|
client_type = "ZulipTwitter/"
|
||||||
CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitteruserrc_fetchuser")
|
CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitteruserrc_fetchuser")
|
||||||
else:
|
else:
|
||||||
parser.error('You must either specify a search term or a username.')
|
parser.error("You must either specify a search term or a username.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
|
@ -119,10 +119,10 @@ try:
|
||||||
config_internal = ConfigParser()
|
config_internal = ConfigParser()
|
||||||
config_internal.read(CONFIGFILE_INTERNAL)
|
config_internal.read(CONFIGFILE_INTERNAL)
|
||||||
|
|
||||||
consumer_key = config.get('twitter', 'consumer_key')
|
consumer_key = config.get("twitter", "consumer_key")
|
||||||
consumer_secret = config.get('twitter', 'consumer_secret')
|
consumer_secret = config.get("twitter", "consumer_secret")
|
||||||
access_token_key = config.get('twitter', 'access_token_key')
|
access_token_key = config.get("twitter", "access_token_key")
|
||||||
access_token_secret = config.get('twitter', 'access_token_secret')
|
access_token_secret = config.get("twitter", "access_token_secret")
|
||||||
except (NoSectionError, NoOptionError):
|
except (NoSectionError, NoOptionError):
|
||||||
parser.error("Please provide a ~/.zulip_twitterrc")
|
parser.error("Please provide a ~/.zulip_twitterrc")
|
||||||
|
|
||||||
|
@ -130,17 +130,17 @@ if not all([consumer_key, consumer_secret, access_token_key, access_token_secret
|
||||||
parser.error("Please provide a ~/.zulip_twitterrc")
|
parser.error("Please provide a ~/.zulip_twitterrc")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
since_id = config_internal.getint('twitter', 'since_id')
|
since_id = config_internal.getint("twitter", "since_id")
|
||||||
except (NoOptionError, NoSectionError):
|
except (NoOptionError, NoSectionError):
|
||||||
since_id = 0
|
since_id = 0
|
||||||
try:
|
try:
|
||||||
previous_twitter_name = config_internal.get('twitter', 'twitter_name')
|
previous_twitter_name = config_internal.get("twitter", "twitter_name")
|
||||||
except (NoOptionError, NoSectionError):
|
except (NoOptionError, NoSectionError):
|
||||||
previous_twitter_name = ''
|
previous_twitter_name = ""
|
||||||
try:
|
try:
|
||||||
previous_search_terms = config_internal.get('twitter', 'search_terms')
|
previous_search_terms = config_internal.get("twitter", "search_terms")
|
||||||
except (NoOptionError, NoSectionError):
|
except (NoOptionError, NoSectionError):
|
||||||
previous_search_terms = ''
|
previous_search_terms = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import twitter
|
import twitter
|
||||||
|
@ -242,17 +242,17 @@ for status in statuses[::-1][: opts.limit_tweets]:
|
||||||
|
|
||||||
ret = client.send_message(message)
|
ret = client.send_message(message)
|
||||||
|
|
||||||
if ret['result'] == 'error':
|
if ret["result"] == "error":
|
||||||
# If sending failed (e.g. no such stream), abort and retry next time
|
# If sending failed (e.g. no such stream), abort and retry next time
|
||||||
print("Error sending message to zulip: %s" % ret['msg'])
|
print("Error sending message to zulip: %s" % ret["msg"])
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
since_id = status.id
|
since_id = status.id
|
||||||
|
|
||||||
if 'twitter' not in config_internal.sections():
|
if "twitter" not in config_internal.sections():
|
||||||
config_internal.add_section('twitter')
|
config_internal.add_section("twitter")
|
||||||
config_internal.set('twitter', 'since_id', str(since_id))
|
config_internal.set("twitter", "since_id", str(since_id))
|
||||||
config_internal.set('twitter', 'search_terms', str(opts.search_terms))
|
config_internal.set("twitter", "search_terms", str(opts.search_terms))
|
||||||
config_internal.set('twitter', 'twitter_name', str(opts.twitter_name))
|
config_internal.set("twitter", "twitter_name", str(opts.twitter_name))
|
||||||
|
|
||||||
write_config(config_internal, CONFIGFILE_INTERNAL)
|
write_config(config_internal, CONFIGFILE_INTERNAL)
|
||||||
|
|
|
@ -13,12 +13,12 @@ import zephyr
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
parser.add_option('--verbose', dest='verbose', default=False, action='store_true')
|
parser.add_option("--verbose", dest="verbose", default=False, action="store_true")
|
||||||
parser.add_option('--site', dest='site', default=None, action='store')
|
parser.add_option("--site", dest="site", default=None, action="store")
|
||||||
parser.add_option('--sharded', default=False, action='store_true')
|
parser.add_option("--sharded", default=False, action="store_true")
|
||||||
(options, args) = parser.parse_args()
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
mit_user = 'tabbott/extra@ATHENA.MIT.EDU'
|
mit_user = "tabbott/extra@ATHENA.MIT.EDU"
|
||||||
|
|
||||||
zulip_client = zulip.Client(verbose=True, client="ZulipMonitoring/0.1", site=options.site)
|
zulip_client = zulip.Client(verbose=True, client="ZulipMonitoring/0.1", site=options.site)
|
||||||
|
|
||||||
|
@ -116,11 +116,11 @@ def send_zephyr(zwrite_args: List[str], content: str) -> bool:
|
||||||
# Subscribe to Zulip
|
# Subscribe to Zulip
|
||||||
try:
|
try:
|
||||||
res = zulip_client.register(event_types=["message"])
|
res = zulip_client.register(event_types=["message"])
|
||||||
if 'error' in res['result']:
|
if "error" in res["result"]:
|
||||||
logging.error("Error subscribing to Zulips!")
|
logging.error("Error subscribing to Zulips!")
|
||||||
logging.error(res['msg'])
|
logging.error(res["msg"])
|
||||||
print_status_and_exit(1)
|
print_status_and_exit(1)
|
||||||
queue_id, last_event_id = (res['queue_id'], res['last_event_id'])
|
queue_id, last_event_id = (res["queue_id"], res["last_event_id"])
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Unexpected error subscribing to Zulips")
|
logger.exception("Unexpected error subscribing to Zulips")
|
||||||
print_status_and_exit(1)
|
print_status_and_exit(1)
|
||||||
|
@ -129,9 +129,9 @@ except Exception:
|
||||||
zephyr_subs_to_add = []
|
zephyr_subs_to_add = []
|
||||||
for (stream, test) in test_streams:
|
for (stream, test) in test_streams:
|
||||||
if stream == "message":
|
if stream == "message":
|
||||||
zephyr_subs_to_add.append((stream, 'personal', mit_user))
|
zephyr_subs_to_add.append((stream, "personal", mit_user))
|
||||||
else:
|
else:
|
||||||
zephyr_subs_to_add.append((stream, '*', '*'))
|
zephyr_subs_to_add.append((stream, "*", "*"))
|
||||||
|
|
||||||
actually_subscribed = False
|
actually_subscribed = False
|
||||||
for tries in range(10):
|
for tries in range(10):
|
||||||
|
@ -263,11 +263,11 @@ logger.info("Starting receiving messages!")
|
||||||
|
|
||||||
# receive zulips
|
# receive zulips
|
||||||
res = zulip_client.get_events(queue_id=queue_id, last_event_id=last_event_id)
|
res = zulip_client.get_events(queue_id=queue_id, last_event_id=last_event_id)
|
||||||
if 'error' in res['result']:
|
if "error" in res["result"]:
|
||||||
logging.error("Error receiving Zulips!")
|
logging.error("Error receiving Zulips!")
|
||||||
logging.error(res['msg'])
|
logging.error(res["msg"])
|
||||||
print_status_and_exit(1)
|
print_status_and_exit(1)
|
||||||
messages = [event['message'] for event in res['events']]
|
messages = [event["message"] for event in res["events"]]
|
||||||
logger.info("Finished receiving Zulip messages!")
|
logger.info("Finished receiving Zulip messages!")
|
||||||
|
|
||||||
receive_zephyrs()
|
receive_zephyrs()
|
||||||
|
@ -296,7 +296,7 @@ def process_keys(content_list: List[str]) -> Tuple[Dict[str, int], Set[str], Set
|
||||||
# The h_foo variables are about the messages we _received_ in Zulip
|
# The h_foo variables are about the messages we _received_ in Zulip
|
||||||
# The z_foo variables are about the messages we _received_ in Zephyr
|
# The z_foo variables are about the messages we _received_ in Zephyr
|
||||||
h_contents = [message["content"] for message in messages]
|
h_contents = [message["content"] for message in messages]
|
||||||
z_contents = [notice.message.split('\0')[1] for notice in notices]
|
z_contents = [notice.message.split("\0")[1] for notice in notices]
|
||||||
(h_key_counts, h_missing_z, h_missing_h, h_duplicates, h_success) = process_keys(h_contents)
|
(h_key_counts, h_missing_z, h_missing_h, h_duplicates, h_success) = process_keys(h_contents)
|
||||||
(z_key_counts, z_missing_z, z_missing_h, z_duplicates, z_success) = process_keys(z_contents)
|
(z_key_counts, z_missing_z, z_missing_h, z_duplicates, z_success) = process_keys(z_contents)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'api'))
|
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "api"))
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,20 +18,20 @@ def write_public_streams() -> None:
|
||||||
# normalization and then lower-casing server-side
|
# normalization and then lower-casing server-side
|
||||||
canonical_cls = unicodedata.normalize("NFKC", stream_name).lower()
|
canonical_cls = unicodedata.normalize("NFKC", stream_name).lower()
|
||||||
if canonical_cls in [
|
if canonical_cls in [
|
||||||
'security',
|
"security",
|
||||||
'login',
|
"login",
|
||||||
'network',
|
"network",
|
||||||
'ops',
|
"ops",
|
||||||
'user_locate',
|
"user_locate",
|
||||||
'mit',
|
"mit",
|
||||||
'moof',
|
"moof",
|
||||||
'wsmonitor',
|
"wsmonitor",
|
||||||
'wg_ctl',
|
"wg_ctl",
|
||||||
'winlogger',
|
"winlogger",
|
||||||
'hm_ctl',
|
"hm_ctl",
|
||||||
'hm_stat',
|
"hm_stat",
|
||||||
'zephyr_admin',
|
"zephyr_admin",
|
||||||
'zephyr_ctl',
|
"zephyr_ctl",
|
||||||
]:
|
]:
|
||||||
# These zephyr classes cannot be subscribed to by us, due
|
# These zephyr classes cannot be subscribed to by us, due
|
||||||
# to MIT's Zephyr access control settings
|
# to MIT's Zephyr access control settings
|
||||||
|
|
|
@ -39,8 +39,8 @@ def to_zulip_username(zephyr_username: str) -> str:
|
||||||
(user, realm) = (zephyr_username, "ATHENA.MIT.EDU")
|
(user, realm) = (zephyr_username, "ATHENA.MIT.EDU")
|
||||||
if realm.upper() == "ATHENA.MIT.EDU":
|
if realm.upper() == "ATHENA.MIT.EDU":
|
||||||
# Hack to make ctl's fake username setup work :)
|
# Hack to make ctl's fake username setup work :)
|
||||||
if user.lower() == 'golem':
|
if user.lower() == "golem":
|
||||||
user = 'ctl'
|
user = "ctl"
|
||||||
return user.lower() + "@mit.edu"
|
return user.lower() + "@mit.edu"
|
||||||
return user.lower() + "|" + realm.upper() + "@mit.edu"
|
return user.lower() + "|" + realm.upper() + "@mit.edu"
|
||||||
|
|
||||||
|
@ -49,10 +49,10 @@ def to_zephyr_username(zulip_username: str) -> str:
|
||||||
(user, realm) = zulip_username.split("@")
|
(user, realm) = zulip_username.split("@")
|
||||||
if "|" not in user:
|
if "|" not in user:
|
||||||
# Hack to make ctl's fake username setup work :)
|
# Hack to make ctl's fake username setup work :)
|
||||||
if user.lower() == 'ctl':
|
if user.lower() == "ctl":
|
||||||
user = 'golem'
|
user = "golem"
|
||||||
return user.lower() + "@ATHENA.MIT.EDU"
|
return user.lower() + "@ATHENA.MIT.EDU"
|
||||||
match_user = re.match(r'([a-zA-Z0-9_]+)\|(.+)', user)
|
match_user = re.match(r"([a-zA-Z0-9_]+)\|(.+)", user)
|
||||||
if not match_user:
|
if not match_user:
|
||||||
raise Exception("Could not parse Zephyr realm for cross-realm user %s" % (zulip_username,))
|
raise Exception("Could not parse Zephyr realm for cross-realm user %s" % (zulip_username,))
|
||||||
return match_user.group(1).lower() + "@" + match_user.group(2).upper()
|
return match_user.group(1).lower() + "@" + match_user.group(2).upper()
|
||||||
|
@ -85,14 +85,14 @@ def unwrap_lines(body: str) -> str:
|
||||||
previous_line = lines[0]
|
previous_line = lines[0]
|
||||||
for line in lines[1:]:
|
for line in lines[1:]:
|
||||||
line = line.rstrip()
|
line = line.rstrip()
|
||||||
if re.match(r'^\W', line, flags=re.UNICODE) and re.match(
|
if re.match(r"^\W", line, flags=re.UNICODE) and re.match(
|
||||||
r'^\W', previous_line, flags=re.UNICODE
|
r"^\W", previous_line, flags=re.UNICODE
|
||||||
):
|
):
|
||||||
result += previous_line + "\n"
|
result += previous_line + "\n"
|
||||||
elif (
|
elif (
|
||||||
line == ""
|
line == ""
|
||||||
or previous_line == ""
|
or previous_line == ""
|
||||||
or re.match(r'^\W', line, flags=re.UNICODE)
|
or re.match(r"^\W", line, flags=re.UNICODE)
|
||||||
or different_paragraph(previous_line, line)
|
or different_paragraph(previous_line, line)
|
||||||
):
|
):
|
||||||
# Use 2 newlines to separate sections so that we
|
# Use 2 newlines to separate sections so that we
|
||||||
|
@ -122,31 +122,31 @@ def send_zulip(zeph: ZephyrDict) -> Dict[str, Any]:
|
||||||
message = {}
|
message = {}
|
||||||
if options.forward_class_messages:
|
if options.forward_class_messages:
|
||||||
message["forged"] = "yes"
|
message["forged"] = "yes"
|
||||||
message['type'] = zeph['type']
|
message["type"] = zeph["type"]
|
||||||
message['time'] = zeph['time']
|
message["time"] = zeph["time"]
|
||||||
message['sender'] = to_zulip_username(zeph['sender'])
|
message["sender"] = to_zulip_username(zeph["sender"])
|
||||||
if "subject" in zeph:
|
if "subject" in zeph:
|
||||||
# Truncate the subject to the current limit in Zulip. No
|
# Truncate the subject to the current limit in Zulip. No
|
||||||
# need to do this for stream names, since we're only
|
# need to do this for stream names, since we're only
|
||||||
# subscribed to valid stream names.
|
# subscribed to valid stream names.
|
||||||
message["subject"] = zeph["subject"][:60]
|
message["subject"] = zeph["subject"][:60]
|
||||||
if zeph['type'] == 'stream':
|
if zeph["type"] == "stream":
|
||||||
# Forward messages sent to -c foo -i bar to stream bar subject "instance"
|
# Forward messages sent to -c foo -i bar to stream bar subject "instance"
|
||||||
if zeph["stream"] == "message":
|
if zeph["stream"] == "message":
|
||||||
message['to'] = zeph['subject'].lower()
|
message["to"] = zeph["subject"].lower()
|
||||||
message['subject'] = "instance %s" % (zeph['subject'],)
|
message["subject"] = "instance %s" % (zeph["subject"],)
|
||||||
elif zeph["stream"] == "tabbott-test5":
|
elif zeph["stream"] == "tabbott-test5":
|
||||||
message['to'] = zeph['subject'].lower()
|
message["to"] = zeph["subject"].lower()
|
||||||
message['subject'] = "test instance %s" % (zeph['subject'],)
|
message["subject"] = "test instance %s" % (zeph["subject"],)
|
||||||
else:
|
else:
|
||||||
message["to"] = zeph["stream"]
|
message["to"] = zeph["stream"]
|
||||||
else:
|
else:
|
||||||
message["to"] = zeph["recipient"]
|
message["to"] = zeph["recipient"]
|
||||||
message['content'] = unwrap_lines(zeph['content'])
|
message["content"] = unwrap_lines(zeph["content"])
|
||||||
|
|
||||||
if options.test_mode and options.site == DEFAULT_SITE:
|
if options.test_mode and options.site == DEFAULT_SITE:
|
||||||
logger.debug("Message is: %s" % (str(message),))
|
logger.debug("Message is: %s" % (str(message),))
|
||||||
return {'result': "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
return zulip_client.send_message(message)
|
return zulip_client.send_message(message)
|
||||||
|
|
||||||
|
@ -311,13 +311,13 @@ def parse_zephyr_body(zephyr_data: str, notice_format: str) -> Tuple[str, str]:
|
||||||
try:
|
try:
|
||||||
(zsig, body) = zephyr_data.split("\x00", 1)
|
(zsig, body) = zephyr_data.split("\x00", 1)
|
||||||
if (
|
if (
|
||||||
notice_format == 'New transaction [$1] entered in $2\nFrom: $3 ($5)\nSubject: $4'
|
notice_format == "New transaction [$1] entered in $2\nFrom: $3 ($5)\nSubject: $4"
|
||||||
or notice_format == 'New transaction [$1] entered in $2\nFrom: $3\nSubject: $4'
|
or notice_format == "New transaction [$1] entered in $2\nFrom: $3\nSubject: $4"
|
||||||
):
|
):
|
||||||
# Logic based off of owl_zephyr_get_message in barnowl
|
# Logic based off of owl_zephyr_get_message in barnowl
|
||||||
fields = body.split('\x00')
|
fields = body.split("\x00")
|
||||||
if len(fields) == 5:
|
if len(fields) == 5:
|
||||||
body = 'New transaction [%s] entered in %s\nFrom: %s (%s)\nSubject: %s' % (
|
body = "New transaction [%s] entered in %s\nFrom: %s (%s)\nSubject: %s" % (
|
||||||
fields[0],
|
fields[0],
|
||||||
fields[1],
|
fields[1],
|
||||||
fields[2],
|
fields[2],
|
||||||
|
@ -327,7 +327,7 @@ def parse_zephyr_body(zephyr_data: str, notice_format: str) -> Tuple[str, str]:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
(zsig, body) = ("", zephyr_data)
|
(zsig, body) = ("", zephyr_data)
|
||||||
# Clean body of any null characters, since they're invalid in our protocol.
|
# Clean body of any null characters, since they're invalid in our protocol.
|
||||||
body = body.replace('\x00', '')
|
body = body.replace("\x00", "")
|
||||||
return (zsig, body)
|
return (zsig, body)
|
||||||
|
|
||||||
|
|
||||||
|
@ -350,8 +350,8 @@ def parse_crypt_table(zephyr_class: str, instance: str) -> Optional[str]:
|
||||||
continue
|
continue
|
||||||
groups = match.groupdict()
|
groups = match.groupdict()
|
||||||
if (
|
if (
|
||||||
groups['class'].lower() == zephyr_class
|
groups["class"].lower() == zephyr_class
|
||||||
and 'keypath' in groups
|
and "keypath" in groups
|
||||||
and groups.get("algorithm") == "AES"
|
and groups.get("algorithm") == "AES"
|
||||||
):
|
):
|
||||||
return groups["keypath"]
|
return groups["keypath"]
|
||||||
|
@ -453,23 +453,23 @@ def process_notice(notice: "zephyr.ZNotice", log: Optional[IO[str]]) -> None:
|
||||||
|
|
||||||
zeph: ZephyrDict
|
zeph: ZephyrDict
|
||||||
zeph = {
|
zeph = {
|
||||||
'time': str(notice.time),
|
"time": str(notice.time),
|
||||||
'sender': notice.sender,
|
"sender": notice.sender,
|
||||||
'zsig': zsig, # logged here but not used by app
|
"zsig": zsig, # logged here but not used by app
|
||||||
'content': body,
|
"content": body,
|
||||||
}
|
}
|
||||||
if is_huddle:
|
if is_huddle:
|
||||||
zeph['type'] = 'private'
|
zeph["type"] = "private"
|
||||||
zeph['recipient'] = huddle_recipients
|
zeph["recipient"] = huddle_recipients
|
||||||
elif is_personal:
|
elif is_personal:
|
||||||
assert notice.recipient is not None
|
assert notice.recipient is not None
|
||||||
zeph['type'] = 'private'
|
zeph["type"] = "private"
|
||||||
zeph['recipient'] = to_zulip_username(notice.recipient)
|
zeph["recipient"] = to_zulip_username(notice.recipient)
|
||||||
else:
|
else:
|
||||||
zeph['type'] = 'stream'
|
zeph["type"] = "stream"
|
||||||
zeph['stream'] = zephyr_class
|
zeph["stream"] = zephyr_class
|
||||||
if notice.instance.strip() != "":
|
if notice.instance.strip() != "":
|
||||||
zeph['subject'] = notice.instance
|
zeph["subject"] = notice.instance
|
||||||
else:
|
else:
|
||||||
zeph["subject"] = '(instance "%s")' % (notice.instance,)
|
zeph["subject"] = '(instance "%s")' % (notice.instance,)
|
||||||
|
|
||||||
|
@ -489,7 +489,7 @@ def process_notice(notice: "zephyr.ZNotice", log: Optional[IO[str]]) -> None:
|
||||||
"Received a message on %s/%s from %s..." % (zephyr_class, notice.instance, notice.sender)
|
"Received a message on %s/%s from %s..." % (zephyr_class, notice.instance, notice.sender)
|
||||||
)
|
)
|
||||||
if log is not None:
|
if log is not None:
|
||||||
log.write(json.dumps(zeph) + '\n')
|
log.write(json.dumps(zeph) + "\n")
|
||||||
log.flush()
|
log.flush()
|
||||||
|
|
||||||
if os.fork() == 0:
|
if os.fork() == 0:
|
||||||
|
@ -593,7 +593,7 @@ def zephyr_to_zulip(options: optparse.Values) -> None:
|
||||||
zeph["subject"] = zeph["instance"]
|
zeph["subject"] = zeph["instance"]
|
||||||
logger.info(
|
logger.info(
|
||||||
"sending saved message to %s from %s..."
|
"sending saved message to %s from %s..."
|
||||||
% (zeph.get('stream', zeph.get('recipient')), zeph['sender'])
|
% (zeph.get("stream", zeph.get("recipient")), zeph["sender"])
|
||||||
)
|
)
|
||||||
send_zulip(zeph)
|
send_zulip(zeph)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -603,7 +603,7 @@ def zephyr_to_zulip(options: optparse.Values) -> None:
|
||||||
logger.info("Successfully initialized; Starting receive loop.")
|
logger.info("Successfully initialized; Starting receive loop.")
|
||||||
|
|
||||||
if options.resend_log_path is not None:
|
if options.resend_log_path is not None:
|
||||||
with open(options.resend_log_path, 'a') as log:
|
with open(options.resend_log_path, "a") as log:
|
||||||
process_loop(log)
|
process_loop(log)
|
||||||
else:
|
else:
|
||||||
process_loop(None)
|
process_loop(None)
|
||||||
|
@ -700,10 +700,10 @@ Feedback button or at support@zulip.com."""
|
||||||
]
|
]
|
||||||
|
|
||||||
# Hack to make ctl's fake username setup work :)
|
# Hack to make ctl's fake username setup work :)
|
||||||
if message['type'] == "stream" and zulip_account_email == "ctl@mit.edu":
|
if message["type"] == "stream" and zulip_account_email == "ctl@mit.edu":
|
||||||
zwrite_args.extend(["-S", "ctl"])
|
zwrite_args.extend(["-S", "ctl"])
|
||||||
|
|
||||||
if message['type'] == "stream":
|
if message["type"] == "stream":
|
||||||
zephyr_class = message["display_recipient"]
|
zephyr_class = message["display_recipient"]
|
||||||
instance = message["subject"]
|
instance = message["subject"]
|
||||||
|
|
||||||
|
@ -725,11 +725,11 @@ Feedback button or at support@zulip.com."""
|
||||||
zephyr_class = "message"
|
zephyr_class = "message"
|
||||||
zwrite_args.extend(["-c", zephyr_class, "-i", instance])
|
zwrite_args.extend(["-c", zephyr_class, "-i", instance])
|
||||||
logger.info("Forwarding message to class %s, instance %s" % (zephyr_class, instance))
|
logger.info("Forwarding message to class %s, instance %s" % (zephyr_class, instance))
|
||||||
elif message['type'] == "private":
|
elif message["type"] == "private":
|
||||||
if len(message['display_recipient']) == 1:
|
if len(message["display_recipient"]) == 1:
|
||||||
recipient = to_zephyr_username(message["display_recipient"][0]["email"])
|
recipient = to_zephyr_username(message["display_recipient"][0]["email"])
|
||||||
recipients = [recipient]
|
recipients = [recipient]
|
||||||
elif len(message['display_recipient']) == 2:
|
elif len(message["display_recipient"]) == 2:
|
||||||
recipient = ""
|
recipient = ""
|
||||||
for r in message["display_recipient"]:
|
for r in message["display_recipient"]:
|
||||||
if r["email"].lower() != zulip_account_email.lower():
|
if r["email"].lower() != zulip_account_email.lower():
|
||||||
|
@ -1085,62 +1085,62 @@ def configure_logger(logger: logging.Logger, direction_name: Optional[str]) -> N
|
||||||
def parse_args() -> Tuple[optparse.Values, List[str]]:
|
def parse_args() -> Tuple[optparse.Values, List[str]]:
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--forward-class-messages', default=False, help=optparse.SUPPRESS_HELP, action='store_true'
|
"--forward-class-messages", default=False, help=optparse.SUPPRESS_HELP, action="store_true"
|
||||||
)
|
)
|
||||||
parser.add_option('--shard', help=optparse.SUPPRESS_HELP)
|
parser.add_option("--shard", help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option('--noshard', default=False, help=optparse.SUPPRESS_HELP, action='store_true')
|
parser.add_option("--noshard", default=False, help=optparse.SUPPRESS_HELP, action="store_true")
|
||||||
parser.add_option('--resend-log', dest='logs_to_resend', help=optparse.SUPPRESS_HELP)
|
parser.add_option("--resend-log", dest="logs_to_resend", help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option('--enable-resend-log', dest='resend_log_path', help=optparse.SUPPRESS_HELP)
|
parser.add_option("--enable-resend-log", dest="resend_log_path", help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option('--log-path', dest='log_path', help=optparse.SUPPRESS_HELP)
|
parser.add_option("--log-path", dest="log_path", help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--stream-file-path',
|
"--stream-file-path",
|
||||||
dest='stream_file_path',
|
dest="stream_file_path",
|
||||||
default="/home/zulip/public_streams",
|
default="/home/zulip/public_streams",
|
||||||
help=optparse.SUPPRESS_HELP,
|
help=optparse.SUPPRESS_HELP,
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--no-forward-personals',
|
"--no-forward-personals",
|
||||||
dest='forward_personals',
|
dest="forward_personals",
|
||||||
help=optparse.SUPPRESS_HELP,
|
help=optparse.SUPPRESS_HELP,
|
||||||
default=True,
|
default=True,
|
||||||
action='store_false',
|
action="store_false",
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--forward-mail-zephyrs',
|
"--forward-mail-zephyrs",
|
||||||
dest='forward_mail_zephyrs',
|
dest="forward_mail_zephyrs",
|
||||||
help=optparse.SUPPRESS_HELP,
|
help=optparse.SUPPRESS_HELP,
|
||||||
default=False,
|
default=False,
|
||||||
action='store_true',
|
action="store_true",
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--no-forward-from-zulip',
|
"--no-forward-from-zulip",
|
||||||
default=True,
|
default=True,
|
||||||
dest='forward_from_zulip',
|
dest="forward_from_zulip",
|
||||||
help=optparse.SUPPRESS_HELP,
|
help=optparse.SUPPRESS_HELP,
|
||||||
action='store_false',
|
action="store_false",
|
||||||
)
|
)
|
||||||
parser.add_option('--verbose', default=False, help=optparse.SUPPRESS_HELP, action='store_true')
|
parser.add_option("--verbose", default=False, help=optparse.SUPPRESS_HELP, action="store_true")
|
||||||
parser.add_option('--sync-subscriptions', default=False, action='store_true')
|
parser.add_option("--sync-subscriptions", default=False, action="store_true")
|
||||||
parser.add_option('--ignore-expired-tickets', default=False, action='store_true')
|
parser.add_option("--ignore-expired-tickets", default=False, action="store_true")
|
||||||
parser.add_option('--site', default=DEFAULT_SITE, help=optparse.SUPPRESS_HELP)
|
parser.add_option("--site", default=DEFAULT_SITE, help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option('--on-startup-command', default=None, help=optparse.SUPPRESS_HELP)
|
parser.add_option("--on-startup-command", default=None, help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option('--user', default=os.environ["USER"], help=optparse.SUPPRESS_HELP)
|
parser.add_option("--user", default=os.environ["USER"], help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--stamp-path',
|
"--stamp-path",
|
||||||
default="/afs/athena.mit.edu/user/t/a/tabbott/for_friends",
|
default="/afs/athena.mit.edu/user/t/a/tabbott/for_friends",
|
||||||
help=optparse.SUPPRESS_HELP,
|
help=optparse.SUPPRESS_HELP,
|
||||||
)
|
)
|
||||||
parser.add_option('--session-path', default=None, help=optparse.SUPPRESS_HELP)
|
parser.add_option("--session-path", default=None, help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option('--nagios-class', default=None, help=optparse.SUPPRESS_HELP)
|
parser.add_option("--nagios-class", default=None, help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option('--nagios-path', default=None, help=optparse.SUPPRESS_HELP)
|
parser.add_option("--nagios-path", default=None, help=optparse.SUPPRESS_HELP)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--use-sessions', default=False, action='store_true', help=optparse.SUPPRESS_HELP
|
"--use-sessions", default=False, action="store_true", help=optparse.SUPPRESS_HELP
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--test-mode', default=False, help=optparse.SUPPRESS_HELP, action='store_true'
|
"--test-mode", default=False, help=optparse.SUPPRESS_HELP, action="store_true"
|
||||||
)
|
)
|
||||||
parser.add_option(
|
parser.add_option(
|
||||||
'--api-key-file', default=os.path.join(os.environ["HOME"], "Private", ".humbug-api-key")
|
"--api-key-file", default=os.path.join(os.environ["HOME"], "Private", ".humbug-api-key")
|
||||||
)
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
@ -1235,7 +1235,7 @@ or specify the --api-key-file option."""
|
||||||
# Personals mirror on behalf of another user.
|
# Personals mirror on behalf of another user.
|
||||||
pgrep_query = "%s.*--user=%s" % (pgrep_query, options.user)
|
pgrep_query = "%s.*--user=%s" % (pgrep_query, options.user)
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
['pgrep', '-U', os.environ["USER"], "-f", pgrep_query],
|
["pgrep", "-U", os.environ["USER"], "-f", pgrep_query],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,7 @@ def version() -> str:
|
||||||
version_line = next(
|
version_line = next(
|
||||||
itertools.dropwhile(lambda x: not x.startswith("__version__"), in_handle)
|
itertools.dropwhile(lambda x: not x.startswith("__version__"), in_handle)
|
||||||
)
|
)
|
||||||
version = version_line.split('=')[-1].strip().replace('"', '')
|
version = version_line.split("=")[-1].strip().replace('"', "")
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,50 +28,50 @@ def recur_expand(target_root: Any, dir: Any) -> Generator[Tuple[str, List[str]],
|
||||||
|
|
||||||
# We should be installable with either setuptools or distutils.
|
# We should be installable with either setuptools or distutils.
|
||||||
package_info = dict(
|
package_info = dict(
|
||||||
name='zulip',
|
name="zulip",
|
||||||
version=version(),
|
version=version(),
|
||||||
description='Bindings for the Zulip message API',
|
description="Bindings for the Zulip message API",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
author='Zulip Open Source Project',
|
author="Zulip Open Source Project",
|
||||||
author_email='zulip-devel@googlegroups.com',
|
author_email="zulip-devel@googlegroups.com",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
"Development Status :: 4 - Beta",
|
||||||
'Environment :: Web Environment',
|
"Environment :: Web Environment",
|
||||||
'Intended Audience :: Developers',
|
"Intended Audience :: Developers",
|
||||||
'License :: OSI Approved :: Apache Software License',
|
"License :: OSI Approved :: Apache Software License",
|
||||||
'Topic :: Communications :: Chat',
|
"Topic :: Communications :: Chat",
|
||||||
'Programming Language :: Python :: 3',
|
"Programming Language :: Python :: 3",
|
||||||
'Programming Language :: Python :: 3.6',
|
"Programming Language :: Python :: 3.6",
|
||||||
'Programming Language :: Python :: 3.7',
|
"Programming Language :: Python :: 3.7",
|
||||||
'Programming Language :: Python :: 3.8',
|
"Programming Language :: Python :: 3.8",
|
||||||
'Programming Language :: Python :: 3.9',
|
"Programming Language :: Python :: 3.9",
|
||||||
],
|
],
|
||||||
python_requires='>=3.6',
|
python_requires=">=3.6",
|
||||||
url='https://www.zulip.org/',
|
url="https://www.zulip.org/",
|
||||||
project_urls={
|
project_urls={
|
||||||
"Source": "https://github.com/zulip/python-zulip-api/",
|
"Source": "https://github.com/zulip/python-zulip-api/",
|
||||||
"Documentation": "https://zulip.com/api",
|
"Documentation": "https://zulip.com/api",
|
||||||
},
|
},
|
||||||
data_files=list(recur_expand('share/zulip', 'integrations')),
|
data_files=list(recur_expand("share/zulip", "integrations")),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
"console_scripts": [
|
||||||
'zulip-send=zulip.send:main',
|
"zulip-send=zulip.send:main",
|
||||||
'zulip-api-examples=zulip.api_examples:main',
|
"zulip-api-examples=zulip.api_examples:main",
|
||||||
'zulip-matrix-bridge=integrations.bridge_with_matrix.matrix_bridge:main',
|
"zulip-matrix-bridge=integrations.bridge_with_matrix.matrix_bridge:main",
|
||||||
'zulip-api=zulip.cli:cli',
|
"zulip-api=zulip.cli:cli",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
package_data={'zulip': ["py.typed"]},
|
package_data={"zulip": ["py.typed"]},
|
||||||
) # type: Dict[str, Any]
|
) # type: Dict[str, Any]
|
||||||
|
|
||||||
setuptools_info = dict(
|
setuptools_info = dict(
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'requests[security]>=0.12.1',
|
"requests[security]>=0.12.1",
|
||||||
'matrix_client',
|
"matrix_client",
|
||||||
'distro',
|
"distro",
|
||||||
'click',
|
"click",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ try:
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
package_info.update(setuptools_info)
|
package_info.update(setuptools_info)
|
||||||
package_info['packages'] = find_packages(exclude=['tests'])
|
package_info["packages"] = find_packages(exclude=["tests"])
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
@ -89,12 +89,12 @@ except ImportError:
|
||||||
try:
|
try:
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
assert LooseVersion(requests.__version__) >= LooseVersion('0.12.1')
|
assert LooseVersion(requests.__version__) >= LooseVersion("0.12.1")
|
||||||
except (ImportError, AssertionError):
|
except (ImportError, AssertionError):
|
||||||
print("requests >=0.12.1 is not installed", file=sys.stderr)
|
print("requests >=0.12.1 is not installed", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
package_info['packages'] = ['zulip']
|
package_info["packages"] = ["zulip"]
|
||||||
|
|
||||||
|
|
||||||
setup(**package_info)
|
setup(**package_info)
|
||||||
|
|
|
@ -15,8 +15,8 @@ class TestDefaultArguments(TestCase):
|
||||||
def test_invalid_arguments(self) -> None:
|
def test_invalid_arguments(self) -> None:
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum"))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum"))
|
||||||
with self.assertRaises(SystemExit) as cm:
|
with self.assertRaises(SystemExit) as cm:
|
||||||
with patch('sys.stderr', new=io.StringIO()) as mock_stderr:
|
with patch("sys.stderr", new=io.StringIO()) as mock_stderr:
|
||||||
parser.parse_args(['invalid argument'])
|
parser.parse_args(["invalid argument"])
|
||||||
self.assertEqual(cm.exception.code, 2)
|
self.assertEqual(cm.exception.code, 2)
|
||||||
# Assert that invalid arguments exit with printing the full usage (non-standard behavior)
|
# Assert that invalid arguments exit with printing the full usage (non-standard behavior)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
|
@ -32,20 +32,20 @@ Zulip API configuration:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('os.path.exists', return_value=False)
|
@patch("os.path.exists", return_value=False)
|
||||||
def test_config_path_with_tilde(self, mock_os_path_exists: bool) -> None:
|
def test_config_path_with_tilde(self, mock_os_path_exists: bool) -> None:
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum"))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum"))
|
||||||
test_path = '~/zuliprc'
|
test_path = "~/zuliprc"
|
||||||
args = parser.parse_args(['--config-file', test_path])
|
args = parser.parse_args(["--config-file", test_path])
|
||||||
with self.assertRaises(ZulipError) as cm:
|
with self.assertRaises(ZulipError) as cm:
|
||||||
zulip.init_from_options(args)
|
zulip.init_from_options(args)
|
||||||
expanded_test_path = os.path.abspath(os.path.expanduser(test_path))
|
expanded_test_path = os.path.abspath(os.path.expanduser(test_path))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str(cm.exception),
|
str(cm.exception),
|
||||||
'api_key or email not specified and '
|
"api_key or email not specified and "
|
||||||
'file {} does not exist'.format(expanded_test_path),
|
"file {} does not exist".format(expanded_test_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -9,17 +9,17 @@ import zulip
|
||||||
class TestHashUtilDecode(TestCase):
|
class TestHashUtilDecode(TestCase):
|
||||||
def test_hash_util_decode(self) -> None:
|
def test_hash_util_decode(self) -> None:
|
||||||
tests = [
|
tests = [
|
||||||
('topic', 'topic'),
|
("topic", "topic"),
|
||||||
('.2Edot', '.dot'),
|
(".2Edot", ".dot"),
|
||||||
('.23stream.20name', '#stream name'),
|
(".23stream.20name", "#stream name"),
|
||||||
('(no.20topic)', '(no topic)'),
|
("(no.20topic)", "(no topic)"),
|
||||||
('.3Cstrong.3Ebold.3C.2Fstrong.3E', '<strong>bold</strong>'),
|
(".3Cstrong.3Ebold.3C.2Fstrong.3E", "<strong>bold</strong>"),
|
||||||
('.3Asome_emoji.3A', ':some_emoji:'),
|
(".3Asome_emoji.3A", ":some_emoji:"),
|
||||||
]
|
]
|
||||||
for encoded_string, decoded_string in tests:
|
for encoded_string, decoded_string in tests:
|
||||||
with self.subTest(encoded_string=encoded_string):
|
with self.subTest(encoded_string=encoded_string):
|
||||||
self.assertEqual(zulip.hash_util_decode(encoded_string), decoded_string)
|
self.assertEqual(zulip.hash_util_decode(encoded_string), decoded_string)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,11 +11,11 @@ def main() -> None:
|
||||||
Prints the path to the Zulip API example scripts."""
|
Prints the path to the Zulip API example scripts."""
|
||||||
parser = argparse.ArgumentParser(usage=usage)
|
parser = argparse.ArgumentParser(usage=usage)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'script_name', nargs='?', default='', help='print path to the script <script_name>'
|
"script_name", nargs="?", default="", help="print path to the script <script_name>"
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
zulip_path = os.path.abspath(os.path.dirname(zulip.__file__))
|
zulip_path = os.path.abspath(os.path.dirname(zulip.__file__))
|
||||||
examples_path = os.path.abspath(os.path.join(zulip_path, 'examples', args.script_name))
|
examples_path = os.path.abspath(os.path.join(zulip_path, "examples", args.script_name))
|
||||||
if os.path.isdir(examples_path) or (args.script_name and os.path.isfile(examples_path)):
|
if os.path.isdir(examples_path) or (args.script_name and os.path.isfile(examples_path)):
|
||||||
print(examples_path)
|
print(examples_path)
|
||||||
else:
|
else:
|
||||||
|
@ -26,5 +26,5 @@ Prints the path to the Zulip API example scripts."""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -14,17 +14,17 @@ Example: alert-words remove banana
|
||||||
|
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('operation', choices=['get', 'add', 'remove'], type=str)
|
parser.add_argument("operation", choices=["get", "add", "remove"], type=str)
|
||||||
parser.add_argument('words', type=str, nargs='*')
|
parser.add_argument("words", type=str, nargs="*")
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
||||||
if options.operation == 'get':
|
if options.operation == "get":
|
||||||
result = client.get_alert_words()
|
result = client.get_alert_words()
|
||||||
elif options.operation == 'add':
|
elif options.operation == "add":
|
||||||
result = client.add_alert_words(options.words)
|
result = client.add_alert_words(options.words)
|
||||||
elif options.operation == 'remove':
|
elif options.operation == "remove":
|
||||||
result = client.remove_alert_words(options.words)
|
result = client.remove_alert_words(options.words)
|
||||||
|
|
||||||
print(result)
|
print(result)
|
||||||
|
|
|
@ -15,10 +15,10 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--new-email', required=True)
|
parser.add_argument("--new-email", required=True)
|
||||||
parser.add_argument('--new-password', required=True)
|
parser.add_argument("--new-password", required=True)
|
||||||
parser.add_argument('--new-full-name', required=True)
|
parser.add_argument("--new-full-name", required=True)
|
||||||
parser.add_argument('--new-short-name', required=True)
|
parser.add_argument("--new-short-name", required=True)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
@ -26,10 +26,10 @@ client = zulip.init_from_options(options)
|
||||||
print(
|
print(
|
||||||
client.create_user(
|
client.create_user(
|
||||||
{
|
{
|
||||||
'email': options.new_email,
|
"email": options.new_email,
|
||||||
'password': options.new_password,
|
"password": options.new_password,
|
||||||
'full_name': options.new_full_name,
|
"full_name": options.new_full_name,
|
||||||
'short_name': options.new_short_name,
|
"short_name": options.new_short_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@ Example: delete-message 42
|
||||||
|
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('message_id', type=int)
|
parser.add_argument("message_id", type=int)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -11,7 +11,7 @@ Example: delete-stream 42
|
||||||
|
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('stream_id', type=int)
|
parser.add_argument("stream_id", type=int)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -15,9 +15,9 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--message-id', type=int, required=True)
|
parser.add_argument("--message-id", type=int, required=True)
|
||||||
parser.add_argument('--subject', default="")
|
parser.add_argument("--subject", default="")
|
||||||
parser.add_argument('--content', default="")
|
parser.add_argument("--content", default="")
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -19,12 +19,12 @@ def quote(string: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--stream-id', type=int, required=True)
|
parser.add_argument("--stream-id", type=int, required=True)
|
||||||
parser.add_argument('--description')
|
parser.add_argument("--description")
|
||||||
parser.add_argument('--new-name')
|
parser.add_argument("--new-name")
|
||||||
parser.add_argument('--private', action='store_true')
|
parser.add_argument("--private", action="store_true")
|
||||||
parser.add_argument('--announcement-only', action='store_true')
|
parser.add_argument("--announcement-only", action="store_true")
|
||||||
parser.add_argument('--history-public-to-subscribers', action='store_true')
|
parser.add_argument("--history-public-to-subscribers", action="store_true")
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
@ -32,12 +32,12 @@ client = zulip.init_from_options(options)
|
||||||
print(
|
print(
|
||||||
client.update_stream(
|
client.update_stream(
|
||||||
{
|
{
|
||||||
'stream_id': options.stream_id,
|
"stream_id": options.stream_id,
|
||||||
'description': quote(options.description),
|
"description": quote(options.description),
|
||||||
'new_name': quote(options.new_name),
|
"new_name": quote(options.new_name),
|
||||||
'is_private': options.private,
|
"is_private": options.private,
|
||||||
'is_announcement_only': options.announcement_only,
|
"is_announcement_only": options.announcement_only,
|
||||||
'history_public_to_subscribers': options.history_public_to_subscribers,
|
"history_public_to_subscribers": options.history_public_to_subscribers,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,11 +13,11 @@ and store them in JSON format.
|
||||||
Example: get-history --stream announce --topic important"""
|
Example: get-history --stream announce --topic important"""
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--stream', required=True, help="The stream name to get the history")
|
parser.add_argument("--stream", required=True, help="The stream name to get the history")
|
||||||
parser.add_argument('--topic', help="The topic name to get the history")
|
parser.add_argument("--topic", help="The topic name to get the history")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--filename',
|
"--filename",
|
||||||
default='history.json',
|
default="history.json",
|
||||||
help="The file name to store the fetched \
|
help="The file name to store the fetched \
|
||||||
history.\n Default 'history.json'",
|
history.\n Default 'history.json'",
|
||||||
)
|
)
|
||||||
|
@ -25,19 +25,19 @@ options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
||||||
narrow = [{'operator': 'stream', 'operand': options.stream}]
|
narrow = [{"operator": "stream", "operand": options.stream}]
|
||||||
if options.topic:
|
if options.topic:
|
||||||
narrow.append({'operator': 'topic', 'operand': options.topic})
|
narrow.append({"operator": "topic", "operand": options.topic})
|
||||||
|
|
||||||
request = {
|
request = {
|
||||||
# Initially we have the anchor as 0, so that it starts fetching
|
# Initially we have the anchor as 0, so that it starts fetching
|
||||||
# from the oldest message in the narrow
|
# from the oldest message in the narrow
|
||||||
'anchor': 0,
|
"anchor": 0,
|
||||||
'num_before': 0,
|
"num_before": 0,
|
||||||
'num_after': 1000,
|
"num_after": 1000,
|
||||||
'narrow': narrow,
|
"narrow": narrow,
|
||||||
'client_gravatar': False,
|
"client_gravatar": False,
|
||||||
'apply_markdown': False,
|
"apply_markdown": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
all_messages = [] # type: List[Dict[str, Any]]
|
all_messages = [] # type: List[Dict[str, Any]]
|
||||||
|
@ -47,17 +47,17 @@ while not found_newest:
|
||||||
result = client.get_messages(request)
|
result = client.get_messages(request)
|
||||||
try:
|
try:
|
||||||
found_newest = result["found_newest"]
|
found_newest = result["found_newest"]
|
||||||
if result['messages']:
|
if result["messages"]:
|
||||||
# Setting the anchor to the next immediate message after the last fetched message.
|
# Setting the anchor to the next immediate message after the last fetched message.
|
||||||
request['anchor'] = result['messages'][-1]['id'] + 1
|
request["anchor"] = result["messages"][-1]["id"] + 1
|
||||||
|
|
||||||
all_messages.extend(result["messages"])
|
all_messages.extend(result["messages"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Might occur when the request is not returned with a success status
|
# Might occur when the request is not returned with a success status
|
||||||
print('Error occured: Payload was:')
|
print("Error occured: Payload was:")
|
||||||
print(result)
|
print(result)
|
||||||
quit()
|
quit()
|
||||||
|
|
||||||
with open(options.filename, "w+") as f:
|
with open(options.filename, "w+") as f:
|
||||||
print('Writing %d messages...' % len(all_messages))
|
print("Writing %d messages..." % len(all_messages))
|
||||||
f.write(json.dumps(all_messages))
|
f.write(json.dumps(all_messages))
|
||||||
|
|
|
@ -17,13 +17,13 @@ Example: get-messages --use-first-unread-anchor --num-before=5 \\
|
||||||
|
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--anchor', type=int)
|
parser.add_argument("--anchor", type=int)
|
||||||
parser.add_argument('--use-first-unread-anchor', action='store_true')
|
parser.add_argument("--use-first-unread-anchor", action="store_true")
|
||||||
parser.add_argument('--num-before', type=int, required=True)
|
parser.add_argument("--num-before", type=int, required=True)
|
||||||
parser.add_argument('--num-after', type=int, required=True)
|
parser.add_argument("--num-after", type=int, required=True)
|
||||||
parser.add_argument('--client-gravatar', action='store_true')
|
parser.add_argument("--client-gravatar", action="store_true")
|
||||||
parser.add_argument('--apply-markdown', action='store_true')
|
parser.add_argument("--apply-markdown", action="store_true")
|
||||||
parser.add_argument('--narrow')
|
parser.add_argument("--narrow")
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
@ -31,13 +31,13 @@ client = zulip.init_from_options(options)
|
||||||
print(
|
print(
|
||||||
client.get_messages(
|
client.get_messages(
|
||||||
{
|
{
|
||||||
'anchor': options.anchor,
|
"anchor": options.anchor,
|
||||||
'use_first_unread_anchor': options.use_first_unread_anchor,
|
"use_first_unread_anchor": options.use_first_unread_anchor,
|
||||||
'num_before': options.num_before,
|
"num_before": options.num_before,
|
||||||
'num_after': options.num_after,
|
"num_after": options.num_after,
|
||||||
'narrow': options.narrow,
|
"narrow": options.narrow,
|
||||||
'client_gravatar': options.client_gravatar,
|
"client_gravatar": options.client_gravatar,
|
||||||
'apply_markdown': options.apply_markdown,
|
"apply_markdown": options.apply_markdown,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,7 @@ Example: get-raw-message 42
|
||||||
|
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('message_id', type=int)
|
parser.add_argument("message_id", type=int)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -10,7 +10,7 @@ Get all the topics for a specific stream.
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--stream-id', required=True)
|
parser.add_argument("--stream-id", required=True)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -10,7 +10,7 @@ Get presence data for another user.
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--email', required=True)
|
parser.add_argument("--email", required=True)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -10,7 +10,7 @@ Example: message-history 42
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('message_id', type=int)
|
parser.add_argument("message_id", type=int)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -11,17 +11,17 @@ Example: mute-topic unmute Denmark party
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('op', choices=['mute', 'unmute'])
|
parser.add_argument("op", choices=["mute", "unmute"])
|
||||||
parser.add_argument('stream')
|
parser.add_argument("stream")
|
||||||
parser.add_argument('topic')
|
parser.add_argument("topic")
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
||||||
OPERATIONS = {'mute': 'add', 'unmute': 'remove'}
|
OPERATIONS = {"mute": "add", "unmute": "remove"}
|
||||||
|
|
||||||
print(
|
print(
|
||||||
client.mute_topic(
|
client.mute_topic(
|
||||||
{'op': OPERATIONS[options.op], 'stream': options.stream, 'topic': options.topic}
|
{"op": OPERATIONS[options.op], "stream": options.stream, "topic": options.topic}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,18 +12,18 @@ Example: send-message --type=stream commits --subject="my subject" --message="te
|
||||||
Example: send-message user1@example.com user2@example.com
|
Example: send-message user1@example.com user2@example.com
|
||||||
"""
|
"""
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('recipients', nargs='+')
|
parser.add_argument("recipients", nargs="+")
|
||||||
parser.add_argument('--subject', default='test')
|
parser.add_argument("--subject", default="test")
|
||||||
parser.add_argument('--message', default='test message')
|
parser.add_argument("--message", default="test message")
|
||||||
parser.add_argument('--type', default='private')
|
parser.add_argument("--type", default="private")
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
||||||
message_data = {
|
message_data = {
|
||||||
'type': options.type,
|
"type": options.type,
|
||||||
'content': options.message,
|
"content": options.message,
|
||||||
'subject': options.subject,
|
"subject": options.subject,
|
||||||
'to': options.recipients,
|
"to": options.recipients,
|
||||||
}
|
}
|
||||||
print(client.send_message(message_data))
|
print(client.send_message(message_data))
|
||||||
|
|
|
@ -15,7 +15,7 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--streams', action='store', required=True)
|
parser.add_argument("--streams", action="store", required=True)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -15,7 +15,7 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--streams', action='store', required=True)
|
parser.add_argument("--streams", action="store", required=True)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
|
@ -12,15 +12,15 @@ Example: update-message-flags remove starred 16 23 42
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('op', choices=['add', 'remove'])
|
parser.add_argument("op", choices=["add", "remove"])
|
||||||
parser.add_argument('flag')
|
parser.add_argument("flag")
|
||||||
parser.add_argument('messages', type=int, nargs='+')
|
parser.add_argument("messages", type=int, nargs="+")
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
client.update_message_flags(
|
client.update_message_flags(
|
||||||
{'op': options.op, 'flag': options.flag, 'messages': options.messages}
|
{"op": options.op, "flag": options.flag, "messages": options.messages}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import zulip
|
||||||
|
|
||||||
|
|
||||||
class StringIO(_StringIO):
|
class StringIO(_StringIO):
|
||||||
name = '' # https://github.com/python/typeshed/issues/598
|
name = "" # https://github.com/python/typeshed/issues/598
|
||||||
|
|
||||||
|
|
||||||
usage = """upload-file [options]
|
usage = """upload-file [options]
|
||||||
|
@ -22,20 +22,20 @@ If no --file-path is specified, a placeholder text file will be used instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
parser.add_argument('--file-path', required=True)
|
parser.add_argument("--file-path", required=True)
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
||||||
if options.file_path:
|
if options.file_path:
|
||||||
file = open(options.file_path, 'rb') # type: IO[Any]
|
file = open(options.file_path, "rb") # type: IO[Any]
|
||||||
else:
|
else:
|
||||||
file = StringIO('This is a test file.')
|
file = StringIO("This is a test file.")
|
||||||
file.name = 'test.txt'
|
file.name = "test.txt"
|
||||||
|
|
||||||
response = client.upload_file(file)
|
response = client.upload_file(file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print('File URI: {}'.format(response['uri']))
|
print("File URI: {}".format(response["uri"]))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print('Error! API response was: {}'.format(response))
|
print("Error! API response was: {}".format(response))
|
||||||
|
|
|
@ -4,7 +4,7 @@ from typing import Any, Dict, List
|
||||||
|
|
||||||
import zulip
|
import zulip
|
||||||
|
|
||||||
welcome_text = 'Hello {}, Welcome to Zulip!\n \
|
welcome_text = "Hello {}, Welcome to Zulip!\n \
|
||||||
* The first thing you should do is to install the development environment. \
|
* The first thing you should do is to install the development environment. \
|
||||||
We recommend following the vagrant setup as it is well documented and used \
|
We recommend following the vagrant setup as it is well documented and used \
|
||||||
by most of the contributors. If you face any trouble during installation \
|
by most of the contributors. If you face any trouble during installation \
|
||||||
|
@ -21,7 +21,7 @@ of the main projects you can contribute to are Zulip \
|
||||||
a [bot](https://github.com/zulip/zulipbot) that you can contribute to!!\n \
|
a [bot](https://github.com/zulip/zulipbot) that you can contribute to!!\n \
|
||||||
* We host our source code on GitHub. If you are not familiar with Git or \
|
* We host our source code on GitHub. If you are not familiar with Git or \
|
||||||
GitHub checkout [this](http://zulip.readthedocs.io/en/latest/git-guide.html) \
|
GitHub checkout [this](http://zulip.readthedocs.io/en/latest/git-guide.html) \
|
||||||
guide. You don\'t have to learn everything but please go through it and learn \
|
guide. You don't have to learn everything but please go through it and learn \
|
||||||
the basics. We are here to help you if you are having any trouble. Post your \
|
the basics. We are here to help you if you are having any trouble. Post your \
|
||||||
questions in #git help . \
|
questions in #git help . \
|
||||||
* Once you have completed these steps you can start contributing. You \
|
* Once you have completed these steps you can start contributing. You \
|
||||||
|
@ -33,55 +33,55 @@ but if you want a bite size issue for mobile or electron feel free post in #mobi
|
||||||
or #electron .\n \
|
or #electron .\n \
|
||||||
* Solving the first issue can be difficult. The key is to not give up. If you spend \
|
* Solving the first issue can be difficult. The key is to not give up. If you spend \
|
||||||
enough time on the issue you should be able to solve it no matter what.\n \
|
enough time on the issue you should be able to solve it no matter what.\n \
|
||||||
* Use `grep` command when you can\'t figure out what files to change :) For example \
|
* Use `grep` command when you can't figure out what files to change :) For example \
|
||||||
if you want know what files to modify in order to change Invite more users to Add \
|
if you want know what files to modify in order to change Invite more users to Add \
|
||||||
more users which you can see below the user status list, grep for "Invite more \
|
more users which you can see below the user status list, grep for \"Invite more \
|
||||||
users" in terminal.\n \
|
users\" in terminal.\n \
|
||||||
* If you are stuck with something and can\'t figure out what to do you can ask \
|
* If you are stuck with something and can't figure out what to do you can ask \
|
||||||
for help in #development help . But make sure that you tried your best to figure \
|
for help in #development help . But make sure that you tried your best to figure \
|
||||||
out the issue by yourself\n \
|
out the issue by yourself\n \
|
||||||
* If you are here for #Outreachy 2017-2018 or #GSoC don\'t worry much about \
|
* If you are here for #Outreachy 2017-2018 or #GSoC don't worry much about \
|
||||||
whether you will get selected or not. You will learn a lot contributing to \
|
whether you will get selected or not. You will learn a lot contributing to \
|
||||||
Zulip in course of next few months and if you do a good job at that you \
|
Zulip in course of next few months and if you do a good job at that you \
|
||||||
will get selected too :)\n \
|
will get selected too :)\n \
|
||||||
* Most important of all welcome to the Zulip family :octopus:'
|
* Most important of all welcome to the Zulip family :octopus:"
|
||||||
|
|
||||||
# These streams will cause the message to be sent
|
# These streams will cause the message to be sent
|
||||||
streams_to_watch = ['new members']
|
streams_to_watch = ["new members"]
|
||||||
|
|
||||||
# These streams will cause anyone who sends a message there to be removed from the watchlist
|
# These streams will cause anyone who sends a message there to be removed from the watchlist
|
||||||
streams_to_cancel = ['development help']
|
streams_to_cancel = ["development help"]
|
||||||
|
|
||||||
|
|
||||||
def get_watchlist() -> List[Any]:
|
def get_watchlist() -> List[Any]:
|
||||||
storage = client.get_storage()
|
storage = client.get_storage()
|
||||||
return list(storage['storage'].values())
|
return list(storage["storage"].values())
|
||||||
|
|
||||||
|
|
||||||
def set_watchlist(watchlist: List[str]) -> None:
|
def set_watchlist(watchlist: List[str]) -> None:
|
||||||
client.update_storage({'storage': dict(enumerate(watchlist))})
|
client.update_storage({"storage": dict(enumerate(watchlist))})
|
||||||
|
|
||||||
|
|
||||||
def handle_event(event: Dict[str, Any]) -> None:
|
def handle_event(event: Dict[str, Any]) -> None:
|
||||||
try:
|
try:
|
||||||
if event['type'] == 'realm_user' and event['op'] == 'add':
|
if event["type"] == "realm_user" and event["op"] == "add":
|
||||||
watchlist = get_watchlist()
|
watchlist = get_watchlist()
|
||||||
watchlist.append(event['person']['email'])
|
watchlist.append(event["person"]["email"])
|
||||||
set_watchlist(watchlist)
|
set_watchlist(watchlist)
|
||||||
return
|
return
|
||||||
if event['type'] == 'message':
|
if event["type"] == "message":
|
||||||
stream = event['message']['display_recipient']
|
stream = event["message"]["display_recipient"]
|
||||||
if stream not in streams_to_watch and stream not in streams_to_cancel:
|
if stream not in streams_to_watch and stream not in streams_to_cancel:
|
||||||
return
|
return
|
||||||
watchlist = get_watchlist()
|
watchlist = get_watchlist()
|
||||||
if event['message']['sender_email'] in watchlist:
|
if event["message"]["sender_email"] in watchlist:
|
||||||
watchlist.remove(event['message']['sender_email'])
|
watchlist.remove(event["message"]["sender_email"])
|
||||||
if stream not in streams_to_cancel:
|
if stream not in streams_to_cancel:
|
||||||
client.send_message(
|
client.send_message(
|
||||||
{
|
{
|
||||||
'type': 'private',
|
"type": "private",
|
||||||
'to': event['message']['sender_email'],
|
"to": event["message"]["sender_email"],
|
||||||
'content': welcome_text.format(event['message']['sender_short_name']),
|
"content": welcome_text.format(event["message"]["sender_short_name"]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
set_watchlist(watchlist)
|
set_watchlist(watchlist)
|
||||||
|
@ -92,7 +92,7 @@ def handle_event(event: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
def start_event_handler() -> None:
|
def start_event_handler() -> None:
|
||||||
print("Starting event handler...")
|
print("Starting event handler...")
|
||||||
client.call_on_each_event(handle_event, event_types=['realm_user', 'message'])
|
client.call_on_each_event(handle_event, event_types=["realm_user", "message"])
|
||||||
|
|
||||||
|
|
||||||
client = zulip.Client()
|
client = zulip.Client()
|
||||||
|
|
|
@ -10,25 +10,25 @@ import zulip
|
||||||
|
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
|
|
||||||
log = logging.getLogger('zulip-send')
|
log = logging.getLogger("zulip-send")
|
||||||
|
|
||||||
|
|
||||||
def do_send_message(client: zulip.Client, message_data: Dict[str, Any]) -> bool:
|
def do_send_message(client: zulip.Client, message_data: Dict[str, Any]) -> bool:
|
||||||
'''Sends a message and optionally prints status about the same.'''
|
"""Sends a message and optionally prints status about the same."""
|
||||||
|
|
||||||
if message_data['type'] == 'stream':
|
if message_data["type"] == "stream":
|
||||||
log.info(
|
log.info(
|
||||||
'Sending message to stream "%s", subject "%s"... '
|
'Sending message to stream "%s", subject "%s"... '
|
||||||
% (message_data['to'], message_data['subject'])
|
% (message_data["to"], message_data["subject"])
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.info('Sending message to %s... ' % (message_data['to'],))
|
log.info("Sending message to %s... " % (message_data["to"],))
|
||||||
response = client.send_message(message_data)
|
response = client.send_message(message_data)
|
||||||
if response['result'] == 'success':
|
if response["result"] == "success":
|
||||||
log.info('Message sent.')
|
log.info("Message sent.")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
log.error(response['msg'])
|
log.error(response["msg"])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,27 +46,27 @@ def main() -> int:
|
||||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'recipients', nargs='*', help='email addresses of the recipients of the message'
|
"recipients", nargs="*", help="email addresses of the recipients of the message"
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-m', '--message', help='Specifies the message to send, prevents interactive prompting.'
|
"-m", "--message", help="Specifies the message to send, prevents interactive prompting."
|
||||||
)
|
)
|
||||||
|
|
||||||
group = parser.add_argument_group('Stream parameters')
|
group = parser.add_argument_group("Stream parameters")
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
'-s',
|
"-s",
|
||||||
'--stream',
|
"--stream",
|
||||||
dest='stream',
|
dest="stream",
|
||||||
action='store',
|
action="store",
|
||||||
help='Allows the user to specify a stream for the message.',
|
help="Allows the user to specify a stream for the message.",
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
'-S',
|
"-S",
|
||||||
'--subject',
|
"--subject",
|
||||||
dest='subject',
|
dest="subject",
|
||||||
action='store',
|
action="store",
|
||||||
help='Allows the user to specify a subject for the message.',
|
help="Allows the user to specify a subject for the message.",
|
||||||
)
|
)
|
||||||
|
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
@ -75,11 +75,11 @@ def main() -> int:
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
# Sanity check user data
|
# Sanity check user data
|
||||||
if len(options.recipients) != 0 and (options.stream or options.subject):
|
if len(options.recipients) != 0 and (options.stream or options.subject):
|
||||||
parser.error('You cannot specify both a username and a stream/subject.')
|
parser.error("You cannot specify both a username and a stream/subject.")
|
||||||
if len(options.recipients) == 0 and (bool(options.stream) != bool(options.subject)):
|
if len(options.recipients) == 0 and (bool(options.stream) != bool(options.subject)):
|
||||||
parser.error('Stream messages must have a subject')
|
parser.error("Stream messages must have a subject")
|
||||||
if len(options.recipients) == 0 and not (options.stream and options.subject):
|
if len(options.recipients) == 0 and not (options.stream and options.subject):
|
||||||
parser.error('You must specify a stream/subject or at least one recipient.')
|
parser.error("You must specify a stream/subject or at least one recipient.")
|
||||||
|
|
||||||
client = zulip.init_from_options(options)
|
client = zulip.init_from_options(options)
|
||||||
|
|
||||||
|
@ -88,16 +88,16 @@ def main() -> int:
|
||||||
|
|
||||||
if options.stream:
|
if options.stream:
|
||||||
message_data = {
|
message_data = {
|
||||||
'type': 'stream',
|
"type": "stream",
|
||||||
'content': options.message,
|
"content": options.message,
|
||||||
'subject': options.subject,
|
"subject": options.subject,
|
||||||
'to': options.stream,
|
"to": options.stream,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
message_data = {
|
message_data = {
|
||||||
'type': 'private',
|
"type": "private",
|
||||||
'content': options.message,
|
"content": options.message,
|
||||||
'to': options.recipients,
|
"to": options.recipients,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not do_send_message(client, message_data):
|
if not do_send_message(client, message_data):
|
||||||
|
@ -105,5 +105,5 @@ def main() -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
|
@ -9,50 +9,50 @@ IS_PYPA_PACKAGE = False
|
||||||
|
|
||||||
|
|
||||||
package_data = {
|
package_data = {
|
||||||
'': ['doc.md', '*.conf', 'assets/*'],
|
"": ["doc.md", "*.conf", "assets/*"],
|
||||||
'zulip_bots': ['py.typed'],
|
"zulip_bots": ["py.typed"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# IS_PYPA_PACKAGE is set to True by tools/release-packages
|
# IS_PYPA_PACKAGE is set to True by tools/release-packages
|
||||||
# before making a PyPA release.
|
# before making a PyPA release.
|
||||||
if not IS_PYPA_PACKAGE:
|
if not IS_PYPA_PACKAGE:
|
||||||
package_data[''].append('fixtures/*.json')
|
package_data[""].append("fixtures/*.json")
|
||||||
package_data[''].append('logo.*')
|
package_data[""].append("logo.*")
|
||||||
|
|
||||||
with open("README.md") as fh:
|
with open("README.md") as fh:
|
||||||
long_description = fh.read()
|
long_description = fh.read()
|
||||||
|
|
||||||
# We should be installable with either setuptools or distutils.
|
# We should be installable with either setuptools or distutils.
|
||||||
package_info = dict(
|
package_info = dict(
|
||||||
name='zulip_bots',
|
name="zulip_bots",
|
||||||
version=ZULIP_BOTS_VERSION,
|
version=ZULIP_BOTS_VERSION,
|
||||||
description='Zulip\'s Bot framework',
|
description="Zulip's Bot framework",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
author='Zulip Open Source Project',
|
author="Zulip Open Source Project",
|
||||||
author_email='zulip-devel@googlegroups.com',
|
author_email="zulip-devel@googlegroups.com",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
"Development Status :: 4 - Beta",
|
||||||
'Environment :: Web Environment',
|
"Environment :: Web Environment",
|
||||||
'Intended Audience :: Developers',
|
"Intended Audience :: Developers",
|
||||||
'License :: OSI Approved :: Apache Software License',
|
"License :: OSI Approved :: Apache Software License",
|
||||||
'Topic :: Communications :: Chat',
|
"Topic :: Communications :: Chat",
|
||||||
'Programming Language :: Python :: 3',
|
"Programming Language :: Python :: 3",
|
||||||
'Programming Language :: Python :: 3.6',
|
"Programming Language :: Python :: 3.6",
|
||||||
'Programming Language :: Python :: 3.7',
|
"Programming Language :: Python :: 3.7",
|
||||||
'Programming Language :: Python :: 3.8',
|
"Programming Language :: Python :: 3.8",
|
||||||
'Programming Language :: Python :: 3.9',
|
"Programming Language :: Python :: 3.9",
|
||||||
],
|
],
|
||||||
python_requires='>=3.6',
|
python_requires=">=3.6",
|
||||||
url='https://www.zulip.org/',
|
url="https://www.zulip.org/",
|
||||||
project_urls={
|
project_urls={
|
||||||
"Source": "https://github.com/zulip/python-zulip-api/",
|
"Source": "https://github.com/zulip/python-zulip-api/",
|
||||||
"Documentation": "https://zulip.com/api",
|
"Documentation": "https://zulip.com/api",
|
||||||
},
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
"console_scripts": [
|
||||||
'zulip-run-bot=zulip_bots.run:main',
|
"zulip-run-bot=zulip_bots.run:main",
|
||||||
'zulip-terminal=zulip_bots.terminal:main',
|
"zulip-terminal=zulip_bots.terminal:main",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
@ -60,12 +60,12 @@ package_info = dict(
|
||||||
|
|
||||||
setuptools_info = dict(
|
setuptools_info = dict(
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pip',
|
"pip",
|
||||||
'zulip',
|
"zulip",
|
||||||
'html2text',
|
"html2text",
|
||||||
'lxml',
|
"lxml",
|
||||||
'BeautifulSoup4',
|
"BeautifulSoup4",
|
||||||
'typing_extensions',
|
"typing_extensions",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,8 +73,8 @@ try:
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
package_info.update(setuptools_info)
|
package_info.update(setuptools_info)
|
||||||
package_info['packages'] = find_packages()
|
package_info["packages"] = find_packages()
|
||||||
package_info['package_data'] = package_data
|
package_info["package_data"] = package_data
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
@ -97,17 +97,17 @@ except ImportError:
|
||||||
print("{name} is not installed.".format(name=module_name), file=sys.stderr)
|
print("{name} is not installed.".format(name=module_name), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
check_dependency_manually('zulip')
|
check_dependency_manually("zulip")
|
||||||
check_dependency_manually('mock', '2.0.0')
|
check_dependency_manually("mock", "2.0.0")
|
||||||
check_dependency_manually('html2text')
|
check_dependency_manually("html2text")
|
||||||
check_dependency_manually('PyDictionary')
|
check_dependency_manually("PyDictionary")
|
||||||
|
|
||||||
# Include all submodules under bots/
|
# Include all submodules under bots/
|
||||||
package_list = ['zulip_bots']
|
package_list = ["zulip_bots"]
|
||||||
dirs = os.listdir('zulip_bots/bots/')
|
dirs = os.listdir("zulip_bots/bots/")
|
||||||
for dir_name in dirs:
|
for dir_name in dirs:
|
||||||
if os.path.isdir(os.path.join('zulip_bots/bots/', dir_name)):
|
if os.path.isdir(os.path.join("zulip_bots/bots/", dir_name)):
|
||||||
package_list.append('zulip_bots.bots.' + dir_name)
|
package_list.append("zulip_bots.bots." + dir_name)
|
||||||
package_info['packages'] = package_list
|
package_info["packages"] = package_list
|
||||||
|
|
||||||
setup(**package_info)
|
setup(**package_info)
|
||||||
|
|
|
@ -9,31 +9,31 @@ from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
class BaremetricsHandler:
|
class BaremetricsHandler:
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('baremetrics')
|
self.config_info = bot_handler.get_config_info("baremetrics")
|
||||||
self.api_key = self.config_info['api_key']
|
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 = [
|
self.commands = [
|
||||||
'help',
|
"help",
|
||||||
'list-commands',
|
"list-commands",
|
||||||
'account-info',
|
"account-info",
|
||||||
'list-sources',
|
"list-sources",
|
||||||
'list-plans <source_id>',
|
"list-plans <source_id>",
|
||||||
'list-customers <source_id>',
|
"list-customers <source_id>",
|
||||||
'list-subscriptions <source_id>',
|
"list-subscriptions <source_id>",
|
||||||
'create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>',
|
"create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.descriptions = [
|
self.descriptions = [
|
||||||
'Display bot info',
|
"Display bot info",
|
||||||
'Display the list of available commands',
|
"Display the list of available commands",
|
||||||
'Display the account info',
|
"Display the account info",
|
||||||
'List the sources',
|
"List the sources",
|
||||||
'List the plans for the source',
|
"List the plans for the source",
|
||||||
'List the customers in the source',
|
"List the customers in the source",
|
||||||
'List the subscriptions in the source',
|
"List the subscriptions in the source",
|
||||||
'Create a plan in the given source',
|
"Create a plan in the given source",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.check_api_key(bot_handler)
|
self.check_api_key(bot_handler)
|
||||||
|
@ -44,36 +44,36 @@ class BaremetricsHandler:
|
||||||
test_query_data = test_query_response.json()
|
test_query_data = test_query_response.json()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if test_query_data['error'] == "Unauthorized. Token not found (001)":
|
if test_query_data["error"] == "Unauthorized. Token not found (001)":
|
||||||
bot_handler.quit('API Key not valid. Please see doc.md to find out how to get it.')
|
bot_handler.quit("API Key not valid. Please see doc.md to find out how to get it.")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
This bot gives updates about customer behavior, financial performance, and analytics
|
This bot gives updates about customer behavior, financial performance, and analytics
|
||||||
for an organization using the Baremetrics Api.\n
|
for an organization using the Baremetrics Api.\n
|
||||||
Enter `list-commands` to show the list of available commands.
|
Enter `list-commands` to show the list of available commands.
|
||||||
Version 1.0
|
Version 1.0
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None:
|
||||||
content = message['content'].strip().split()
|
content = message["content"].strip().split()
|
||||||
|
|
||||||
if content == []:
|
if content == []:
|
||||||
bot_handler.send_reply(message, 'No Command Specified')
|
bot_handler.send_reply(message, "No Command Specified")
|
||||||
return
|
return
|
||||||
|
|
||||||
content[0] = content[0].lower()
|
content[0] = content[0].lower()
|
||||||
|
|
||||||
if content == ['help']:
|
if content == ["help"]:
|
||||||
bot_handler.send_reply(message, self.usage())
|
bot_handler.send_reply(message, self.usage())
|
||||||
return
|
return
|
||||||
|
|
||||||
if content == ['list-commands']:
|
if content == ["list-commands"]:
|
||||||
response = '**Available Commands:** \n'
|
response = "**Available Commands:** \n"
|
||||||
for command, description in zip(self.commands, self.descriptions):
|
for command, description in zip(self.commands, self.descriptions):
|
||||||
response += ' - {} : {}\n'.format(command, description)
|
response += " - {} : {}\n".format(command, description)
|
||||||
|
|
||||||
bot_handler.send_reply(message, response)
|
bot_handler.send_reply(message, response)
|
||||||
return
|
return
|
||||||
|
@ -85,177 +85,177 @@ class BaremetricsHandler:
|
||||||
try:
|
try:
|
||||||
instruction = commands[0]
|
instruction = commands[0]
|
||||||
|
|
||||||
if instruction == 'account-info':
|
if instruction == "account-info":
|
||||||
return self.get_account_info()
|
return self.get_account_info()
|
||||||
|
|
||||||
if instruction == 'list-sources':
|
if instruction == "list-sources":
|
||||||
return self.get_sources()
|
return self.get_sources()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if instruction == 'list-plans':
|
if instruction == "list-plans":
|
||||||
return self.get_plans(commands[1])
|
return self.get_plans(commands[1])
|
||||||
|
|
||||||
if instruction == 'list-customers':
|
if instruction == "list-customers":
|
||||||
return self.get_customers(commands[1])
|
return self.get_customers(commands[1])
|
||||||
|
|
||||||
if instruction == 'list-subscriptions':
|
if instruction == "list-subscriptions":
|
||||||
return self.get_subscriptions(commands[1])
|
return self.get_subscriptions(commands[1])
|
||||||
|
|
||||||
if instruction == 'create-plan':
|
if instruction == "create-plan":
|
||||||
if len(commands) == 8:
|
if len(commands) == 8:
|
||||||
return self.create_plan(commands[1:])
|
return self.create_plan(commands[1:])
|
||||||
else:
|
else:
|
||||||
return 'Invalid number of arguments.'
|
return "Invalid number of arguments."
|
||||||
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return 'Missing Params.'
|
return "Missing Params."
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return 'Invalid Response From API.'
|
return "Invalid Response From API."
|
||||||
|
|
||||||
return 'Invalid Command.'
|
return "Invalid Command."
|
||||||
|
|
||||||
def get_account_info(self) -> str:
|
def get_account_info(self) -> str:
|
||||||
url = "https://api.baremetrics.com/v1/account"
|
url = "https://api.baremetrics.com/v1/account"
|
||||||
account_response = requests.get(url, headers=self.auth_header)
|
account_response = requests.get(url, headers=self.auth_header)
|
||||||
|
|
||||||
account_data = account_response.json()
|
account_data = account_response.json()
|
||||||
account_data = account_data['account']
|
account_data = account_data["account"]
|
||||||
|
|
||||||
template = [
|
template = [
|
||||||
'**Your account information:**',
|
"**Your account information:**",
|
||||||
'Id: {id}',
|
"Id: {id}",
|
||||||
'Company: {company}',
|
"Company: {company}",
|
||||||
'Default Currency: {currency}',
|
"Default Currency: {currency}",
|
||||||
]
|
]
|
||||||
|
|
||||||
return "\n".join(template).format(
|
return "\n".join(template).format(
|
||||||
currency=account_data['default_currency']['name'], **account_data
|
currency=account_data["default_currency"]["name"], **account_data
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_sources(self) -> str:
|
def get_sources(self) -> str:
|
||||||
url = 'https://api.baremetrics.com/v1/sources'
|
url = "https://api.baremetrics.com/v1/sources"
|
||||||
sources_response = requests.get(url, headers=self.auth_header)
|
sources_response = requests.get(url, headers=self.auth_header)
|
||||||
|
|
||||||
sources_data = sources_response.json()
|
sources_data = sources_response.json()
|
||||||
sources_data = sources_data['sources']
|
sources_data = sources_data["sources"]
|
||||||
|
|
||||||
response = '**Listing sources:** \n'
|
response = "**Listing sources:** \n"
|
||||||
for index, source in enumerate(sources_data):
|
for index, source in enumerate(sources_data):
|
||||||
response += (
|
response += (
|
||||||
'{_count}.ID: {id}\n' 'Provider: {provider}\n' 'Provider ID: {provider_id}\n\n'
|
"{_count}.ID: {id}\n" "Provider: {provider}\n" "Provider ID: {provider_id}\n\n"
|
||||||
).format(_count=index + 1, **source)
|
).format(_count=index + 1, **source)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_plans(self, source_id: str) -> str:
|
def get_plans(self, source_id: str) -> str:
|
||||||
url = 'https://api.baremetrics.com/v1/{}/plans'.format(source_id)
|
url = "https://api.baremetrics.com/v1/{}/plans".format(source_id)
|
||||||
plans_response = requests.get(url, headers=self.auth_header)
|
plans_response = requests.get(url, headers=self.auth_header)
|
||||||
|
|
||||||
plans_data = plans_response.json()
|
plans_data = plans_response.json()
|
||||||
plans_data = plans_data['plans']
|
plans_data = plans_data["plans"]
|
||||||
|
|
||||||
template = '\n'.join(
|
template = "\n".join(
|
||||||
[
|
[
|
||||||
'{_count}.Name: {name}',
|
"{_count}.Name: {name}",
|
||||||
'Active: {active}',
|
"Active: {active}",
|
||||||
'Interval: {interval}',
|
"Interval: {interval}",
|
||||||
'Interval Count: {interval_count}',
|
"Interval Count: {interval_count}",
|
||||||
'Amounts:',
|
"Amounts:",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
response = ['**Listing plans:**']
|
response = ["**Listing plans:**"]
|
||||||
for index, plan in enumerate(plans_data):
|
for index, plan in enumerate(plans_data):
|
||||||
response += (
|
response += (
|
||||||
[template.format(_count=index + 1, **plan)]
|
[template.format(_count=index + 1, **plan)]
|
||||||
+ [' - {amount} {currency}'.format(**amount) for amount in plan['amounts']]
|
+ [" - {amount} {currency}".format(**amount) for amount in plan["amounts"]]
|
||||||
+ ['']
|
+ [""]
|
||||||
)
|
)
|
||||||
|
|
||||||
return '\n'.join(response)
|
return "\n".join(response)
|
||||||
|
|
||||||
def get_customers(self, source_id: str) -> str:
|
def get_customers(self, source_id: str) -> str:
|
||||||
url = 'https://api.baremetrics.com/v1/{}/customers'.format(source_id)
|
url = "https://api.baremetrics.com/v1/{}/customers".format(source_id)
|
||||||
customers_response = requests.get(url, headers=self.auth_header)
|
customers_response = requests.get(url, headers=self.auth_header)
|
||||||
|
|
||||||
customers_data = customers_response.json()
|
customers_data = customers_response.json()
|
||||||
customers_data = customers_data['customers']
|
customers_data = customers_data["customers"]
|
||||||
|
|
||||||
# FIXME BUG here? mismatch of name and display name?
|
# FIXME BUG here? mismatch of name and display name?
|
||||||
template = '\n'.join(
|
template = "\n".join(
|
||||||
[
|
[
|
||||||
'{_count}.Name: {display_name}',
|
"{_count}.Name: {display_name}",
|
||||||
'Display Name: {name}',
|
"Display Name: {name}",
|
||||||
'OID: {oid}',
|
"OID: {oid}",
|
||||||
'Active: {is_active}',
|
"Active: {is_active}",
|
||||||
'Email: {email}',
|
"Email: {email}",
|
||||||
'Notes: {notes}',
|
"Notes: {notes}",
|
||||||
'Current Plans:',
|
"Current Plans:",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
response = ['**Listing customers:**']
|
response = ["**Listing customers:**"]
|
||||||
for index, customer in enumerate(customers_data):
|
for index, customer in enumerate(customers_data):
|
||||||
response += (
|
response += (
|
||||||
[template.format(_count=index + 1, **customer)]
|
[template.format(_count=index + 1, **customer)]
|
||||||
+ [' - {name}'.format(**plan) for plan in customer['current_plans']]
|
+ [" - {name}".format(**plan) for plan in customer["current_plans"]]
|
||||||
+ ['']
|
+ [""]
|
||||||
)
|
)
|
||||||
|
|
||||||
return '\n'.join(response)
|
return "\n".join(response)
|
||||||
|
|
||||||
def get_subscriptions(self, source_id: str) -> str:
|
def get_subscriptions(self, source_id: str) -> str:
|
||||||
url = 'https://api.baremetrics.com/v1/{}/subscriptions'.format(source_id)
|
url = "https://api.baremetrics.com/v1/{}/subscriptions".format(source_id)
|
||||||
subscriptions_response = requests.get(url, headers=self.auth_header)
|
subscriptions_response = requests.get(url, headers=self.auth_header)
|
||||||
|
|
||||||
subscriptions_data = subscriptions_response.json()
|
subscriptions_data = subscriptions_response.json()
|
||||||
subscriptions_data = subscriptions_data['subscriptions']
|
subscriptions_data = subscriptions_data["subscriptions"]
|
||||||
|
|
||||||
template = '\n'.join(
|
template = "\n".join(
|
||||||
[
|
[
|
||||||
'{_count}.Customer Name: {name}',
|
"{_count}.Customer Name: {name}",
|
||||||
'Customer Display Name: {display_name}',
|
"Customer Display Name: {display_name}",
|
||||||
'Customer OID: {oid}',
|
"Customer OID: {oid}",
|
||||||
'Customer Email: {email}',
|
"Customer Email: {email}",
|
||||||
'Active: {_active}',
|
"Active: {_active}",
|
||||||
'Plan Name: {_plan_name}',
|
"Plan Name: {_plan_name}",
|
||||||
'Plan Amounts:',
|
"Plan Amounts:",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
response = ['**Listing subscriptions:**']
|
response = ["**Listing subscriptions:**"]
|
||||||
for index, subscription in enumerate(subscriptions_data):
|
for index, subscription in enumerate(subscriptions_data):
|
||||||
response += (
|
response += (
|
||||||
[
|
[
|
||||||
template.format(
|
template.format(
|
||||||
_count=index + 1,
|
_count=index + 1,
|
||||||
_active=subscription['active'],
|
_active=subscription["active"],
|
||||||
_plan_name=subscription['plan']['name'],
|
_plan_name=subscription["plan"]["name"],
|
||||||
**subscription['customer'],
|
**subscription["customer"],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
+ [
|
+ [
|
||||||
' - {amount} {symbol}'.format(**amount)
|
" - {amount} {symbol}".format(**amount)
|
||||||
for amount in subscription['plan']['amounts']
|
for amount in subscription["plan"]["amounts"]
|
||||||
]
|
]
|
||||||
+ ['']
|
+ [""]
|
||||||
)
|
)
|
||||||
|
|
||||||
return '\n'.join(response)
|
return "\n".join(response)
|
||||||
|
|
||||||
def create_plan(self, parameters: List[str]) -> str:
|
def create_plan(self, parameters: List[str]) -> str:
|
||||||
data_header = {
|
data_header = {
|
||||||
'oid': parameters[1],
|
"oid": parameters[1],
|
||||||
'name': parameters[2],
|
"name": parameters[2],
|
||||||
'currency': parameters[3],
|
"currency": parameters[3],
|
||||||
'amount': int(parameters[4]),
|
"amount": int(parameters[4]),
|
||||||
'interval': parameters[5],
|
"interval": parameters[5],
|
||||||
'interval_count': int(parameters[6]),
|
"interval_count": int(parameters[6]),
|
||||||
} # type: Any
|
} # type: Any
|
||||||
|
|
||||||
url = 'https://api.baremetrics.com/v1/{}/plans'.format(parameters[0])
|
url = "https://api.baremetrics.com/v1/{}/plans".format(parameters[0])
|
||||||
create_plan_response = requests.post(url, data=data_header, headers=self.auth_header)
|
create_plan_response = requests.post(url, data=data_header, headers=self.auth_header)
|
||||||
if 'error' not in create_plan_response.json():
|
if "error" not in create_plan_response.json():
|
||||||
return 'Plan Created.'
|
return "Plan Created."
|
||||||
else:
|
else:
|
||||||
return 'Invalid Arguments Error.'
|
return "Invalid Arguments Error."
|
||||||
|
|
||||||
|
|
||||||
handler_class = BaremetricsHandler
|
handler_class = BaremetricsHandler
|
||||||
|
|
|
@ -8,121 +8,121 @@ class TestBaremetricsBot(BotTestCase, DefaultTests):
|
||||||
bot_name = "baremetrics"
|
bot_name = "baremetrics"
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
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')
|
self.verify_reply("", "No Command Specified")
|
||||||
|
|
||||||
def test_help_query(self) -> None:
|
def test_help_query(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(
|
self.verify_reply(
|
||||||
'help',
|
"help",
|
||||||
'''
|
"""
|
||||||
This bot gives updates about customer behavior, financial performance, and analytics
|
This bot gives updates about customer behavior, financial performance, and analytics
|
||||||
for an organization using the Baremetrics Api.\n
|
for an organization using the Baremetrics Api.\n
|
||||||
Enter `list-commands` to show the list of available commands.
|
Enter `list-commands` to show the list of available commands.
|
||||||
Version 1.0
|
Version 1.0
|
||||||
''',
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_list_commands_command(self) -> None:
|
def test_list_commands_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(
|
self.verify_reply(
|
||||||
'list-commands',
|
"list-commands",
|
||||||
'**Available Commands:** \n'
|
"**Available Commands:** \n"
|
||||||
' - help : Display bot info\n'
|
" - help : Display bot info\n"
|
||||||
' - list-commands : Display the list of available commands\n'
|
" - list-commands : Display the list of available commands\n"
|
||||||
' - account-info : Display the account info\n'
|
" - account-info : Display the account info\n"
|
||||||
' - list-sources : List the sources\n'
|
" - list-sources : List the sources\n"
|
||||||
' - list-plans <source_id> : List the plans for the source\n'
|
" - list-plans <source_id> : List the plans for the source\n"
|
||||||
' - list-customers <source_id> : List the customers in the source\n'
|
" - list-customers <source_id> : List the customers in the source\n"
|
||||||
' - list-subscriptions <source_id> : List the subscriptions in the '
|
" - list-subscriptions <source_id> : List the subscriptions in the "
|
||||||
'source\n'
|
"source\n"
|
||||||
' - create-plan <source_id> <oid> <name> <currency> <amount> <interval> '
|
" - create-plan <source_id> <oid> <name> <currency> <amount> <interval> "
|
||||||
'<interval_count> : Create a plan in the given source\n',
|
"<interval_count> : Create a plan in the given source\n",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_account_info_command(self) -> None:
|
def test_account_info_command(self) -> None:
|
||||||
with self.mock_config_info({'api_key': 'TEST'}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('account_info'):
|
with self.mock_http_conversation("account_info"):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'account-info',
|
"account-info",
|
||||||
'**Your account information:**\nId: 376418\nCompany: NA\nDefault '
|
"**Your account information:**\nId: 376418\nCompany: NA\nDefault "
|
||||||
'Currency: United States Dollar',
|
"Currency: United States Dollar",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_list_sources_command(self) -> None:
|
def test_list_sources_command(self) -> None:
|
||||||
with self.mock_config_info({'api_key': 'TEST'}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('list_sources'):
|
with self.mock_http_conversation("list_sources"):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'list-sources',
|
"list-sources",
|
||||||
'**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: '
|
"**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: "
|
||||||
'baremetrics\nProvider ID: None\n\n',
|
"baremetrics\nProvider ID: None\n\n",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_list_plans_command(self) -> None:
|
def test_list_plans_command(self) -> None:
|
||||||
r = (
|
r = (
|
||||||
'**Listing plans:**\n1.Name: Plan 1\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n'
|
"**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\n2.Name: Plan 2\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n"
|
||||||
' - 450000 USD\n'
|
" - 450000 USD\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.mock_config_info({'api_key': 'TEST'}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('list_plans'):
|
with self.mock_http_conversation("list_plans"):
|
||||||
self.verify_reply('list-plans TEST', r)
|
self.verify_reply("list-plans TEST", r)
|
||||||
|
|
||||||
def test_list_customers_command(self) -> None:
|
def test_list_customers_command(self) -> None:
|
||||||
r = (
|
r = (
|
||||||
'**Listing customers:**\n1.Name: Customer 1\nDisplay Name: Customer 1\nOID: customer_1\nActive: True\n'
|
"**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'
|
"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_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('list_customers'):
|
with self.mock_http_conversation("list_customers"):
|
||||||
self.verify_reply('list-customers TEST', r)
|
self.verify_reply("list-customers TEST", r)
|
||||||
|
|
||||||
def test_list_subscriptions_command(self) -> None:
|
def test_list_subscriptions_command(self) -> None:
|
||||||
r = (
|
r = (
|
||||||
'**Listing subscriptions:**\n1.Customer Name: Customer 1\nCustomer Display Name: Customer 1\n'
|
"**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'
|
"Customer OID: customer_1\nCustomer Email: customer_1@baremetrics.com\nActive: True\n"
|
||||||
'Plan Name: Plan 1\nPlan Amounts:\n - 1000 $\n'
|
"Plan Name: Plan 1\nPlan Amounts:\n - 1000 $\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.mock_config_info({'api_key': 'TEST'}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('list_subscriptions'):
|
with self.mock_http_conversation("list_subscriptions"):
|
||||||
self.verify_reply('list-subscriptions TEST', r)
|
self.verify_reply("list-subscriptions TEST", r)
|
||||||
|
|
||||||
def test_exception_when_api_key_is_invalid(self) -> None:
|
def test_exception_when_api_key_is_invalid(self) -> None:
|
||||||
bot_test_instance = BaremetricsHandler()
|
bot_test_instance = BaremetricsHandler()
|
||||||
|
|
||||||
with self.mock_config_info({'api_key': 'TEST'}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('invalid_api_key'):
|
with self.mock_http_conversation("invalid_api_key"):
|
||||||
with self.assertRaises(StubBotHandler.BotQuitException):
|
with self.assertRaises(StubBotHandler.BotQuitException):
|
||||||
bot_test_instance.initialize(StubBotHandler())
|
bot_test_instance.initialize(StubBotHandler())
|
||||||
|
|
||||||
def test_invalid_command(self) -> None:
|
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.')
|
self.verify_reply("abcd", "Invalid Command.")
|
||||||
|
|
||||||
def test_missing_params(self) -> None:
|
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.')
|
self.verify_reply("list-plans", "Missing Params.")
|
||||||
|
|
||||||
def test_key_error(self) -> None:
|
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'):
|
with self.mock_http_conversation("test_key_error"):
|
||||||
self.verify_reply('list-plans TEST', 'Invalid Response From API.')
|
self.verify_reply("list-plans TEST", "Invalid Response From API.")
|
||||||
|
|
||||||
def test_create_plan_command(self) -> None:
|
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'):
|
with self.mock_http_conversation("create_plan"):
|
||||||
self.verify_reply('create-plan TEST 1 TEST USD 123 TEST 123', 'Plan Created.')
|
self.verify_reply("create-plan TEST 1 TEST USD 123 TEST 123", "Plan Created.")
|
||||||
|
|
||||||
def test_create_plan_error_command(self) -> None:
|
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'):
|
with self.mock_http_conversation("create_plan_error"):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'create-plan TEST 1 TEST USD 123 TEST 123', 'Invalid Arguments Error.'
|
"create-plan TEST 1 TEST USD 123 TEST 123", "Invalid Arguments Error."
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_plan_argnum_error_command(self) -> None:
|
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.')
|
self.verify_reply("create-plan alpha beta", "Invalid number of arguments.")
|
||||||
|
|
|
@ -6,7 +6,7 @@ from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
from zulip_bots.lib import BotHandler
|
from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
help_message = '''
|
help_message = """
|
||||||
You can add datapoints towards your beeminder goals \
|
You can add datapoints towards your beeminder goals \
|
||||||
following the syntax shown below :smile:.\n \
|
following the syntax shown below :smile:.\n \
|
||||||
\n**@mention-botname daystamp, value, comment**\
|
\n**@mention-botname daystamp, value, comment**\
|
||||||
|
@ -14,22 +14,22 @@ following the syntax shown below :smile:.\n \
|
||||||
[**NOTE:** Optional field, default is *current daystamp*],\
|
[**NOTE:** Optional field, default is *current daystamp*],\
|
||||||
\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
|
\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
|
||||||
\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
|
\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_beeminder_response(message_content: str, config_info: Dict[str, str]) -> str:
|
def get_beeminder_response(message_content: str, config_info: Dict[str, str]) -> str:
|
||||||
username = config_info['username']
|
username = config_info["username"]
|
||||||
goalname = config_info['goalname']
|
goalname = config_info["goalname"]
|
||||||
auth_token = config_info['auth_token']
|
auth_token = config_info["auth_token"]
|
||||||
|
|
||||||
message_content = message_content.strip()
|
message_content = message_content.strip()
|
||||||
if message_content == '' or message_content == 'help':
|
if message_content == "" or message_content == "help":
|
||||||
return help_message
|
return help_message
|
||||||
|
|
||||||
url = "https://www.beeminder.com/api/v1/users/{}/goals/{}/datapoints.json".format(
|
url = "https://www.beeminder.com/api/v1/users/{}/goals/{}/datapoints.json".format(
|
||||||
username, goalname
|
username, goalname
|
||||||
)
|
)
|
||||||
message_pieces = message_content.split(',')
|
message_pieces = message_content.split(",")
|
||||||
for i in range(len(message_pieces)):
|
for i in range(len(message_pieces)):
|
||||||
message_pieces[i] = message_pieces[i].strip()
|
message_pieces[i] = message_pieces[i].strip()
|
||||||
|
|
||||||
|
@ -81,21 +81,21 @@ right now.\nPlease try again later"
|
||||||
|
|
||||||
|
|
||||||
class BeeminderHandler:
|
class BeeminderHandler:
|
||||||
'''
|
"""
|
||||||
This plugin allows users to easily add datapoints
|
This plugin allows users to easily add datapoints
|
||||||
towards their beeminder goals via zulip
|
towards their beeminder goals via zulip
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('beeminder')
|
self.config_info = bot_handler.get_config_info("beeminder")
|
||||||
# Check for valid auth_token
|
# Check for valid auth_token
|
||||||
auth_token = self.config_info['auth_token']
|
auth_token = self.config_info["auth_token"]
|
||||||
try:
|
try:
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
"https://www.beeminder.com/api/v1/users/me.json", params={'auth_token': auth_token}
|
"https://www.beeminder.com/api/v1/users/me.json", params={"auth_token": auth_token}
|
||||||
)
|
)
|
||||||
if r.status_code == 401:
|
if r.status_code == 401:
|
||||||
bot_handler.quit('Invalid key!')
|
bot_handler.quit("Invalid key!")
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
logging.exception(str(e))
|
logging.exception(str(e))
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ class BeeminderHandler:
|
||||||
return "This plugin allows users to add datapoints towards their Beeminder goals"
|
return "This plugin allows users to add datapoints towards their Beeminder goals"
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
response = get_beeminder_response(message['content'], self.config_info)
|
response = get_beeminder_response(message["content"], self.config_info)
|
||||||
bot_handler.send_reply(message, response)
|
bot_handler.send_reply(message, response)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ class TestBeeminderBot(BotTestCase, DefaultTests):
|
||||||
bot_name = "beeminder"
|
bot_name = "beeminder"
|
||||||
normal_config = {"auth_token": "XXXXXX", "username": "aaron", "goalname": "goal"}
|
normal_config = {"auth_token": "XXXXXX", "username": "aaron", "goalname": "goal"}
|
||||||
|
|
||||||
help_message = '''
|
help_message = """
|
||||||
You can add datapoints towards your beeminder goals \
|
You can add datapoints towards your beeminder goals \
|
||||||
following the syntax shown below :smile:.\n \
|
following the syntax shown below :smile:.\n \
|
||||||
\n**@mention-botname daystamp, value, comment**\
|
\n**@mention-botname daystamp, value, comment**\
|
||||||
|
@ -17,44 +17,44 @@ following the syntax shown below :smile:.\n \
|
||||||
[**NOTE:** Optional field, default is *current daystamp*],\
|
[**NOTE:** Optional field, default is *current daystamp*],\
|
||||||
\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
|
\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
|
||||||
\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
|
\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
):
|
):
|
||||||
self.verify_reply('', self.help_message)
|
self.verify_reply("", self.help_message)
|
||||||
|
|
||||||
def test_help_message(self) -> None:
|
def test_help_message(self) -> None:
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
):
|
):
|
||||||
self.verify_reply('help', self.help_message)
|
self.verify_reply("help", self.help_message)
|
||||||
|
|
||||||
def test_message_with_daystamp_and_value(self) -> None:
|
def test_message_with_daystamp_and_value(self) -> None:
|
||||||
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
|
bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created."
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
), self.mock_http_conversation('test_message_with_daystamp_and_value'):
|
), self.mock_http_conversation("test_message_with_daystamp_and_value"):
|
||||||
self.verify_reply('20180602, 2', bot_response)
|
self.verify_reply("20180602, 2", bot_response)
|
||||||
|
|
||||||
def test_message_with_value_and_comment(self) -> None:
|
def test_message_with_value_and_comment(self) -> None:
|
||||||
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
|
bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created."
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
), self.mock_http_conversation('test_message_with_value_and_comment'):
|
), self.mock_http_conversation("test_message_with_value_and_comment"):
|
||||||
self.verify_reply('2, hi there!', bot_response)
|
self.verify_reply("2, hi there!", bot_response)
|
||||||
|
|
||||||
def test_message_with_daystamp_and_value_and_comment(self) -> None:
|
def test_message_with_daystamp_and_value_and_comment(self) -> None:
|
||||||
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
|
bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created."
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
), self.mock_http_conversation('test_message_with_daystamp_and_value_and_comment'):
|
), self.mock_http_conversation("test_message_with_daystamp_and_value_and_comment"):
|
||||||
self.verify_reply('20180602, 2, hi there!', bot_response)
|
self.verify_reply("20180602, 2, hi there!", bot_response)
|
||||||
|
|
||||||
def test_syntax_error(self) -> None:
|
def test_syntax_error(self) -> None:
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
):
|
):
|
||||||
bot_response = "Make sure you follow the syntax.\n You can take a look \
|
bot_response = "Make sure you follow the syntax.\n You can take a look \
|
||||||
at syntax by: @mention-botname help"
|
at syntax by: @mention-botname help"
|
||||||
|
@ -62,12 +62,12 @@ at syntax by: @mention-botname help"
|
||||||
|
|
||||||
def test_connection_error_when_handle_message(self) -> None:
|
def test_connection_error_when_handle_message(self) -> None:
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
), patch('requests.post', side_effect=ConnectionError()), patch('logging.exception'):
|
), patch("requests.post", side_effect=ConnectionError()), patch("logging.exception"):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'?$!',
|
"?$!",
|
||||||
'Uh-Oh, couldn\'t process the request \
|
"Uh-Oh, couldn't process the request \
|
||||||
right now.\nPlease try again later',
|
right now.\nPlease try again later",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_when_handle_message(self) -> None:
|
def test_invalid_when_handle_message(self) -> None:
|
||||||
|
@ -75,20 +75,20 @@ right now.\nPlease try again later',
|
||||||
StubBotHandler()
|
StubBotHandler()
|
||||||
|
|
||||||
with self.mock_config_info(
|
with self.mock_config_info(
|
||||||
{'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'}
|
{"auth_token": "someInvalidKey", "username": "aaron", "goalname": "goal"}
|
||||||
), patch('requests.get', side_effect=ConnectionError()), self.mock_http_conversation(
|
), patch("requests.get", side_effect=ConnectionError()), self.mock_http_conversation(
|
||||||
'test_invalid_when_handle_message'
|
"test_invalid_when_handle_message"
|
||||||
), patch(
|
), patch(
|
||||||
'logging.exception'
|
"logging.exception"
|
||||||
):
|
):
|
||||||
self.verify_reply('5', 'Error. Check your key!')
|
self.verify_reply("5", "Error. Check your key!")
|
||||||
|
|
||||||
def test_error(self) -> None:
|
def test_error(self) -> None:
|
||||||
bot_request = 'notNumber'
|
bot_request = "notNumber"
|
||||||
bot_response = "Error occured : 422"
|
bot_response = "Error occured : 422"
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_valid_auth_token'
|
"test_valid_auth_token"
|
||||||
), self.mock_http_conversation('test_error'):
|
), self.mock_http_conversation("test_error"):
|
||||||
self.verify_reply(bot_request, bot_response)
|
self.verify_reply(bot_request, bot_response)
|
||||||
|
|
||||||
def test_invalid_when_initialize(self) -> None:
|
def test_invalid_when_initialize(self) -> None:
|
||||||
|
@ -96,8 +96,8 @@ right now.\nPlease try again later',
|
||||||
bot_handler = StubBotHandler()
|
bot_handler = StubBotHandler()
|
||||||
|
|
||||||
with self.mock_config_info(
|
with self.mock_config_info(
|
||||||
{'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'}
|
{"auth_token": "someInvalidKey", "username": "aaron", "goalname": "goal"}
|
||||||
), self.mock_http_conversation('test_invalid_when_initialize'), self.assertRaises(
|
), self.mock_http_conversation("test_invalid_when_initialize"), self.assertRaises(
|
||||||
bot_handler.BotQuitException
|
bot_handler.BotQuitException
|
||||||
):
|
):
|
||||||
bot.initialize(bot_handler)
|
bot.initialize(bot_handler)
|
||||||
|
@ -107,7 +107,7 @@ right now.\nPlease try again later',
|
||||||
bot_handler = StubBotHandler()
|
bot_handler = StubBotHandler()
|
||||||
|
|
||||||
with self.mock_config_info(self.normal_config), patch(
|
with self.mock_config_info(self.normal_config), patch(
|
||||||
'requests.get', side_effect=ConnectionError()
|
"requests.get", side_effect=ConnectionError()
|
||||||
), patch('logging.exception') as mock_logging:
|
), patch("logging.exception") as mock_logging:
|
||||||
bot.initialize(bot_handler)
|
bot.initialize(bot_handler)
|
||||||
self.assertTrue(mock_logging.called)
|
self.assertTrue(mock_logging.called)
|
||||||
|
|
|
@ -7,38 +7,38 @@ import chess.uci
|
||||||
|
|
||||||
from zulip_bots.lib import BotHandler
|
from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
START_REGEX = re.compile('start with other user$')
|
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>.+)$')
|
MOVE_REGEX = re.compile("do (?P<move_san>.+)$")
|
||||||
RESIGN_REGEX = re.compile('resign$')
|
RESIGN_REGEX = re.compile("resign$")
|
||||||
|
|
||||||
|
|
||||||
class ChessHandler:
|
class ChessHandler:
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return (
|
return (
|
||||||
'Chess Bot is a bot that allows you to play chess against either '
|
"Chess Bot is a bot that allows you to play chess against either "
|
||||||
'another user or the computer. Use `start with other user` or '
|
"another user or the computer. Use `start with other user` or "
|
||||||
'`start as <color> with computer` to start a game.\n\n'
|
"`start as <color> with computer` to start a game.\n\n"
|
||||||
'In order to play against a computer, `chess.conf` must be set '
|
"In order to play against a computer, `chess.conf` must be set "
|
||||||
'with the key `stockfish_location` set to the location of the '
|
"with the key `stockfish_location` set to the location of the "
|
||||||
'Stockfish program on this computer.'
|
"Stockfish program on this computer."
|
||||||
)
|
)
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('chess')
|
self.config_info = bot_handler.get_config_info("chess")
|
||||||
|
|
||||||
try:
|
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()
|
self.engine.uci()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# It is helpful to allow for fake Stockfish locations if the bot
|
# It is helpful to allow for fake Stockfish locations if the bot
|
||||||
# runner is testing or knows they won't be using an engine.
|
# runner is testing or knows they won't be using an engine.
|
||||||
print('That Stockfish doesn\'t exist. Continuing.')
|
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']
|
content = message["content"]
|
||||||
|
|
||||||
if content == '':
|
if content == "":
|
||||||
bot_handler.send_reply(message, self.usage())
|
bot_handler.send_reply(message, self.usage())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -50,29 +50,29 @@ class ChessHandler:
|
||||||
is_with_computer = False
|
is_with_computer = False
|
||||||
last_fen = chess.Board().fen()
|
last_fen = chess.Board().fen()
|
||||||
|
|
||||||
if bot_handler.storage.contains('is_with_computer'):
|
if bot_handler.storage.contains("is_with_computer"):
|
||||||
is_with_computer = (
|
is_with_computer = (
|
||||||
# `bot_handler`'s `storage` only accepts `str` values.
|
# `bot_handler`'s `storage` only accepts `str` values.
|
||||||
bot_handler.storage.get('is_with_computer')
|
bot_handler.storage.get("is_with_computer")
|
||||||
== str(True)
|
== str(True)
|
||||||
)
|
)
|
||||||
|
|
||||||
if bot_handler.storage.contains('last_fen'):
|
if bot_handler.storage.contains("last_fen"):
|
||||||
last_fen = bot_handler.storage.get('last_fen')
|
last_fen = bot_handler.storage.get("last_fen")
|
||||||
|
|
||||||
if start_regex_match:
|
if start_regex_match:
|
||||||
self.start(message, bot_handler)
|
self.start(message, bot_handler)
|
||||||
elif start_computer_regex_match:
|
elif start_computer_regex_match:
|
||||||
self.start_computer(
|
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:
|
elif move_regex_match:
|
||||||
if is_with_computer:
|
if is_with_computer:
|
||||||
self.move_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:
|
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:
|
elif resign_regex_match:
|
||||||
self.resign(message, bot_handler, last_fen)
|
self.resign(message, bot_handler, last_fen)
|
||||||
|
|
||||||
|
@ -88,9 +88,9 @@ class ChessHandler:
|
||||||
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`'s `storage` only accepts `str` values.
|
||||||
bot_handler.storage.put('is_with_computer', str(False))
|
bot_handler.storage.put("is_with_computer", str(False))
|
||||||
|
|
||||||
bot_handler.storage.put('last_fen', new_board.fen())
|
bot_handler.storage.put("last_fen", new_board.fen())
|
||||||
|
|
||||||
def start_computer(
|
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
|
||||||
|
@ -112,9 +112,9 @@ class ChessHandler:
|
||||||
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`'s `storage` only accepts `str` values.
|
||||||
bot_handler.storage.put('is_with_computer', str(True))
|
bot_handler.storage.put("is_with_computer", str(True))
|
||||||
|
|
||||||
bot_handler.storage.put('last_fen', new_board.fen())
|
bot_handler.storage.put("last_fen", new_board.fen())
|
||||||
else:
|
else:
|
||||||
self.move_computer_first(
|
self.move_computer_first(
|
||||||
message,
|
message,
|
||||||
|
@ -204,18 +204,18 @@ class ChessHandler:
|
||||||
# wants the game to be a draw, after 3 or 75 it a draw. For now,
|
# wants the game to be a draw, after 3 or 75 it a draw. For now,
|
||||||
# just assume that the players would want the draw.
|
# just assume that the players would want the draw.
|
||||||
if new_board.is_game_over(True):
|
if new_board.is_game_over(True):
|
||||||
game_over_output = ''
|
game_over_output = ""
|
||||||
|
|
||||||
if new_board.is_checkmate():
|
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():
|
elif new_board.is_stalemate():
|
||||||
game_over_output = make_draw_response('stalemate')
|
game_over_output = make_draw_response("stalemate")
|
||||||
elif new_board.is_insufficient_material():
|
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():
|
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():
|
elif new_board.can_claim_threefold_repetition():
|
||||||
game_over_output = make_draw_response('3-fold 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)
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ class ChessHandler:
|
||||||
|
|
||||||
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())
|
bot_handler.storage.put("last_fen", new_board.fen())
|
||||||
|
|
||||||
def move_computer(
|
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
|
||||||
|
@ -299,7 +299,7 @@ class ChessHandler:
|
||||||
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(
|
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
|
||||||
|
@ -329,10 +329,10 @@ class ChessHandler:
|
||||||
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`'s `storage` only accepts `str` values.
|
||||||
bot_handler.storage.put('is_with_computer', str(True))
|
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.
|
"""Resigns the game for the current player.
|
||||||
|
@ -347,7 +347,7 @@ class ChessHandler:
|
||||||
if not last_board:
|
if not last_board:
|
||||||
return
|
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
|
handler_class = ChessHandler
|
||||||
|
@ -376,7 +376,7 @@ def make_draw_response(reason: str) -> str:
|
||||||
|
|
||||||
Returns: The draw response string.
|
Returns: The draw response string.
|
||||||
"""
|
"""
|
||||||
return 'It\'s a draw because of {}!'.format(reason)
|
return "It's a draw because of {}!".format(reason)
|
||||||
|
|
||||||
|
|
||||||
def make_loss_response(board: chess.Board, reason: str) -> str:
|
def make_loss_response(board: chess.Board, reason: str) -> str:
|
||||||
|
@ -389,10 +389,10 @@ def make_loss_response(board: chess.Board, reason: str) -> str:
|
||||||
|
|
||||||
Returns: The loss response string.
|
Returns: The loss response string.
|
||||||
"""
|
"""
|
||||||
return ('*{}* {}. **{}** wins!\n\n' '{}').format(
|
return ("*{}* {}. **{}** wins!\n\n" "{}").format(
|
||||||
'White' if board.turn else 'Black',
|
"White" if board.turn else "Black",
|
||||||
reason,
|
reason,
|
||||||
'Black' if board.turn else 'White',
|
"Black" if board.turn else "White",
|
||||||
make_str(board, board.turn),
|
make_str(board, board.turn),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -406,7 +406,7 @@ def make_not_legal_response(board: chess.Board, move_san: str) -> str:
|
||||||
|
|
||||||
Returns: The not-legal-move response string.
|
Returns: The not-legal-move response string.
|
||||||
"""
|
"""
|
||||||
return ('Sorry, the move *{}* isn\'t legal.\n\n' '{}' '\n\n\n' '{}').format(
|
return ("Sorry, the move *{}* isn't legal.\n\n" "{}" "\n\n\n" "{}").format(
|
||||||
move_san, make_str(board, board.turn), make_footer()
|
move_san, make_str(board, board.turn), make_footer()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -417,8 +417,8 @@ def make_copied_wrong_response() -> str:
|
||||||
Returns: The copied-wrong response string.
|
Returns: The copied-wrong response string.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
'Sorry, it seems like you copied down the response wrong.\n\n'
|
"Sorry, it seems like you copied down the response wrong.\n\n"
|
||||||
'Please try to copy the response again from the last message!'
|
"Please try to copy the response again from the last message!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -433,13 +433,13 @@ def make_start_reponse(board: chess.Board) -> str:
|
||||||
Returns: The starting response string.
|
Returns: The starting response string.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
'New game! The board looks like this:\n\n'
|
"New game! The board looks like this:\n\n"
|
||||||
'{}'
|
"{}"
|
||||||
'\n\n\n'
|
"\n\n\n"
|
||||||
'Now it\'s **{}**\'s turn.'
|
"Now it's **{}**'s turn."
|
||||||
'\n\n\n'
|
"\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:
|
def make_start_computer_reponse(board: chess.Board) -> str:
|
||||||
|
@ -454,13 +454,13 @@ def make_start_computer_reponse(board: chess.Board) -> str:
|
||||||
Returns: The starting response string.
|
Returns: The starting response string.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
'New game with computer! The board looks like this:\n\n'
|
"New game with computer! The board looks like this:\n\n"
|
||||||
'{}'
|
"{}"
|
||||||
'\n\n\n'
|
"\n\n\n"
|
||||||
'Now it\'s **{}**\'s turn.'
|
"Now it's **{}**'s turn."
|
||||||
'\n\n\n'
|
"\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:
|
||||||
|
@ -474,21 +474,21 @@ def make_move_reponse(last_board: chess.Board, new_board: chess.Board, move: che
|
||||||
Returns: The move response string.
|
Returns: The move response string.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
'The board was like this:\n\n'
|
"The board was like this:\n\n"
|
||||||
'{}'
|
"{}"
|
||||||
'\n\n\n'
|
"\n\n\n"
|
||||||
'Then *{}* moved *{}*:\n\n'
|
"Then *{}* moved *{}*:\n\n"
|
||||||
'{}'
|
"{}"
|
||||||
'\n\n\n'
|
"\n\n\n"
|
||||||
'Now it\'s **{}**\'s turn.'
|
"Now it's **{}**'s turn."
|
||||||
'\n\n\n'
|
"\n\n\n"
|
||||||
'{}'
|
"{}"
|
||||||
).format(
|
).format(
|
||||||
make_str(last_board, new_board.turn),
|
make_str(last_board, new_board.turn),
|
||||||
'white' if last_board.turn else 'black',
|
"white" if last_board.turn else "black",
|
||||||
last_board.san(move),
|
last_board.san(move),
|
||||||
make_str(new_board, new_board.turn),
|
make_str(new_board, new_board.turn),
|
||||||
'white' if new_board.turn else 'black',
|
"white" if new_board.turn else "black",
|
||||||
make_footer(),
|
make_footer(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -498,10 +498,10 @@ def make_footer() -> str:
|
||||||
responses.
|
responses.
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
'To make your next move, respond to Chess Bot with\n\n'
|
"To make your next move, respond to Chess Bot with\n\n"
|
||||||
'```do <your move>```\n\n'
|
"```do <your move>```\n\n"
|
||||||
'*Remember to @-mention Chess Bot at the beginning of your '
|
"*Remember to @-mention Chess Bot at the beginning of your "
|
||||||
'response.*'
|
"response.*"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -525,7 +525,7 @@ def make_str(board: chess.Board, is_white_on_bottom: bool) -> 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)
|
trimmed_str = trim_whitespace_before_newline(properly_flipped_str)
|
||||||
monospaced_str = '```\n{}\n```'.format(trimmed_str)
|
monospaced_str = "```\n{}\n```".format(trimmed_str)
|
||||||
|
|
||||||
return monospaced_str
|
return monospaced_str
|
||||||
|
|
||||||
|
@ -542,11 +542,11 @@ def guide_with_numbers(board_str: str) -> str:
|
||||||
# Spaces and newlines would mess up the loop because they add extra indexes
|
# Spaces and newlines would mess up the loop because they add extra indexes
|
||||||
# between pieces. Newlines are added later by the loop and spaces are added
|
# between pieces. Newlines are added later by the loop and spaces are added
|
||||||
# back in at the end.
|
# back in at the end.
|
||||||
board_without_whitespace_str = board_str.replace(' ', '').replace('\n', '')
|
board_without_whitespace_str = board_str.replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
# The first number, 8, needs to be added first because it comes before a
|
# The first number, 8, needs to be added first because it comes before a
|
||||||
# newline. From then on, numbers are inserted at newlines.
|
# newline. From then on, numbers are inserted at newlines.
|
||||||
row_list = list('8' + board_without_whitespace_str)
|
row_list = list("8" + board_without_whitespace_str)
|
||||||
|
|
||||||
for i, char in enumerate(row_list):
|
for i, char in enumerate(row_list):
|
||||||
# `(i + 1) % 10 == 0` if it is the end of a row, i.e., the 10th column
|
# `(i + 1) % 10 == 0` if it is the end of a row, i.e., the 10th column
|
||||||
|
@ -563,14 +563,14 @@ def guide_with_numbers(board_str: str) -> str:
|
||||||
# the newline isn't counted by the loop. If they were split into 3,
|
# the newline isn't counted by the loop. If they were split into 3,
|
||||||
# or combined into just 1 string, the counter would become off
|
# or combined into just 1 string, the counter would become off
|
||||||
# because it would be counting what is really 2 rows as 3 or 1.
|
# because it would be counting what is really 2 rows as 3 or 1.
|
||||||
row_list[i:i] = [str(row_num) + '\n', str(row_num - 1)]
|
row_list[i:i] = [str(row_num) + "\n", str(row_num - 1)]
|
||||||
|
|
||||||
# 1 is appended to the end because it isn't created in the loop, and lines
|
# 1 is appended to the end because it isn't created in the loop, and lines
|
||||||
# that begin with spaces have their spaces removed for aesthetics.
|
# that begin with spaces have their spaces removed for aesthetics.
|
||||||
row_str = (' '.join(row_list) + ' 1').replace('\n ', '\n')
|
row_str = (" ".join(row_list) + " 1").replace("\n ", "\n")
|
||||||
|
|
||||||
# a, b, c, d, e, f, g, and h are easy to add in.
|
# 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
|
return row_and_col_str
|
||||||
|
|
||||||
|
@ -586,21 +586,21 @@ def replace_with_unicode(board_str: str) -> str:
|
||||||
"""
|
"""
|
||||||
replaced_str = board_str
|
replaced_str = board_str
|
||||||
|
|
||||||
replaced_str = replaced_str.replace('P', '♙')
|
replaced_str = replaced_str.replace("P", "♙")
|
||||||
replaced_str = replaced_str.replace('N', '♘')
|
replaced_str = replaced_str.replace("N", "♘")
|
||||||
replaced_str = replaced_str.replace('B', '♗')
|
replaced_str = replaced_str.replace("B", "♗")
|
||||||
replaced_str = replaced_str.replace('R', '♖')
|
replaced_str = replaced_str.replace("R", "♖")
|
||||||
replaced_str = replaced_str.replace('Q', '♕')
|
replaced_str = replaced_str.replace("Q", "♕")
|
||||||
replaced_str = replaced_str.replace('K', '♔')
|
replaced_str = replaced_str.replace("K", "♔")
|
||||||
|
|
||||||
replaced_str = replaced_str.replace('p', '♟')
|
replaced_str = replaced_str.replace("p", "♟")
|
||||||
replaced_str = replaced_str.replace('n', '♞')
|
replaced_str = replaced_str.replace("n", "♞")
|
||||||
replaced_str = replaced_str.replace('b', '♝')
|
replaced_str = replaced_str.replace("b", "♝")
|
||||||
replaced_str = replaced_str.replace('r', '♜')
|
replaced_str = replaced_str.replace("r", "♜")
|
||||||
replaced_str = replaced_str.replace('q', '♛')
|
replaced_str = replaced_str.replace("q", "♛")
|
||||||
replaced_str = replaced_str.replace('k', '♚')
|
replaced_str = replaced_str.replace("k", "♚")
|
||||||
|
|
||||||
replaced_str = replaced_str.replace('.', '·')
|
replaced_str = replaced_str.replace(".", "·")
|
||||||
|
|
||||||
return replaced_str
|
return replaced_str
|
||||||
|
|
||||||
|
@ -613,4 +613,4 @@ def trim_whitespace_before_newline(str_to_trim: str) -> str:
|
||||||
|
|
||||||
Returns: The trimmed string.
|
Returns: The trimmed string.
|
||||||
"""
|
"""
|
||||||
return re.sub(r'\s+$', '', str_to_trim, flags=re.M)
|
return re.sub(r"\s+$", "", str_to_trim, flags=re.M)
|
||||||
|
|
|
@ -4,7 +4,7 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
||||||
class TestChessBot(BotTestCase, DefaultTests):
|
class TestChessBot(BotTestCase, DefaultTests):
|
||||||
bot_name = "chessbot"
|
bot_name = "chessbot"
|
||||||
|
|
||||||
START_RESPONSE = '''New game! The board looks like this:
|
START_RESPONSE = """New game! The board looks like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
a b c d e f g h
|
a b c d e f g h
|
||||||
|
@ -27,9 +27,9 @@ To make your next move, respond to Chess Bot with
|
||||||
|
|
||||||
```do <your move>```
|
```do <your move>```
|
||||||
|
|
||||||
*Remember to @-mention Chess Bot at the beginning of your response.*'''
|
*Remember to @-mention Chess Bot at the beginning of your response.*"""
|
||||||
|
|
||||||
DO_E4_RESPONSE = '''The board was like this:
|
DO_E4_RESPONSE = """The board was like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
h g f e d c b a
|
h g f e d c b a
|
||||||
|
@ -68,9 +68,9 @@ To make your next move, respond to Chess Bot with
|
||||||
|
|
||||||
```do <your move>```
|
```do <your move>```
|
||||||
|
|
||||||
*Remember to @-mention Chess Bot at the beginning of your response.*'''
|
*Remember to @-mention Chess Bot at the beginning of your response.*"""
|
||||||
|
|
||||||
DO_KE4_RESPONSE = '''Sorry, the move *Ke4* isn't legal.
|
DO_KE4_RESPONSE = """Sorry, the move *Ke4* isn't legal.
|
||||||
|
|
||||||
```
|
```
|
||||||
h g f e d c b a
|
h g f e d c b a
|
||||||
|
@ -90,9 +90,9 @@ To make your next move, respond to Chess Bot with
|
||||||
|
|
||||||
```do <your move>```
|
```do <your move>```
|
||||||
|
|
||||||
*Remember to @-mention Chess Bot at the beginning of your response.*'''
|
*Remember to @-mention Chess Bot at the beginning of your response.*"""
|
||||||
|
|
||||||
RESIGN_RESPONSE = '''*Black* resigned. **White** wins!
|
RESIGN_RESPONSE = """*Black* resigned. **White** wins!
|
||||||
|
|
||||||
```
|
```
|
||||||
h g f e d c b a
|
h g f e d c b a
|
||||||
|
@ -105,20 +105,20 @@ To make your next move, respond to Chess Bot with
|
||||||
7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 7
|
7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 7
|
||||||
8 ♜ ♞ ♝ ♚ ♛ ♝ ♞ ♜ 8
|
8 ♜ ♞ ♝ ♚ ♛ ♝ ♞ ♜ 8
|
||||||
h g f e d c b a
|
h g f e d c b a
|
||||||
```'''
|
```"""
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
with self.mock_config_info({'stockfish_location': '/foo/bar'}):
|
with self.mock_config_info({"stockfish_location": "/foo/bar"}):
|
||||||
response = self.get_response(dict(content=''))
|
response = self.get_response(dict(content=""))
|
||||||
self.assertIn('play chess', response['content'])
|
self.assertIn("play chess", response["content"])
|
||||||
|
|
||||||
def test_main(self) -> None:
|
def test_main(self) -> None:
|
||||||
with self.mock_config_info({'stockfish_location': '/foo/bar'}):
|
with self.mock_config_info({"stockfish_location": "/foo/bar"}):
|
||||||
self.verify_dialog(
|
self.verify_dialog(
|
||||||
[
|
[
|
||||||
('start with other user', self.START_RESPONSE),
|
("start with other user", self.START_RESPONSE),
|
||||||
('do e4', self.DO_E4_RESPONSE),
|
("do e4", self.DO_E4_RESPONSE),
|
||||||
('do Ke4', self.DO_KE4_RESPONSE),
|
("do Ke4", self.DO_KE4_RESPONSE),
|
||||||
('resign', self.RESIGN_RESPONSE),
|
("resign", self.RESIGN_RESPONSE),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,21 +5,21 @@ from zulip_bots.game_handler import GameAdapter
|
||||||
|
|
||||||
|
|
||||||
class ConnectFourMessageHandler:
|
class ConnectFourMessageHandler:
|
||||||
tokens = [':blue_circle:', ':red_circle:']
|
tokens = [":blue_circle:", ":red_circle:"]
|
||||||
|
|
||||||
def parse_board(self, board: Any) -> str:
|
def parse_board(self, board: Any) -> str:
|
||||||
# Header for the top of the board
|
# Header for the top of the board
|
||||||
board_str = ':one: :two: :three: :four: :five: :six: :seven:'
|
board_str = ":one: :two: :three: :four: :five: :six: :seven:"
|
||||||
|
|
||||||
for row in range(0, 6):
|
for row in range(0, 6):
|
||||||
board_str += '\n\n'
|
board_str += "\n\n"
|
||||||
for column in range(0, 7):
|
for column in range(0, 7):
|
||||||
if board[row][column] == 0:
|
if board[row][column] == 0:
|
||||||
board_str += ':white_circle: '
|
board_str += ":white_circle: "
|
||||||
elif board[row][column] == 1:
|
elif board[row][column] == 1:
|
||||||
board_str += self.tokens[0] + ' '
|
board_str += self.tokens[0] + " "
|
||||||
elif board[row][column] == -1:
|
elif board[row][column] == -1:
|
||||||
board_str += self.tokens[1] + ' '
|
board_str += self.tokens[1] + " "
|
||||||
|
|
||||||
return board_str
|
return board_str
|
||||||
|
|
||||||
|
@ -27,33 +27,33 @@ class ConnectFourMessageHandler:
|
||||||
return self.tokens[turn]
|
return self.tokens[turn]
|
||||||
|
|
||||||
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
||||||
column_number = move_info.replace('move ', '')
|
column_number = move_info.replace("move ", "")
|
||||||
return original_player + ' moved in column ' + column_number
|
return original_player + " moved in column " + column_number
|
||||||
|
|
||||||
def game_start_message(self) -> str:
|
def game_start_message(self) -> str:
|
||||||
return 'Type `move <column-number>` or `<column-number>` to place a token.\n\
|
return "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!'
|
The first player to get 4 in a row wins!\n Good Luck!"
|
||||||
|
|
||||||
|
|
||||||
class ConnectFourBotHandler(GameAdapter):
|
class ConnectFourBotHandler(GameAdapter):
|
||||||
'''
|
"""
|
||||||
Bot that uses the Game Adapter class
|
Bot that uses the Game Adapter class
|
||||||
to allow users to play other users
|
to allow users to play other users
|
||||||
or the comptuer in a game of Connect
|
or the comptuer in a game of Connect
|
||||||
Four
|
Four
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
game_name = 'Connect Four'
|
game_name = "Connect Four"
|
||||||
bot_name = 'connect_four'
|
bot_name = "connect_four"
|
||||||
move_help_message = (
|
move_help_message = (
|
||||||
'* To make your move during a game, type\n'
|
"* To make your move during a game, type\n"
|
||||||
'```move <column-number>``` or ```<column-number>```'
|
"```move <column-number>``` or ```<column-number>```"
|
||||||
)
|
)
|
||||||
move_regex = '(move ([1-7])$)|(([1-7])$)'
|
move_regex = "(move ([1-7])$)|(([1-7])$)"
|
||||||
model = ConnectFourModel
|
model = ConnectFourModel
|
||||||
gameMessageHandler = ConnectFourMessageHandler
|
gameMessageHandler = ConnectFourMessageHandler
|
||||||
rules = '''Try to get four pieces in row, Diagonals count too!'''
|
rules = """Try to get four pieces in row, Diagonals count too!"""
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
game_name,
|
game_name,
|
||||||
|
|
|
@ -5,10 +5,10 @@ from zulip_bots.game_handler import BadMoveException
|
||||||
|
|
||||||
|
|
||||||
class ConnectFourModel:
|
class ConnectFourModel:
|
||||||
'''
|
"""
|
||||||
Object that manages running the Connect
|
Object that manages running the Connect
|
||||||
Four logic for the Connect Four Bot
|
Four logic for the Connect Four Bot
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.blank_board = [
|
self.blank_board = [
|
||||||
|
@ -54,11 +54,11 @@ class ConnectFourModel:
|
||||||
token_number = 1
|
token_number = 1
|
||||||
finding_move = True
|
finding_move = True
|
||||||
row = 5
|
row = 5
|
||||||
column = int(move.replace('move ', '')) - 1
|
column = int(move.replace("move ", "")) - 1
|
||||||
|
|
||||||
while finding_move:
|
while finding_move:
|
||||||
if row < 0:
|
if row < 0:
|
||||||
raise BadMoveException('Make sure your move is in a column with free space.')
|
raise BadMoveException("Make sure your move is in a column with free space.")
|
||||||
if self.current_board[row][column] == 0:
|
if self.current_board[row][column] == 0:
|
||||||
self.current_board[row][column] = token_number
|
self.current_board[row][column] = token_number
|
||||||
finding_move = False
|
finding_move = False
|
||||||
|
@ -143,7 +143,7 @@ class ConnectFourModel:
|
||||||
top_row_multiple = reduce(lambda x, y: x * y, self.current_board[0])
|
top_row_multiple = reduce(lambda x, y: x * y, self.current_board[0])
|
||||||
|
|
||||||
if top_row_multiple != 0:
|
if top_row_multiple != 0:
|
||||||
return 'draw'
|
return "draw"
|
||||||
|
|
||||||
winner = (
|
winner = (
|
||||||
get_horizontal_wins(self.current_board)
|
get_horizontal_wins(self.current_board)
|
||||||
|
@ -156,4 +156,4 @@ class ConnectFourModel:
|
||||||
elif winner == -1:
|
elif winner == -1:
|
||||||
return second_player
|
return second_player
|
||||||
|
|
||||||
return ''
|
return ""
|
||||||
|
|
|
@ -6,10 +6,10 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
||||||
|
|
||||||
|
|
||||||
class TestConnectFourBot(BotTestCase, DefaultTests):
|
class TestConnectFourBot(BotTestCase, DefaultTests):
|
||||||
bot_name = 'connect_four'
|
bot_name = "connect_four"
|
||||||
|
|
||||||
def make_request_message(
|
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]:
|
) -> 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
|
return message
|
||||||
|
@ -20,14 +20,14 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
||||||
request: str,
|
request: str,
|
||||||
expected_response: str,
|
expected_response: str,
|
||||||
response_number: int,
|
response_number: int,
|
||||||
user: str = 'foo@example.com',
|
user: str = "foo@example.com",
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
"""
|
||||||
This function serves a similar purpose
|
This function serves a similar purpose
|
||||||
to BotTestCase.verify_dialog, but allows
|
to BotTestCase.verify_dialog, but allows
|
||||||
for multiple responses to be validated,
|
for multiple responses to be validated,
|
||||||
and for mocking of the bot's internal data
|
and for mocking of the bot's internal data
|
||||||
'''
|
"""
|
||||||
|
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
message = self.make_request_message(request, user)
|
message = self.make_request_message(request, user)
|
||||||
|
@ -38,10 +38,10 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
||||||
responses = [message for (method, message) in bot_handler.transcript]
|
responses = [message for (method, message) in bot_handler.transcript]
|
||||||
|
|
||||||
first_response = responses[response_number]
|
first_response = responses[response_number]
|
||||||
self.assertEqual(expected_response, first_response['content'])
|
self.assertEqual(expected_response, first_response["content"])
|
||||||
|
|
||||||
def help_message(self) -> str:
|
def help_message(self) -> str:
|
||||||
return '''** Connect Four Bot Help:**
|
return """** Connect Four Bot Help:**
|
||||||
*Preface all commands with @**test-bot***
|
*Preface all commands with @**test-bot***
|
||||||
* To start a game in a stream (*recommended*), type
|
* To start a game in a stream (*recommended*), type
|
||||||
`start game`
|
`start game`
|
||||||
|
@ -62,15 +62,15 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
||||||
* To see rules of this game, type
|
* To see rules of this game, type
|
||||||
`rules`
|
`rules`
|
||||||
* To make your move during a game, type
|
* To make your move during a game, type
|
||||||
```move <column-number>``` or ```<column-number>```'''
|
```move <column-number>``` or ```<column-number>```"""
|
||||||
|
|
||||||
def test_static_responses(self) -> None:
|
def test_static_responses(self) -> None:
|
||||||
self.verify_response('help', self.help_message(), 0)
|
self.verify_response("help", self.help_message(), 0)
|
||||||
|
|
||||||
def test_game_message_handler_responses(self) -> None:
|
def test_game_message_handler_responses(self) -> None:
|
||||||
board = (
|
board = (
|
||||||
':one: :two: :three: :four: :five: :six: :seven:\n\n'
|
":one: :two: :three: :four: :five: :six: :seven:\n\n"
|
||||||
+ '\
|
+ "\
|
||||||
:white_circle: :white_circle: :white_circle: :white_circle: \
|
:white_circle: :white_circle: :white_circle: :white_circle: \
|
||||||
:white_circle: :white_circle: :white_circle: \n\n\
|
:white_circle: :white_circle: :white_circle: \n\n\
|
||||||
:white_circle: :white_circle: :white_circle: :white_circle: \
|
:white_circle: :white_circle: :white_circle: :white_circle: \
|
||||||
|
@ -82,18 +82,18 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
||||||
:blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \
|
:blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \
|
||||||
:white_circle: :white_circle: \n\n\
|
:white_circle: :white_circle: \n\n\
|
||||||
:blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \
|
:blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \
|
||||||
:white_circle: :white_circle: '
|
:white_circle: :white_circle: "
|
||||||
)
|
)
|
||||||
bot, bot_handler = self._get_handlers()
|
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(
|
self.assertEqual(
|
||||||
bot.gameMessageHandler.alert_move_message('foo', 'move 6'), 'foo moved in column 6'
|
bot.gameMessageHandler.alert_move_message("foo", "move 6"), "foo moved in column 6"
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bot.gameMessageHandler.game_start_message(),
|
bot.gameMessageHandler.game_start_message(),
|
||||||
'Type `move <column-number>` or `<column-number>` to place a token.\n\
|
"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!',
|
The first player to get 4 in a row wins!\n Good Luck!",
|
||||||
)
|
)
|
||||||
|
|
||||||
blank_board = [
|
blank_board = [
|
||||||
|
@ -142,22 +142,22 @@ The first player to get 4 in a row wins!\n Good Luck!',
|
||||||
final_board: List[List[int]],
|
final_board: List[List[int]],
|
||||||
) -> None:
|
) -> None:
|
||||||
connectFourModel.update_board(initial_board)
|
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)
|
self.assertEqual(test_board, final_board)
|
||||||
|
|
||||||
def confirmGameOver(board: List[List[int]], result: str) -> None:
|
def confirmGameOver(board: List[List[int]], result: str) -> None:
|
||||||
connectFourModel.update_board(board)
|
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)
|
self.assertEqual(game_over, result)
|
||||||
|
|
||||||
def confirmWinStates(array: List[List[List[List[int]]]]) -> None:
|
def confirmWinStates(array: List[List[List[List[int]]]]) -> None:
|
||||||
for board in array[0]:
|
for board in array[0]:
|
||||||
confirmGameOver(board, 'first_player')
|
confirmGameOver(board, "first_player")
|
||||||
|
|
||||||
for board in array[1]:
|
for board in array[1]:
|
||||||
confirmGameOver(board, 'second_player')
|
confirmGameOver(board, "second_player")
|
||||||
|
|
||||||
connectFourModel = ConnectFourModel()
|
connectFourModel = ConnectFourModel()
|
||||||
|
|
||||||
|
@ -553,8 +553,8 @@ The first player to get 4 in a row wins!\n Good Luck!',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test Game Over Logic:
|
# Test Game Over Logic:
|
||||||
confirmGameOver(blank_board, '')
|
confirmGameOver(blank_board, "")
|
||||||
confirmGameOver(full_board, 'draw')
|
confirmGameOver(full_board, "draw")
|
||||||
|
|
||||||
# Test Win States:
|
# Test Win States:
|
||||||
confirmWinStates(horizontal_win_boards)
|
confirmWinStates(horizontal_win_boards)
|
||||||
|
@ -564,7 +564,7 @@ The first player to get 4 in a row wins!\n Good Luck!',
|
||||||
|
|
||||||
def test_more_logic(self) -> None:
|
def test_more_logic(self) -> None:
|
||||||
model = ConnectFourModel()
|
model = ConnectFourModel()
|
||||||
move = 'move 4'
|
move = "move 4"
|
||||||
col = 3 # zero-indexed
|
col = 3 # zero-indexed
|
||||||
|
|
||||||
self.assertEqual(model.get_column(col), [0, 0, 0, 0, 0, 0])
|
self.assertEqual(model.get_column(col), [0, 0, 0, 0, 0, 0])
|
||||||
|
|
|
@ -28,17 +28,17 @@ def round_to(x: float, digits: int) -> float:
|
||||||
|
|
||||||
|
|
||||||
class ConverterHandler:
|
class ConverterHandler:
|
||||||
'''
|
"""
|
||||||
This plugin allows users to make conversions between various units,
|
This plugin allows users to make conversions between various units,
|
||||||
e.g. Celsius to Fahrenheit, or kilobytes to gigabytes.
|
e.g. Celsius to Fahrenheit, or kilobytes to gigabytes.
|
||||||
It looks for messages of the format
|
It looks for messages of the format
|
||||||
'@mention-bot <number> <unit_from> <unit_to>'
|
'@mention-bot <number> <unit_from> <unit_to>'
|
||||||
The message '@mention-bot help' posts a short description of how to use
|
The message '@mention-bot help' posts a short description of how to use
|
||||||
the plugin, along with a list of all supported units.
|
the plugin, along with a list of all supported units.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
This plugin allows users to make conversions between
|
This plugin allows users to make conversions between
|
||||||
various units, e.g. Celsius to Fahrenheit,
|
various units, e.g. Celsius to Fahrenheit,
|
||||||
or kilobytes to gigabytes. It looks for messages of
|
or kilobytes to gigabytes. It looks for messages of
|
||||||
|
@ -46,7 +46,7 @@ class ConverterHandler:
|
||||||
The message '@mention-bot help' posts a short description of
|
The message '@mention-bot help' posts a short description of
|
||||||
how to use the plugin, along with a list of
|
how to use the plugin, along with a list of
|
||||||
all supported units.
|
all supported units.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
bot_response = get_bot_converter_response(message, bot_handler)
|
bot_response = get_bot_converter_response(message, bot_handler)
|
||||||
|
@ -54,7 +54,7 @@ class ConverterHandler:
|
||||||
|
|
||||||
|
|
||||||
def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) -> str:
|
def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) -> str:
|
||||||
content = message['content']
|
content = message["content"]
|
||||||
|
|
||||||
words = content.lower().split()
|
words = content.lower().split()
|
||||||
convert_indexes = [i for i, word in enumerate(words) if word == "@convert"]
|
convert_indexes = [i for i, word in enumerate(words) if word == "@convert"]
|
||||||
|
@ -62,7 +62,7 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for convert_index in convert_indexes:
|
for convert_index in convert_indexes:
|
||||||
if (convert_index + 1) < len(words) and words[convert_index + 1] == 'help':
|
if (convert_index + 1) < len(words) and words[convert_index + 1] == "help":
|
||||||
results.append(utils.HELP_MESSAGE)
|
results.append(utils.HELP_MESSAGE)
|
||||||
continue
|
continue
|
||||||
if (convert_index + 3) < len(words):
|
if (convert_index + 3) < len(words):
|
||||||
|
@ -72,7 +72,7 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
|
||||||
exponent = 0
|
exponent = 0
|
||||||
|
|
||||||
if not is_float(number):
|
if not is_float(number):
|
||||||
results.append('`' + number + '` is not a valid number. ' + utils.QUICK_HELP)
|
results.append("`" + number + "` is not a valid number. " + utils.QUICK_HELP)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# cannot reassign "number" as a float after using as string, so changed name
|
# cannot reassign "number" as a float after using as string, so changed name
|
||||||
|
@ -91,22 +91,22 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
|
||||||
ut_to_std = utils.UNITS.get(unit_to, []) # type: List[Any]
|
ut_to_std = utils.UNITS.get(unit_to, []) # type: List[Any]
|
||||||
|
|
||||||
if not uf_to_std:
|
if not uf_to_std:
|
||||||
results.append('`' + unit_from + '` is not a valid unit. ' + utils.QUICK_HELP)
|
results.append("`" + unit_from + "` is not a valid unit. " + utils.QUICK_HELP)
|
||||||
if not ut_to_std:
|
if not ut_to_std:
|
||||||
results.append('`' + unit_to + '` is not a valid unit.' + utils.QUICK_HELP)
|
results.append("`" + unit_to + "` is not a valid unit." + utils.QUICK_HELP)
|
||||||
if not uf_to_std or not ut_to_std:
|
if not uf_to_std or not ut_to_std:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
base_unit = uf_to_std[2]
|
base_unit = uf_to_std[2]
|
||||||
if uf_to_std[2] != ut_to_std[2]:
|
if uf_to_std[2] != ut_to_std[2]:
|
||||||
unit_from = unit_from.capitalize() if uf_to_std[2] == 'kelvin' else unit_from
|
unit_from = unit_from.capitalize() if uf_to_std[2] == "kelvin" else unit_from
|
||||||
results.append(
|
results.append(
|
||||||
'`'
|
"`"
|
||||||
+ unit_to.capitalize()
|
+ unit_to.capitalize()
|
||||||
+ '` and `'
|
+ "` and `"
|
||||||
+ unit_from
|
+ unit_from
|
||||||
+ '`'
|
+ "`"
|
||||||
+ ' are not from the same category. '
|
+ " are not from the same category. "
|
||||||
+ utils.QUICK_HELP
|
+ utils.QUICK_HELP
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
@ -117,24 +117,24 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
|
||||||
number_res -= ut_to_std[0]
|
number_res -= ut_to_std[0]
|
||||||
number_res /= ut_to_std[1]
|
number_res /= ut_to_std[1]
|
||||||
|
|
||||||
if base_unit == 'bit':
|
if base_unit == "bit":
|
||||||
number_res *= 1024 ** (exponent // 3)
|
number_res *= 1024 ** (exponent // 3)
|
||||||
else:
|
else:
|
||||||
number_res *= 10 ** exponent
|
number_res *= 10 ** exponent
|
||||||
number_res = round_to(number_res, 7)
|
number_res = round_to(number_res, 7)
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
'{} {} = {} {}'.format(
|
"{} {} = {} {}".format(
|
||||||
number, words[convert_index + 2], number_res, words[convert_index + 3]
|
number, words[convert_index + 2], number_res, words[convert_index + 3]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
results.append('Too few arguments given. ' + utils.QUICK_HELP)
|
results.append("Too few arguments given. " + utils.QUICK_HELP)
|
||||||
|
|
||||||
new_content = ''
|
new_content = ""
|
||||||
for idx, result in enumerate(results, 1):
|
for idx, result in enumerate(results, 1):
|
||||||
new_content += ((str(idx) + '. conversion: ') if len(results) > 1 else '') + result + '\n'
|
new_content += ((str(idx) + ". conversion: ") if len(results) > 1 else "") + result + "\n"
|
||||||
|
|
||||||
return new_content
|
return new_content
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,13 @@ class TestConverterBot(BotTestCase, DefaultTests):
|
||||||
dialog = [
|
dialog = [
|
||||||
(
|
(
|
||||||
"",
|
"",
|
||||||
'Too few arguments given. Enter `@convert help` '
|
"Too few arguments given. Enter `@convert help` "
|
||||||
'for help on using the converter.\n',
|
"for help on using the converter.\n",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"foo bar",
|
"foo bar",
|
||||||
'Too few arguments given. Enter `@convert help` '
|
"Too few arguments given. Enter `@convert help` "
|
||||||
'for help on using the converter.\n',
|
"for help on using the converter.\n",
|
||||||
),
|
),
|
||||||
("2 m cm", "2 m = 200.0 cm\n"),
|
("2 m cm", "2 m = 200.0 cm\n"),
|
||||||
("12.0 celsius fahrenheit", "12.0 celsius = 53.600054 fahrenheit\n"),
|
("12.0 celsius fahrenheit", "12.0 celsius = 53.600054 fahrenheit\n"),
|
||||||
|
|
|
@ -3,152 +3,152 @@
|
||||||
# factor that need to be added and multiplied to convert the unit into
|
# factor that need to be added and multiplied to convert the unit into
|
||||||
# the base unit in the last parameter.
|
# the base unit in the last parameter.
|
||||||
UNITS = {
|
UNITS = {
|
||||||
'bit': [0, 1, 'bit'],
|
"bit": [0, 1, "bit"],
|
||||||
'byte': [0, 8, 'bit'],
|
"byte": [0, 8, "bit"],
|
||||||
'cubic-centimeter': [0, 0.000001, 'cubic-meter'],
|
"cubic-centimeter": [0, 0.000001, "cubic-meter"],
|
||||||
'cubic-decimeter': [0, 0.001, 'cubic-meter'],
|
"cubic-decimeter": [0, 0.001, "cubic-meter"],
|
||||||
'liter': [0, 0.001, 'cubic-meter'],
|
"liter": [0, 0.001, "cubic-meter"],
|
||||||
'cubic-meter': [0, 1, 'cubic-meter'],
|
"cubic-meter": [0, 1, "cubic-meter"],
|
||||||
'cubic-inch': [0, 0.000016387064, 'cubic-meter'],
|
"cubic-inch": [0, 0.000016387064, "cubic-meter"],
|
||||||
'fluid-ounce': [0, 0.000029574, 'cubic-meter'],
|
"fluid-ounce": [0, 0.000029574, "cubic-meter"],
|
||||||
'cubic-foot': [0, 0.028316846592, 'cubic-meter'],
|
"cubic-foot": [0, 0.028316846592, "cubic-meter"],
|
||||||
'cubic-yard': [0, 0.764554857984, 'cubic-meter'],
|
"cubic-yard": [0, 0.764554857984, "cubic-meter"],
|
||||||
'teaspoon': [0, 0.0000049289216, 'cubic-meter'],
|
"teaspoon": [0, 0.0000049289216, "cubic-meter"],
|
||||||
'tablespoon': [0, 0.000014787, 'cubic-meter'],
|
"tablespoon": [0, 0.000014787, "cubic-meter"],
|
||||||
'cup': [0, 0.00023658823648491, 'cubic-meter'],
|
"cup": [0, 0.00023658823648491, "cubic-meter"],
|
||||||
'gram': [0, 1, 'gram'],
|
"gram": [0, 1, "gram"],
|
||||||
'kilogram': [0, 1000, 'gram'],
|
"kilogram": [0, 1000, "gram"],
|
||||||
'ton': [0, 1000000, 'gram'],
|
"ton": [0, 1000000, "gram"],
|
||||||
'ounce': [0, 28.349523125, 'gram'],
|
"ounce": [0, 28.349523125, "gram"],
|
||||||
'pound': [0, 453.59237, 'gram'],
|
"pound": [0, 453.59237, "gram"],
|
||||||
'kelvin': [0, 1, 'kelvin'],
|
"kelvin": [0, 1, "kelvin"],
|
||||||
'celsius': [273.15, 1, 'kelvin'],
|
"celsius": [273.15, 1, "kelvin"],
|
||||||
'fahrenheit': [255.372222, 0.555555, 'kelvin'],
|
"fahrenheit": [255.372222, 0.555555, "kelvin"],
|
||||||
'centimeter': [0, 0.01, 'meter'],
|
"centimeter": [0, 0.01, "meter"],
|
||||||
'decimeter': [0, 0.1, 'meter'],
|
"decimeter": [0, 0.1, "meter"],
|
||||||
'meter': [0, 1, 'meter'],
|
"meter": [0, 1, "meter"],
|
||||||
'kilometer': [0, 1000, 'meter'],
|
"kilometer": [0, 1000, "meter"],
|
||||||
'inch': [0, 0.0254, 'meter'],
|
"inch": [0, 0.0254, "meter"],
|
||||||
'foot': [0, 0.3048, 'meter'],
|
"foot": [0, 0.3048, "meter"],
|
||||||
'yard': [0, 0.9144, 'meter'],
|
"yard": [0, 0.9144, "meter"],
|
||||||
'mile': [0, 1609.344, 'meter'],
|
"mile": [0, 1609.344, "meter"],
|
||||||
'nautical-mile': [0, 1852, 'meter'],
|
"nautical-mile": [0, 1852, "meter"],
|
||||||
'square-centimeter': [0, 0.0001, 'square-meter'],
|
"square-centimeter": [0, 0.0001, "square-meter"],
|
||||||
'square-decimeter': [0, 0.01, 'square-meter'],
|
"square-decimeter": [0, 0.01, "square-meter"],
|
||||||
'square-meter': [0, 1, 'square-meter'],
|
"square-meter": [0, 1, "square-meter"],
|
||||||
'square-kilometer': [0, 1000000, 'square-meter'],
|
"square-kilometer": [0, 1000000, "square-meter"],
|
||||||
'square-inch': [0, 0.00064516, 'square-meter'],
|
"square-inch": [0, 0.00064516, "square-meter"],
|
||||||
'square-foot': [0, 0.09290304, 'square-meter'],
|
"square-foot": [0, 0.09290304, "square-meter"],
|
||||||
'square-yard': [0, 0.83612736, 'square-meter'],
|
"square-yard": [0, 0.83612736, "square-meter"],
|
||||||
'square-mile': [0, 2589988.110336, 'square-meter'],
|
"square-mile": [0, 2589988.110336, "square-meter"],
|
||||||
'are': [0, 100, 'square-meter'],
|
"are": [0, 100, "square-meter"],
|
||||||
'hectare': [0, 10000, 'square-meter'],
|
"hectare": [0, 10000, "square-meter"],
|
||||||
'acre': [0, 4046.8564224, 'square-meter'],
|
"acre": [0, 4046.8564224, "square-meter"],
|
||||||
}
|
}
|
||||||
|
|
||||||
PREFIXES = {
|
PREFIXES = {
|
||||||
'atto': -18,
|
"atto": -18,
|
||||||
'femto': -15,
|
"femto": -15,
|
||||||
'pico': -12,
|
"pico": -12,
|
||||||
'nano': -9,
|
"nano": -9,
|
||||||
'micro': -6,
|
"micro": -6,
|
||||||
'milli': -3,
|
"milli": -3,
|
||||||
'centi': -2,
|
"centi": -2,
|
||||||
'deci': -1,
|
"deci": -1,
|
||||||
'deca': 1,
|
"deca": 1,
|
||||||
'hecto': 2,
|
"hecto": 2,
|
||||||
'kilo': 3,
|
"kilo": 3,
|
||||||
'mega': 6,
|
"mega": 6,
|
||||||
'giga': 9,
|
"giga": 9,
|
||||||
'tera': 12,
|
"tera": 12,
|
||||||
'peta': 15,
|
"peta": 15,
|
||||||
'exa': 18,
|
"exa": 18,
|
||||||
}
|
}
|
||||||
|
|
||||||
ALIASES = {
|
ALIASES = {
|
||||||
'a': 'are',
|
"a": "are",
|
||||||
'ac': 'acre',
|
"ac": "acre",
|
||||||
'c': 'celsius',
|
"c": "celsius",
|
||||||
'cm': 'centimeter',
|
"cm": "centimeter",
|
||||||
'cm2': 'square-centimeter',
|
"cm2": "square-centimeter",
|
||||||
'cm3': 'cubic-centimeter',
|
"cm3": "cubic-centimeter",
|
||||||
'cm^2': 'square-centimeter',
|
"cm^2": "square-centimeter",
|
||||||
'cm^3': 'cubic-centimeter',
|
"cm^3": "cubic-centimeter",
|
||||||
'dm': 'decimeter',
|
"dm": "decimeter",
|
||||||
'dm2': 'square-decimeter',
|
"dm2": "square-decimeter",
|
||||||
'dm3': 'cubic-decimeter',
|
"dm3": "cubic-decimeter",
|
||||||
'dm^2': 'square-decimeter',
|
"dm^2": "square-decimeter",
|
||||||
'dm^3': 'cubic-decimeter',
|
"dm^3": "cubic-decimeter",
|
||||||
'f': 'fahrenheit',
|
"f": "fahrenheit",
|
||||||
'fl-oz': 'fluid-ounce',
|
"fl-oz": "fluid-ounce",
|
||||||
'ft': 'foot',
|
"ft": "foot",
|
||||||
'ft2': 'square-foot',
|
"ft2": "square-foot",
|
||||||
'ft3': 'cubic-foot',
|
"ft3": "cubic-foot",
|
||||||
'ft^2': 'square-foot',
|
"ft^2": "square-foot",
|
||||||
'ft^3': 'cubic-foot',
|
"ft^3": "cubic-foot",
|
||||||
'g': 'gram',
|
"g": "gram",
|
||||||
'ha': 'hectare',
|
"ha": "hectare",
|
||||||
'in': 'inch',
|
"in": "inch",
|
||||||
'in2': 'square-inch',
|
"in2": "square-inch",
|
||||||
'in3': 'cubic-inch',
|
"in3": "cubic-inch",
|
||||||
'in^2': 'square-inch',
|
"in^2": "square-inch",
|
||||||
'in^3': 'cubic-inch',
|
"in^3": "cubic-inch",
|
||||||
'k': 'kelvin',
|
"k": "kelvin",
|
||||||
'kg': 'kilogram',
|
"kg": "kilogram",
|
||||||
'km': 'kilometer',
|
"km": "kilometer",
|
||||||
'km2': 'square-kilometer',
|
"km2": "square-kilometer",
|
||||||
'km^2': 'square-kilometer',
|
"km^2": "square-kilometer",
|
||||||
'l': 'liter',
|
"l": "liter",
|
||||||
'lb': 'pound',
|
"lb": "pound",
|
||||||
'm': 'meter',
|
"m": "meter",
|
||||||
'm2': 'square-meter',
|
"m2": "square-meter",
|
||||||
'm3': 'cubic-meter',
|
"m3": "cubic-meter",
|
||||||
'm^2': 'square-meter',
|
"m^2": "square-meter",
|
||||||
'm^3': 'cubic-meter',
|
"m^3": "cubic-meter",
|
||||||
'mi': 'mile',
|
"mi": "mile",
|
||||||
'mi2': 'square-mile',
|
"mi2": "square-mile",
|
||||||
'mi^2': 'square-mile',
|
"mi^2": "square-mile",
|
||||||
'nmi': 'nautical-mile',
|
"nmi": "nautical-mile",
|
||||||
'oz': 'ounce',
|
"oz": "ounce",
|
||||||
't': 'ton',
|
"t": "ton",
|
||||||
'tbsp': 'tablespoon',
|
"tbsp": "tablespoon",
|
||||||
'tsp': 'teaspoon',
|
"tsp": "teaspoon",
|
||||||
'y': 'yard',
|
"y": "yard",
|
||||||
'y2': 'square-yard',
|
"y2": "square-yard",
|
||||||
'y3': 'cubic-yard',
|
"y3": "cubic-yard",
|
||||||
'y^2': 'square-yard',
|
"y^2": "square-yard",
|
||||||
'y^3': 'cubic-yard',
|
"y^3": "cubic-yard",
|
||||||
}
|
}
|
||||||
|
|
||||||
HELP_MESSAGE = (
|
HELP_MESSAGE = (
|
||||||
'Converter usage:\n'
|
"Converter usage:\n"
|
||||||
'`@convert <number> <unit_from> <unit_to>`\n'
|
"`@convert <number> <unit_from> <unit_to>`\n"
|
||||||
'Converts `number` in the unit <unit_from> to '
|
"Converts `number` in the unit <unit_from> to "
|
||||||
'the <unit_to> and prints the result\n'
|
"the <unit_to> and prints the result\n"
|
||||||
'`number`: integer or floating point number, e.g. 12, 13.05, 0.002\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'
|
"<unit_from> and <unit_to> are two of the following units:\n"
|
||||||
'* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), '
|
"* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), "
|
||||||
'square-meter (m^2, m2), square-kilometer (km^2, km2),'
|
"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-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'
|
" square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n"
|
||||||
'* bit, byte\n'
|
"* bit, byte\n"
|
||||||
'* centimeter (cm), decimeter(dm), meter (m),'
|
"* centimeter (cm), decimeter(dm), meter (m),"
|
||||||
' kilometer (km), inch (in), foot (ft), yard (y),'
|
" kilometer (km), inch (in), foot (ft), yard (y),"
|
||||||
' mile (mi), nautical-mile (nmi)\n'
|
" mile (mi), nautical-mile (nmi)\n"
|
||||||
'* Kelvin (K), Celsius(C), Fahrenheit (F)\n'
|
"* Kelvin (K), Celsius(C), Fahrenheit (F)\n"
|
||||||
'* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), '
|
"* 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-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), "
|
||||||
'cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n'
|
"cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n"
|
||||||
'* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\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'
|
"* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n"
|
||||||
'Allowed prefixes are:\n'
|
"Allowed prefixes are:\n"
|
||||||
'* atto, pico, femto, nano, micro, milli, centi, deci\n'
|
"* atto, pico, femto, nano, micro, milli, centi, deci\n"
|
||||||
'* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n'
|
"* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n"
|
||||||
'Usage examples:\n'
|
"Usage examples:\n"
|
||||||
'* `@convert 12 celsius fahrenheit`\n'
|
"* `@convert 12 celsius fahrenheit`\n"
|
||||||
'* `@convert 0.002 kilomile millimeter`\n'
|
"* `@convert 0.002 kilomile millimeter`\n"
|
||||||
'* `@convert 31.5 square-mile ha`\n'
|
"* `@convert 31.5 square-mile ha`\n"
|
||||||
'* `@convert 56 g lb`\n'
|
"* `@convert 56 g lb`\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
QUICK_HELP = 'Enter `@convert help` for help on using the converter.'
|
QUICK_HELP = "Enter `@convert help` for help on using the converter."
|
||||||
|
|
|
@ -10,31 +10,31 @@ from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
|
|
||||||
class DefineHandler:
|
class DefineHandler:
|
||||||
'''
|
"""
|
||||||
This plugin define a word that the user inputs. It
|
This plugin define a word that the user inputs. It
|
||||||
looks for messages starting with '@mention-bot'.
|
looks for messages starting with '@mention-bot'.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
DEFINITION_API_URL = 'https://owlbot.info/api/v2/dictionary/{}?format=json'
|
DEFINITION_API_URL = "https://owlbot.info/api/v2/dictionary/{}?format=json"
|
||||||
REQUEST_ERROR_MESSAGE = 'Could not load definition.'
|
REQUEST_ERROR_MESSAGE = "Could not load definition."
|
||||||
EMPTY_WORD_REQUEST_ERROR_MESSAGE = 'Please enter a word to define.'
|
EMPTY_WORD_REQUEST_ERROR_MESSAGE = "Please enter a word to define."
|
||||||
PHRASE_ERROR_MESSAGE = 'Definitions for phrases are not available.'
|
PHRASE_ERROR_MESSAGE = "Definitions for phrases are not available."
|
||||||
SYMBOLS_PRESENT_ERROR_MESSAGE = 'Definitions of words with symbols are not possible.'
|
SYMBOLS_PRESENT_ERROR_MESSAGE = "Definitions of words with symbols are not possible."
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
This plugin will allow users to define a word. Users should preface
|
This plugin will allow users to define a word. Users should preface
|
||||||
messages with @mention-bot.
|
messages with @mention-bot.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
original_content = message['content'].strip()
|
original_content = message["content"].strip()
|
||||||
bot_response = self.get_bot_define_response(original_content)
|
bot_response = self.get_bot_define_response(original_content)
|
||||||
|
|
||||||
bot_handler.send_reply(message, bot_response)
|
bot_handler.send_reply(message, bot_response)
|
||||||
|
|
||||||
def get_bot_define_response(self, original_content: str) -> str:
|
def get_bot_define_response(self, original_content: str) -> str:
|
||||||
split_content = original_content.split(' ')
|
split_content = original_content.split(" ")
|
||||||
# If there are more than one word (a phrase)
|
# If there are more than one word (a phrase)
|
||||||
if len(split_content) > 1:
|
if len(split_content) > 1:
|
||||||
return DefineHandler.PHRASE_ERROR_MESSAGE
|
return DefineHandler.PHRASE_ERROR_MESSAGE
|
||||||
|
@ -51,7 +51,7 @@ class DefineHandler:
|
||||||
if not to_define_lower:
|
if not to_define_lower:
|
||||||
return self.EMPTY_WORD_REQUEST_ERROR_MESSAGE
|
return self.EMPTY_WORD_REQUEST_ERROR_MESSAGE
|
||||||
else:
|
else:
|
||||||
response = '**{}**:\n'.format(to_define)
|
response = "**{}**:\n".format(to_define)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Use OwlBot API to fetch definition.
|
# Use OwlBot API to fetch definition.
|
||||||
|
@ -65,9 +65,9 @@ class DefineHandler:
|
||||||
else: # Definitions available.
|
else: # Definitions available.
|
||||||
# Show definitions line by line.
|
# Show definitions line by line.
|
||||||
for d in definitions:
|
for d in definitions:
|
||||||
example = d['example'] if d['example'] else '*No example available.*'
|
example = d["example"] if d["example"] else "*No example available.*"
|
||||||
response += '\n' + '* (**{}**) {}\n {}'.format(
|
response += "\n" + "* (**{}**) {}\n {}".format(
|
||||||
d['type'], d['definition'], html2text.html2text(example)
|
d["type"], d["definition"], html2text.html2text(example)
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -15,8 +15,8 @@ class TestDefineBot(BotTestCase, DefaultTests):
|
||||||
"kept as a pet or for catching mice, and many breeds have been "
|
"kept as a pet or for catching mice, and many breeds have been "
|
||||||
"developed.\n their pet cat\n\n"
|
"developed.\n their pet cat\n\n"
|
||||||
)
|
)
|
||||||
with self.mock_http_conversation('test_single_type_word'):
|
with self.mock_http_conversation("test_single_type_word"):
|
||||||
self.verify_reply('cat', bot_response)
|
self.verify_reply("cat", bot_response)
|
||||||
|
|
||||||
# Multi-type word.
|
# Multi-type word.
|
||||||
bot_response = (
|
bot_response = (
|
||||||
|
@ -32,26 +32,26 @@ class TestDefineBot(BotTestCase, DefaultTests):
|
||||||
"* (**exclamation**) used as an appeal for urgent assistance.\n"
|
"* (**exclamation**) used as an appeal for urgent assistance.\n"
|
||||||
" Help! I'm drowning!\n\n"
|
" Help! I'm drowning!\n\n"
|
||||||
)
|
)
|
||||||
with self.mock_http_conversation('test_multi_type_word'):
|
with self.mock_http_conversation("test_multi_type_word"):
|
||||||
self.verify_reply('help', bot_response)
|
self.verify_reply("help", bot_response)
|
||||||
|
|
||||||
# Incorrect word.
|
# Incorrect word.
|
||||||
bot_response = "**foo**:\nCould not load definition."
|
bot_response = "**foo**:\nCould not load definition."
|
||||||
with self.mock_http_conversation('test_incorrect_word'):
|
with self.mock_http_conversation("test_incorrect_word"):
|
||||||
self.verify_reply('foo', bot_response)
|
self.verify_reply("foo", bot_response)
|
||||||
|
|
||||||
# Phrases are not defined. No request is sent to the Internet.
|
# Phrases are not defined. No request is sent to the Internet.
|
||||||
bot_response = "Definitions for phrases are not available."
|
bot_response = "Definitions for phrases are not available."
|
||||||
self.verify_reply('The sky is blue', bot_response)
|
self.verify_reply("The sky is blue", bot_response)
|
||||||
|
|
||||||
# Symbols are considered invalid for words
|
# Symbols are considered invalid for words
|
||||||
bot_response = "Definitions of words with symbols are not possible."
|
bot_response = "Definitions of words with symbols are not possible."
|
||||||
self.verify_reply('#', bot_response)
|
self.verify_reply("#", bot_response)
|
||||||
|
|
||||||
# Empty messages are returned with a prompt to reply. No request is sent to the Internet.
|
# Empty messages are returned with a prompt to reply. No request is sent to the Internet.
|
||||||
bot_response = "Please enter a word to define."
|
bot_response = "Please enter a word to define."
|
||||||
self.verify_reply('', bot_response)
|
self.verify_reply("", bot_response)
|
||||||
|
|
||||||
def test_connection_error(self) -> None:
|
def test_connection_error(self) -> None:
|
||||||
with patch('requests.get', side_effect=Exception), patch('logging.exception'):
|
with patch("requests.get", side_effect=Exception), patch("logging.exception"):
|
||||||
self.verify_reply('aeroplane', '**aeroplane**:\nCould not load definition.')
|
self.verify_reply("aeroplane", "**aeroplane**:\nCould not load definition.")
|
||||||
|
|
|
@ -7,55 +7,55 @@ import apiai
|
||||||
|
|
||||||
from zulip_bots.lib import BotHandler
|
from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
help_message = '''DialogFlow bot
|
help_message = """DialogFlow bot
|
||||||
This bot will interact with dialogflow bots.
|
This bot will interact with dialogflow bots.
|
||||||
Simply send this bot a message, and it will respond depending on the configured bot's behaviour.
|
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:
|
def get_bot_result(message_content: str, config: Dict[str, str], sender_id: str) -> str:
|
||||||
if message_content.strip() == '' or message_content.strip() == 'help':
|
if message_content.strip() == "" or message_content.strip() == "help":
|
||||||
return config['bot_info']
|
return config["bot_info"]
|
||||||
ai = apiai.ApiAI(config['key'])
|
ai = apiai.ApiAI(config["key"])
|
||||||
try:
|
try:
|
||||||
request = ai.text_request()
|
request = ai.text_request()
|
||||||
request.session_id = sender_id
|
request.session_id = sender_id
|
||||||
request.query = message_content
|
request.query = message_content
|
||||||
response = request.getresponse()
|
response = request.getresponse()
|
||||||
res_str = response.read().decode('utf8', 'ignore')
|
res_str = response.read().decode("utf8", "ignore")
|
||||||
res_json = json.loads(res_str)
|
res_json = json.loads(res_str)
|
||||||
if res_json['status']['errorType'] != 'success' and 'result' not in res_json.keys():
|
if res_json["status"]["errorType"] != "success" and "result" not in res_json.keys():
|
||||||
return 'Error {}: {}.'.format(
|
return "Error {}: {}.".format(
|
||||||
res_json['status']['code'], res_json['status']['errorDetails']
|
res_json["status"]["code"], res_json["status"]["errorDetails"]
|
||||||
)
|
)
|
||||||
if res_json['result']['fulfillment']['speech'] == '':
|
if res_json["result"]["fulfillment"]["speech"] == "":
|
||||||
if 'alternateResult' in res_json.keys():
|
if "alternateResult" in res_json.keys():
|
||||||
if res_json['alternateResult']['fulfillment']['speech'] != '':
|
if res_json["alternateResult"]["fulfillment"]["speech"] != "":
|
||||||
return res_json['alternateResult']['fulfillment']['speech']
|
return res_json["alternateResult"]["fulfillment"]["speech"]
|
||||||
return 'Error. No result.'
|
return "Error. No result."
|
||||||
return res_json['result']['fulfillment']['speech']
|
return res_json["result"]["fulfillment"]["speech"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.exception(str(e))
|
logging.exception(str(e))
|
||||||
return 'Error. {}.'.format(str(e))
|
return "Error. {}.".format(str(e))
|
||||||
|
|
||||||
|
|
||||||
class DialogFlowHandler:
|
class DialogFlowHandler:
|
||||||
'''
|
"""
|
||||||
This plugin allows users to easily add their own
|
This plugin allows users to easily add their own
|
||||||
DialogFlow bots to zulip
|
DialogFlow bots to zulip
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('dialogflow')
|
self.config_info = bot_handler.get_config_info("dialogflow")
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
This plugin will allow users to easily add their own
|
This plugin will allow users to easily add their own
|
||||||
DialogFlow bots to zulip
|
DialogFlow bots to zulip
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
result = get_bot_result(message['content'], self.config_info, message['sender_id'])
|
result = get_bot_result(message["content"], self.config_info, message["sender_id"])
|
||||||
bot_handler.send_reply(message, result)
|
bot_handler.send_reply(message, result)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,13 @@ class MockTextRequest:
|
||||||
def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]:
|
def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]:
|
||||||
response_data = read_bot_fixture_data(bot_name, test_name)
|
response_data = read_bot_fixture_data(bot_name, test_name)
|
||||||
try:
|
try:
|
||||||
response_data['request']
|
response_data["request"]
|
||||||
df_response = response_data['response']
|
df_response = response_data["response"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("ERROR: 'request' or 'response' field not found in fixture.")
|
print("ERROR: 'request' or 'response' field not found in fixture.")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
with patch('apiai.ApiAI.text_request') as mock_text_request:
|
with patch("apiai.ApiAI.text_request") as mock_text_request:
|
||||||
request = MockTextRequest()
|
request = MockTextRequest()
|
||||||
request.response = df_response
|
request.response = df_response
|
||||||
mock_text_request.return_value = request
|
mock_text_request.return_value = request
|
||||||
|
@ -42,34 +42,34 @@ def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]:
|
||||||
|
|
||||||
|
|
||||||
class TestDialogFlowBot(BotTestCase, DefaultTests):
|
class TestDialogFlowBot(BotTestCase, DefaultTests):
|
||||||
bot_name = 'dialogflow'
|
bot_name = "dialogflow"
|
||||||
|
|
||||||
def _test(self, test_name: str, message: str, response: str) -> None:
|
def _test(self, test_name: str, message: str, response: str) -> None:
|
||||||
with self.mock_config_info(
|
with self.mock_config_info(
|
||||||
{'key': 'abcdefg', 'bot_info': 'bot info foo bar'}
|
{"key": "abcdefg", "bot_info": "bot info foo bar"}
|
||||||
), mock_dialogflow(test_name, 'dialogflow'):
|
), mock_dialogflow(test_name, "dialogflow"):
|
||||||
self.verify_reply(message, response)
|
self.verify_reply(message, response)
|
||||||
|
|
||||||
def test_normal(self) -> None:
|
def test_normal(self) -> None:
|
||||||
self._test('test_normal', 'hello', 'how are you?')
|
self._test("test_normal", "hello", "how are you?")
|
||||||
|
|
||||||
def test_403(self) -> None:
|
def test_403(self) -> None:
|
||||||
self._test('test_403', 'hello', 'Error 403: Access Denied.')
|
self._test("test_403", "hello", "Error 403: Access Denied.")
|
||||||
|
|
||||||
def test_empty_response(self) -> None:
|
def test_empty_response(self) -> None:
|
||||||
self._test('test_empty_response', 'hello', 'Error. No result.')
|
self._test("test_empty_response", "hello", "Error. No result.")
|
||||||
|
|
||||||
def test_exception(self) -> None:
|
def test_exception(self) -> None:
|
||||||
with patch('logging.exception'):
|
with patch("logging.exception"):
|
||||||
self._test('test_exception', 'hello', 'Error. \'status\'.')
|
self._test("test_exception", "hello", "Error. 'status'.")
|
||||||
|
|
||||||
def test_help(self) -> None:
|
def test_help(self) -> None:
|
||||||
self._test('test_normal', 'help', 'bot info foo bar')
|
self._test("test_normal", "help", "bot info foo bar")
|
||||||
self._test('test_normal', '', 'bot info foo bar')
|
self._test("test_normal", "", "bot info foo bar")
|
||||||
|
|
||||||
def test_alternate_response(self) -> None:
|
def test_alternate_response(self) -> None:
|
||||||
self._test('test_alternate_result', 'hello', 'alternate result')
|
self._test("test_alternate_result", "hello", "alternate result")
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
with self.mock_config_info({'key': 'abcdefg', 'bot_info': 'bot info foo bar'}):
|
with self.mock_config_info({"key": "abcdefg", "bot_info": "bot info foo bar"}):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -9,21 +9,21 @@ URL = "[{name}](https://www.dropbox.com/home{path})"
|
||||||
|
|
||||||
|
|
||||||
class DropboxHandler:
|
class DropboxHandler:
|
||||||
'''
|
"""
|
||||||
This bot allows you to easily share, search and upload files
|
This bot allows you to easily share, search and upload files
|
||||||
between zulip and your dropbox account.
|
between zulip and your dropbox account.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('dropbox_share')
|
self.config_info = bot_handler.get_config_info("dropbox_share")
|
||||||
self.ACCESS_TOKEN = self.config_info.get('access_token')
|
self.ACCESS_TOKEN = self.config_info.get("access_token")
|
||||||
self.client = Dropbox(self.ACCESS_TOKEN)
|
self.client = Dropbox(self.ACCESS_TOKEN)
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return get_help()
|
return get_help()
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
command = message['content']
|
command = message["content"]
|
||||||
if command == "":
|
if command == "":
|
||||||
command = "help"
|
command = "help"
|
||||||
msg = dbx_command(self.client, command)
|
msg = dbx_command(self.client, command)
|
||||||
|
@ -31,7 +31,7 @@ class DropboxHandler:
|
||||||
|
|
||||||
|
|
||||||
def get_help() -> str:
|
def get_help() -> str:
|
||||||
return '''
|
return """
|
||||||
Example commands:
|
Example commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -44,11 +44,11 @@ def get_help() -> str:
|
||||||
@mention-bot search: search a file/folder
|
@mention-bot search: search a file/folder
|
||||||
@mention-bot share: get a shareable link for the file/folder
|
@mention-bot share: get a shareable link for the file/folder
|
||||||
```
|
```
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_usage_examples() -> str:
|
def get_usage_examples() -> str:
|
||||||
return '''
|
return """
|
||||||
Usage:
|
Usage:
|
||||||
```
|
```
|
||||||
@dropbox ls - Shows files/folders in the root folder.
|
@dropbox ls - Shows files/folders in the root folder.
|
||||||
|
@ -62,62 +62,62 @@ def get_usage_examples() -> str:
|
||||||
@dropbox search boo --mr 10 - Search for boo and get at max 10 results.
|
@dropbox search boo --mr 10 - Search for boo and get at max 10 results.
|
||||||
@dropbox search boo --fd foo - Search for boo in folder foo.
|
@dropbox search boo --fd foo - Search for boo in folder foo.
|
||||||
```
|
```
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
REGEXES = dict(
|
REGEXES = dict(
|
||||||
command='(ls|mkdir|read|rm|write|search|usage|help)',
|
command="(ls|mkdir|read|rm|write|search|usage|help)",
|
||||||
path=r'(\S+)',
|
path=r"(\S+)",
|
||||||
optional_path=r'(\S*)',
|
optional_path=r"(\S*)",
|
||||||
some_text='(.+?)',
|
some_text="(.+?)",
|
||||||
folder=r'?(?:--fd (\S+))?',
|
folder=r"?(?:--fd (\S+))?",
|
||||||
max_results=r'?(?:--mr (\d+))?',
|
max_results=r"?(?:--mr (\d+))?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_commands() -> Dict[str, Tuple[Any, List[str]]]:
|
def get_commands() -> Dict[str, Tuple[Any, List[str]]]:
|
||||||
return {
|
return {
|
||||||
'help': (dbx_help, ['command']),
|
"help": (dbx_help, ["command"]),
|
||||||
'ls': (dbx_ls, ['optional_path']),
|
"ls": (dbx_ls, ["optional_path"]),
|
||||||
'mkdir': (dbx_mkdir, ['path']),
|
"mkdir": (dbx_mkdir, ["path"]),
|
||||||
'rm': (dbx_rm, ['path']),
|
"rm": (dbx_rm, ["path"]),
|
||||||
'write': (dbx_write, ['path', 'some_text']),
|
"write": (dbx_write, ["path", "some_text"]),
|
||||||
'read': (dbx_read, ['path']),
|
"read": (dbx_read, ["path"]),
|
||||||
'search': (dbx_search, ['some_text', 'folder', 'max_results']),
|
"search": (dbx_search, ["some_text", "folder", "max_results"]),
|
||||||
'share': (dbx_share, ['path']),
|
"share": (dbx_share, ["path"]),
|
||||||
'usage': (dbx_usage, []),
|
"usage": (dbx_usage, []),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def dbx_command(client: Any, cmd: str) -> str:
|
def dbx_command(client: Any, cmd: str) -> str:
|
||||||
cmd = cmd.strip()
|
cmd = cmd.strip()
|
||||||
if cmd == 'help':
|
if cmd == "help":
|
||||||
return get_help()
|
return get_help()
|
||||||
cmd_name = cmd.split()[0]
|
cmd_name = cmd.split()[0]
|
||||||
cmd_args = cmd[len(cmd_name) :].strip()
|
cmd_args = cmd[len(cmd_name) :].strip()
|
||||||
commands = get_commands()
|
commands = get_commands()
|
||||||
if cmd_name not in commands:
|
if cmd_name not in commands:
|
||||||
return 'ERROR: unrecognized command\n' + get_help()
|
return "ERROR: unrecognized command\n" + get_help()
|
||||||
f, arg_names = commands[cmd_name]
|
f, arg_names = commands[cmd_name]
|
||||||
partial_regexes = [REGEXES[a] for a in arg_names]
|
partial_regexes = [REGEXES[a] for a in arg_names]
|
||||||
regex = ' '.join(partial_regexes)
|
regex = " ".join(partial_regexes)
|
||||||
regex += '$'
|
regex += "$"
|
||||||
m = re.match(regex, cmd_args)
|
m = re.match(regex, cmd_args)
|
||||||
if m:
|
if m:
|
||||||
return f(client, *m.groups())
|
return f(client, *m.groups())
|
||||||
else:
|
else:
|
||||||
return 'ERROR: ' + syntax_help(cmd_name)
|
return "ERROR: " + syntax_help(cmd_name)
|
||||||
|
|
||||||
|
|
||||||
def syntax_help(cmd_name: str) -> str:
|
def syntax_help(cmd_name: str) -> str:
|
||||||
commands = get_commands()
|
commands = get_commands()
|
||||||
f, arg_names = commands[cmd_name]
|
f, arg_names = commands[cmd_name]
|
||||||
arg_syntax = ' '.join('<' + a + '>' for a in arg_names)
|
arg_syntax = " ".join("<" + a + ">" for a in arg_names)
|
||||||
if arg_syntax:
|
if arg_syntax:
|
||||||
cmd = cmd_name + ' ' + arg_syntax
|
cmd = cmd_name + " " + arg_syntax
|
||||||
else:
|
else:
|
||||||
cmd = cmd_name
|
cmd = cmd_name
|
||||||
return 'syntax: {}'.format(cmd)
|
return "syntax: {}".format(cmd)
|
||||||
|
|
||||||
|
|
||||||
def dbx_help(client: Any, cmd_name: str) -> str:
|
def dbx_help(client: Any, cmd_name: str) -> str:
|
||||||
|
@ -129,7 +129,7 @@ def dbx_usage(client: Any) -> str:
|
||||||
|
|
||||||
|
|
||||||
def dbx_mkdir(client: Any, fn: str) -> str:
|
def dbx_mkdir(client: Any, fn: str) -> str:
|
||||||
fn = '/' + fn # foo/boo -> /foo/boo
|
fn = "/" + fn # foo/boo -> /foo/boo
|
||||||
try:
|
try:
|
||||||
result = client.files_create_folder(fn)
|
result = client.files_create_folder(fn)
|
||||||
msg = "CREATED FOLDER: " + URL.format(name=result.name, path=result.path_lower)
|
msg = "CREATED FOLDER: " + URL.format(name=result.name, path=result.path_lower)
|
||||||
|
@ -143,8 +143,8 @@ def dbx_mkdir(client: Any, fn: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def dbx_ls(client: Any, fn: str) -> str:
|
def dbx_ls(client: Any, fn: str) -> str:
|
||||||
if fn != '':
|
if fn != "":
|
||||||
fn = '/' + fn
|
fn = "/" + fn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = client.files_list_folder(fn)
|
result = client.files_list_folder(fn)
|
||||||
|
@ -152,9 +152,9 @@ def dbx_ls(client: Any, fn: str) -> str:
|
||||||
for meta in result.entries:
|
for meta in result.entries:
|
||||||
files_list += [" - " + URL.format(name=meta.name, path=meta.path_lower)]
|
files_list += [" - " + URL.format(name=meta.name, path=meta.path_lower)]
|
||||||
|
|
||||||
msg = '\n'.join(files_list)
|
msg = "\n".join(files_list)
|
||||||
if msg == '':
|
if msg == "":
|
||||||
msg = '`No files available`'
|
msg = "`No files available`"
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = (
|
msg = (
|
||||||
|
@ -167,7 +167,7 @@ def dbx_ls(client: Any, fn: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def dbx_rm(client: Any, fn: str) -> str:
|
def dbx_rm(client: Any, fn: str) -> str:
|
||||||
fn = '/' + fn
|
fn = "/" + fn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = client.files_delete(fn)
|
result = client.files_delete(fn)
|
||||||
|
@ -181,7 +181,7 @@ def dbx_rm(client: Any, fn: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def dbx_write(client: Any, fn: str, content: str) -> str:
|
def dbx_write(client: Any, fn: str, content: str) -> str:
|
||||||
fn = '/' + fn
|
fn = "/" + fn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = client.files_upload(content.encode(), fn)
|
result = client.files_upload(content.encode(), fn)
|
||||||
|
@ -193,7 +193,7 @@ def dbx_write(client: Any, fn: str, content: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def dbx_read(client: Any, fn: str) -> str:
|
def dbx_read(client: Any, fn: str) -> str:
|
||||||
fn = '/' + fn
|
fn = "/" + fn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = client.files_download(fn)
|
result = client.files_download(fn)
|
||||||
|
@ -208,11 +208,11 @@ def dbx_read(client: Any, fn: str) -> str:
|
||||||
|
|
||||||
def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
|
def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
|
||||||
if folder is None:
|
if folder is None:
|
||||||
folder = ''
|
folder = ""
|
||||||
else:
|
else:
|
||||||
folder = '/' + folder
|
folder = "/" + folder
|
||||||
if max_results is None:
|
if max_results is None:
|
||||||
max_results = '20'
|
max_results = "20"
|
||||||
try:
|
try:
|
||||||
result = client.files_search(folder, query, max_results=int(max_results))
|
result = client.files_search(folder, query, max_results=int(max_results))
|
||||||
msg_list = []
|
msg_list = []
|
||||||
|
@ -221,7 +221,7 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
|
||||||
file_info = entry.metadata
|
file_info = entry.metadata
|
||||||
count += 1
|
count += 1
|
||||||
msg_list += [" - " + URL.format(name=file_info.name, path=file_info.path_lower)]
|
msg_list += [" - " + URL.format(name=file_info.name, path=file_info.path_lower)]
|
||||||
msg = '\n'.join(msg_list)
|
msg = "\n".join(msg_list)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = (
|
msg = (
|
||||||
|
@ -230,7 +230,7 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
|
||||||
" `--fd <folderName>` to search in specific folder."
|
" `--fd <folderName>` to search in specific folder."
|
||||||
)
|
)
|
||||||
|
|
||||||
if msg == '':
|
if msg == "":
|
||||||
msg = (
|
msg = (
|
||||||
"No files/folders found matching your query.\n"
|
"No files/folders found matching your query.\n"
|
||||||
"For file name searching, the last token is used for prefix matching"
|
"For file name searching, the last token is used for prefix matching"
|
||||||
|
@ -241,7 +241,7 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def dbx_share(client: Any, fn: str):
|
def dbx_share(client: Any, fn: str):
|
||||||
fn = '/' + fn
|
fn = "/" + fn
|
||||||
try:
|
try:
|
||||||
result = client.sharing_create_shared_link(fn)
|
result = client.sharing_create_shared_link(fn)
|
||||||
msg = result.url
|
msg = result.url
|
||||||
|
|
|
@ -13,15 +13,15 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
||||||
|
|
||||||
def get_root_files_list(*args, **kwargs):
|
def get_root_files_list(*args, **kwargs):
|
||||||
return MockListFolderResult(
|
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):
|
def get_folder_files_list(*args, **kwargs):
|
||||||
return MockListFolderResult(
|
return MockListFolderResult(
|
||||||
entries=[
|
entries=[
|
||||||
MockFileMetadata('moo', '/foo/moo'),
|
MockFileMetadata("moo", "/foo/moo"),
|
||||||
MockFileMetadata('noo', '/foo/noo'),
|
MockFileMetadata("noo", "/foo/noo"),
|
||||||
],
|
],
|
||||||
has_more=False,
|
has_more=False,
|
||||||
)
|
)
|
||||||
|
@ -32,18 +32,18 @@ def get_empty_files_list(*args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def create_file(*args, **kwargs):
|
def create_file(*args, **kwargs):
|
||||||
return MockFileMetadata('foo', '/foo')
|
return MockFileMetadata("foo", "/foo")
|
||||||
|
|
||||||
|
|
||||||
def download_file(*args, **kwargs):
|
def download_file(*args, **kwargs):
|
||||||
return [MockFileMetadata('foo', '/foo'), MockHttpResponse('boo')]
|
return [MockFileMetadata("foo", "/foo"), MockHttpResponse("boo")]
|
||||||
|
|
||||||
|
|
||||||
def search_files(*args, **kwargs):
|
def search_files(*args, **kwargs):
|
||||||
return MockSearchResult(
|
return MockSearchResult(
|
||||||
[
|
[
|
||||||
MockSearchMatch(MockFileMetadata('foo', '/foo')),
|
MockSearchMatch(MockFileMetadata("foo", "/foo")),
|
||||||
MockSearchMatch(MockFileMetadata('fooboo', '/fooboo')),
|
MockSearchMatch(MockFileMetadata("fooboo", "/fooboo")),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,11 +53,11 @@ def get_empty_search_result(*args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def get_shared_link(*args, **kwargs):
|
def get_shared_link(*args, **kwargs):
|
||||||
return MockPathLinkMetadata('http://www.foo.com/boo')
|
return MockPathLinkMetadata("http://www.foo.com/boo")
|
||||||
|
|
||||||
|
|
||||||
def get_help() -> str:
|
def get_help() -> str:
|
||||||
return '''
|
return """
|
||||||
Example commands:
|
Example commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -70,7 +70,7 @@ def get_help() -> str:
|
||||||
@mention-bot search: search a file/folder
|
@mention-bot search: search a file/folder
|
||||||
@mention-bot share: get a shareable link for the file/folder
|
@mention-bot share: get a shareable link for the file/folder
|
||||||
```
|
```
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TestDropboxBot(BotTestCase, DefaultTests):
|
class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
|
@ -79,8 +79,8 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self):
|
def test_bot_responds_to_empty_message(self):
|
||||||
with self.mock_config_info(self.config_info):
|
with self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('', get_help())
|
self.verify_reply("", get_help())
|
||||||
self.verify_reply('help', get_help())
|
self.verify_reply("help", get_help())
|
||||||
|
|
||||||
def test_dbx_ls_root(self):
|
def test_dbx_ls_root(self):
|
||||||
bot_response = (
|
bot_response = (
|
||||||
|
@ -88,7 +88,7 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
" - [boo](https://www.dropbox.com/home/boo)"
|
" - [boo](https://www.dropbox.com/home/boo)"
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_list_folder', side_effect=get_root_files_list
|
"dropbox.Dropbox.files_list_folder", side_effect=get_root_files_list
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply("ls", bot_response)
|
self.verify_reply("ls", bot_response)
|
||||||
|
|
||||||
|
@ -98,14 +98,14 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
" - [noo](https://www.dropbox.com/home/foo/noo)"
|
" - [noo](https://www.dropbox.com/home/foo/noo)"
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_list_folder', side_effect=get_folder_files_list
|
"dropbox.Dropbox.files_list_folder", side_effect=get_folder_files_list
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply("ls foo", bot_response)
|
self.verify_reply("ls foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_ls_empty(self):
|
def test_dbx_ls_empty(self):
|
||||||
bot_response = '`No files available`'
|
bot_response = "`No files available`"
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_list_folder', side_effect=get_empty_files_list
|
"dropbox.Dropbox.files_list_folder", side_effect=get_empty_files_list
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply("ls", bot_response)
|
self.verify_reply("ls", bot_response)
|
||||||
|
|
||||||
|
@ -116,16 +116,16 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
"or simply `ls` for listing folders in the root directory"
|
"or simply `ls` for listing folders in the root directory"
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_list_folder', side_effect=Exception()
|
"dropbox.Dropbox.files_list_folder", side_effect=Exception()
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply("ls", bot_response)
|
self.verify_reply("ls", bot_response)
|
||||||
|
|
||||||
def test_dbx_mkdir(self):
|
def test_dbx_mkdir(self):
|
||||||
bot_response = "CREATED FOLDER: [foo](https://www.dropbox.com/home/foo)"
|
bot_response = "CREATED FOLDER: [foo](https://www.dropbox.com/home/foo)"
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_create_folder', side_effect=create_file
|
"dropbox.Dropbox.files_create_folder", side_effect=create_file
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('mkdir foo', bot_response)
|
self.verify_reply("mkdir foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_mkdir_error(self):
|
def test_dbx_mkdir_error(self):
|
||||||
bot_response = (
|
bot_response = (
|
||||||
|
@ -133,49 +133,49 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
"Usage: `mkdir <foldername>` to create a folder."
|
"Usage: `mkdir <foldername>` to create a folder."
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_create_folder', side_effect=Exception()
|
"dropbox.Dropbox.files_create_folder", side_effect=Exception()
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('mkdir foo/bar', bot_response)
|
self.verify_reply("mkdir foo/bar", bot_response)
|
||||||
|
|
||||||
def test_dbx_rm(self):
|
def test_dbx_rm(self):
|
||||||
bot_response = "DELETED File/Folder : [foo](https://www.dropbox.com/home/foo)"
|
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(
|
with patch("dropbox.Dropbox.files_delete", side_effect=create_file), self.mock_config_info(
|
||||||
self.config_info
|
self.config_info
|
||||||
):
|
):
|
||||||
self.verify_reply('rm foo', bot_response)
|
self.verify_reply("rm foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_rm_error(self):
|
def test_dbx_rm_error(self):
|
||||||
bot_response = (
|
bot_response = (
|
||||||
"Please provide a correct folder path and name.\n"
|
"Please provide a correct folder path and name.\n"
|
||||||
"Usage: `rm <foldername>` to delete a folder in root directory."
|
"Usage: `rm <foldername>` to delete a folder in root directory."
|
||||||
)
|
)
|
||||||
with patch('dropbox.Dropbox.files_delete', side_effect=Exception()), self.mock_config_info(
|
with patch("dropbox.Dropbox.files_delete", side_effect=Exception()), self.mock_config_info(
|
||||||
self.config_info
|
self.config_info
|
||||||
):
|
):
|
||||||
self.verify_reply('rm foo', bot_response)
|
self.verify_reply("rm foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_write(self):
|
def test_dbx_write(self):
|
||||||
bot_response = "Written to file: [foo](https://www.dropbox.com/home/foo)"
|
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(
|
with patch("dropbox.Dropbox.files_upload", side_effect=create_file), self.mock_config_info(
|
||||||
self.config_info
|
self.config_info
|
||||||
):
|
):
|
||||||
self.verify_reply('write foo boo', bot_response)
|
self.verify_reply("write foo boo", bot_response)
|
||||||
|
|
||||||
def test_dbx_write_error(self):
|
def test_dbx_write_error(self):
|
||||||
bot_response = (
|
bot_response = (
|
||||||
"Incorrect file path or file already exists.\nUsage: `write <filename> CONTENT`"
|
"Incorrect file path or file already exists.\nUsage: `write <filename> CONTENT`"
|
||||||
)
|
)
|
||||||
with patch('dropbox.Dropbox.files_upload', side_effect=Exception()), self.mock_config_info(
|
with patch("dropbox.Dropbox.files_upload", side_effect=Exception()), self.mock_config_info(
|
||||||
self.config_info
|
self.config_info
|
||||||
):
|
):
|
||||||
self.verify_reply('write foo boo', bot_response)
|
self.verify_reply("write foo boo", bot_response)
|
||||||
|
|
||||||
def test_dbx_read(self):
|
def test_dbx_read(self):
|
||||||
bot_response = "**foo** :\nboo"
|
bot_response = "**foo** :\nboo"
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_download', side_effect=download_file
|
"dropbox.Dropbox.files_download", side_effect=download_file
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('read foo', bot_response)
|
self.verify_reply("read foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_read_error(self):
|
def test_dbx_read_error(self):
|
||||||
bot_response = (
|
bot_response = (
|
||||||
|
@ -183,16 +183,16 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
"Usage: `read <filename>` to read content of a file"
|
"Usage: `read <filename>` to read content of a file"
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_download', side_effect=Exception()
|
"dropbox.Dropbox.files_download", side_effect=Exception()
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('read foo', bot_response)
|
self.verify_reply("read foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_search(self):
|
def test_dbx_search(self):
|
||||||
bot_response = " - [foo](https://www.dropbox.com/home/foo)\n - [fooboo](https://www.dropbox.com/home/fooboo)"
|
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(
|
with patch("dropbox.Dropbox.files_search", side_effect=search_files), self.mock_config_info(
|
||||||
self.config_info
|
self.config_info
|
||||||
):
|
):
|
||||||
self.verify_reply('search foo', bot_response)
|
self.verify_reply("search foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_search_empty(self):
|
def test_dbx_search_empty(self):
|
||||||
bot_response = (
|
bot_response = (
|
||||||
|
@ -201,9 +201,9 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
" (i.e. “bat c” matches “bat cave” but not “batman car”)."
|
" (i.e. “bat c” matches “bat cave” but not “batman car”)."
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.files_search', side_effect=get_empty_search_result
|
"dropbox.Dropbox.files_search", side_effect=get_empty_search_result
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('search boo --fd foo', bot_response)
|
self.verify_reply("search boo --fd foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_search_error(self):
|
def test_dbx_search_error(self):
|
||||||
bot_response = (
|
bot_response = (
|
||||||
|
@ -211,32 +211,32 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
"Note:`--mr <int>` is optional and is used to specify maximun results.\n"
|
"Note:`--mr <int>` is optional and is used to specify maximun results.\n"
|
||||||
" `--fd <folderName>` to search in specific folder."
|
" `--fd <folderName>` to search in specific folder."
|
||||||
)
|
)
|
||||||
with patch('dropbox.Dropbox.files_search', side_effect=Exception()), self.mock_config_info(
|
with patch("dropbox.Dropbox.files_search", side_effect=Exception()), self.mock_config_info(
|
||||||
self.config_info
|
self.config_info
|
||||||
):
|
):
|
||||||
self.verify_reply('search foo', bot_response)
|
self.verify_reply("search foo", bot_response)
|
||||||
|
|
||||||
def test_dbx_share(self):
|
def test_dbx_share(self):
|
||||||
bot_response = 'http://www.foo.com/boo'
|
bot_response = "http://www.foo.com/boo"
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.sharing_create_shared_link', side_effect=get_shared_link
|
"dropbox.Dropbox.sharing_create_shared_link", side_effect=get_shared_link
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('share boo', bot_response)
|
self.verify_reply("share boo", bot_response)
|
||||||
|
|
||||||
def test_dbx_share_error(self):
|
def test_dbx_share_error(self):
|
||||||
bot_response = "Please provide a correct file name.\nUsage: `share <filename>`"
|
bot_response = "Please provide a correct file name.\nUsage: `share <filename>`"
|
||||||
with patch(
|
with patch(
|
||||||
'dropbox.Dropbox.sharing_create_shared_link', side_effect=Exception()
|
"dropbox.Dropbox.sharing_create_shared_link", side_effect=Exception()
|
||||||
), self.mock_config_info(self.config_info):
|
), self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('share boo', bot_response)
|
self.verify_reply("share boo", bot_response)
|
||||||
|
|
||||||
def test_dbx_help(self):
|
def test_dbx_help(self):
|
||||||
bot_response = 'syntax: ls <optional_path>'
|
bot_response = "syntax: ls <optional_path>"
|
||||||
with self.mock_config_info(self.config_info):
|
with self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('help ls', bot_response)
|
self.verify_reply("help ls", bot_response)
|
||||||
|
|
||||||
def test_dbx_usage(self):
|
def test_dbx_usage(self):
|
||||||
bot_response = '''
|
bot_response = """
|
||||||
Usage:
|
Usage:
|
||||||
```
|
```
|
||||||
@dropbox ls - Shows files/folders in the root folder.
|
@dropbox ls - Shows files/folders in the root folder.
|
||||||
|
@ -250,9 +250,9 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
@dropbox search boo --mr 10 - Search for boo and get at max 10 results.
|
@dropbox search boo --mr 10 - Search for boo and get at max 10 results.
|
||||||
@dropbox search boo --fd foo - Search for boo in folder foo.
|
@dropbox search boo --fd foo - Search for boo in folder foo.
|
||||||
```
|
```
|
||||||
'''
|
"""
|
||||||
with self.mock_config_info(self.config_info):
|
with self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('usage', bot_response)
|
self.verify_reply("usage", bot_response)
|
||||||
|
|
||||||
def test_invalid_commands(self):
|
def test_invalid_commands(self):
|
||||||
ls_error_response = "ERROR: syntax: ls <optional_path>"
|
ls_error_response = "ERROR: syntax: ls <optional_path>"
|
||||||
|
@ -277,7 +277,7 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
self.verify_reply("usage foo", usage_error_response)
|
self.verify_reply("usage foo", usage_error_response)
|
||||||
|
|
||||||
def test_unkown_command(self):
|
def test_unkown_command(self):
|
||||||
bot_response = '''ERROR: unrecognized command
|
bot_response = """ERROR: unrecognized command
|
||||||
|
|
||||||
Example commands:
|
Example commands:
|
||||||
|
|
||||||
|
@ -291,6 +291,6 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
||||||
@mention-bot search: search a file/folder
|
@mention-bot search: search a file/folder
|
||||||
@mention-bot share: get a shareable link for the file/folder
|
@mention-bot share: get a shareable link for the file/folder
|
||||||
```
|
```
|
||||||
'''
|
"""
|
||||||
with self.mock_config_info(self.config_info):
|
with self.mock_config_info(self.config_info):
|
||||||
self.verify_reply('unknown command', bot_response)
|
self.verify_reply("unknown command", bot_response)
|
||||||
|
|
|
@ -7,9 +7,9 @@ def encrypt(text: str) -> str:
|
||||||
# This is where the actual ROT13 is applied
|
# This is where the actual ROT13 is applied
|
||||||
# WHY IS .JOIN NOT WORKING?!
|
# WHY IS .JOIN NOT WORKING?!
|
||||||
textlist = list(text)
|
textlist = list(text)
|
||||||
newtext = ''
|
newtext = ""
|
||||||
firsthalf = 'abcdefghijklmABCDEFGHIJKLM'
|
firsthalf = "abcdefghijklmABCDEFGHIJKLM"
|
||||||
lasthalf = 'nopqrstuvwxyzNOPQRSTUVWXYZ'
|
lasthalf = "nopqrstuvwxyzNOPQRSTUVWXYZ"
|
||||||
for char in textlist:
|
for char in textlist:
|
||||||
if char in firsthalf:
|
if char in firsthalf:
|
||||||
newtext += lasthalf[firsthalf.index(char)]
|
newtext += lasthalf[firsthalf.index(char)]
|
||||||
|
@ -22,24 +22,24 @@ def encrypt(text: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
class EncryptHandler:
|
class EncryptHandler:
|
||||||
'''
|
"""
|
||||||
This bot allows users to quickly encrypt messages using ROT13 encryption.
|
This bot allows users to quickly encrypt messages using ROT13 encryption.
|
||||||
It encrypts/decrypts messages starting with @mention-bot.
|
It encrypts/decrypts messages starting with @mention-bot.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
This bot uses ROT13 encryption for its purposes.
|
This bot uses ROT13 encryption for its purposes.
|
||||||
It responds to me starting with @mention-bot.
|
It responds to me starting with @mention-bot.
|
||||||
Feeding encrypted messages into the bot decrypts them.
|
Feeding encrypted messages into the bot decrypts them.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
bot_response = self.get_bot_encrypt_response(message)
|
bot_response = self.get_bot_encrypt_response(message)
|
||||||
bot_handler.send_reply(message, bot_response)
|
bot_handler.send_reply(message, bot_response)
|
||||||
|
|
||||||
def get_bot_encrypt_response(self, message: Dict[str, str]) -> str:
|
def get_bot_encrypt_response(self, message: Dict[str, str]) -> str:
|
||||||
original_content = message['content']
|
original_content = message["content"]
|
||||||
temp_content = encrypt(original_content)
|
temp_content = encrypt(original_content)
|
||||||
send_content = "Encrypted/Decrypted text: " + temp_content
|
send_content = "Encrypted/Decrypted text: " + temp_content
|
||||||
return send_content
|
return send_content
|
||||||
|
|
|
@ -7,7 +7,7 @@ class TestEncryptBot(BotTestCase, DefaultTests):
|
||||||
def test_bot(self) -> None:
|
def test_bot(self) -> None:
|
||||||
dialog = [
|
dialog = [
|
||||||
("", "Encrypted/Decrypted text: "),
|
("", "Encrypted/Decrypted text: "),
|
||||||
("Let\'s Do It", "Encrypted/Decrypted text: Yrg\'f Qb Vg"),
|
("Let's Do It", "Encrypted/Decrypted text: Yrg'f Qb Vg"),
|
||||||
("me&mom together..!!", "Encrypted/Decrypted text: zr&zbz gbtrgure..!!"),
|
("me&mom together..!!", "Encrypted/Decrypted text: zr&zbz gbtrgure..!!"),
|
||||||
("foo bar", "Encrypted/Decrypted text: sbb one"),
|
("foo bar", "Encrypted/Decrypted text: sbb one"),
|
||||||
("Please encrypt this", "Encrypted/Decrypted text: Cyrnfr rapelcg guvf"),
|
("Please encrypt this", "Encrypted/Decrypted text: Cyrnfr rapelcg guvf"),
|
||||||
|
|
|
@ -8,36 +8,36 @@ from zulip_bots.lib import BotHandler
|
||||||
class FileUploaderHandler:
|
class FileUploaderHandler:
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return (
|
return (
|
||||||
'This interactive bot is used to upload files (such as images) to the Zulip server:'
|
"This interactive bot is used to upload files (such as images) to the Zulip server:"
|
||||||
'\n- @uploader <local_file_path> : Upload a file, where <local_file_path> is the path to the file'
|
"\n- @uploader <local_file_path> : Upload a file, where <local_file_path> is the path to the file"
|
||||||
'\n- @uploader help : Display help message'
|
"\n- @uploader help : Display help message"
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
HELP_STR = (
|
HELP_STR = (
|
||||||
'Use this bot with any of the following commands:'
|
"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 <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file"
|
||||||
'\n* `@uploader help` : Display help message'
|
"\n* `@uploader help` : Display help message"
|
||||||
)
|
)
|
||||||
|
|
||||||
content = message['content'].strip()
|
content = message["content"].strip()
|
||||||
if content == 'help':
|
if content == "help":
|
||||||
bot_handler.send_reply(message, HELP_STR)
|
bot_handler.send_reply(message, HELP_STR)
|
||||||
return
|
return
|
||||||
|
|
||||||
path = Path(os.path.expanduser(content))
|
path = Path(os.path.expanduser(content))
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
bot_handler.send_reply(message, 'File `{}` not found'.format(content))
|
bot_handler.send_reply(message, "File `{}` not found".format(content))
|
||||||
return
|
return
|
||||||
|
|
||||||
path = path.resolve()
|
path = path.resolve()
|
||||||
upload = bot_handler.upload_file_from_path(str(path))
|
upload = bot_handler.upload_file_from_path(str(path))
|
||||||
if upload['result'] != 'success':
|
if upload["result"] != "success":
|
||||||
msg = upload['msg']
|
msg = upload["msg"]
|
||||||
bot_handler.send_reply(message, 'Failed to upload `{}` file: {}'.format(path, msg))
|
bot_handler.send_reply(message, "Failed to upload `{}` file: {}".format(path, msg))
|
||||||
return
|
return
|
||||||
|
|
||||||
uploaded_file_reply = '[{}]({})'.format(path.name, upload['uri'])
|
uploaded_file_reply = "[{}]({})".format(path.name, upload["uri"])
|
||||||
bot_handler.send_reply(message, uploaded_file_reply)
|
bot_handler.send_reply(message, uploaded_file_reply)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,36 +7,36 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
||||||
class TestFileUploaderBot(BotTestCase, DefaultTests):
|
class TestFileUploaderBot(BotTestCase, DefaultTests):
|
||||||
bot_name = "file_uploader"
|
bot_name = "file_uploader"
|
||||||
|
|
||||||
@patch('pathlib.Path.is_file', return_value=False)
|
@patch("pathlib.Path.is_file", return_value=False)
|
||||||
def test_file_not_found(self, is_file: Mock) -> None:
|
def test_file_not_found(self, is_file: Mock) -> None:
|
||||||
self.verify_reply('file.txt', 'File `file.txt` not found')
|
self.verify_reply("file.txt", "File `file.txt` not found")
|
||||||
|
|
||||||
@patch('pathlib.Path.resolve', return_value=Path('/file.txt'))
|
@patch("pathlib.Path.resolve", return_value=Path("/file.txt"))
|
||||||
@patch('pathlib.Path.is_file', return_value=True)
|
@patch("pathlib.Path.is_file", return_value=True)
|
||||||
def test_file_upload_failed(self, is_file: Mock, resolve: Mock) -> None:
|
def test_file_upload_failed(self, is_file: Mock, resolve: Mock) -> None:
|
||||||
server_reply = dict(result='', msg='error')
|
server_reply = dict(result="", msg="error")
|
||||||
with patch(
|
with patch(
|
||||||
'zulip_bots.test_lib.StubBotHandler.upload_file_from_path', return_value=server_reply
|
"zulip_bots.test_lib.StubBotHandler.upload_file_from_path", return_value=server_reply
|
||||||
):
|
):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'file.txt', 'Failed to upload `{}` file: error'.format(Path('file.txt').resolve())
|
"file.txt", "Failed to upload `{}` file: error".format(Path("file.txt").resolve())
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('pathlib.Path.resolve', return_value=Path('/file.txt'))
|
@patch("pathlib.Path.resolve", return_value=Path("/file.txt"))
|
||||||
@patch('pathlib.Path.is_file', return_value=True)
|
@patch("pathlib.Path.is_file", return_value=True)
|
||||||
def test_file_upload_success(self, is_file: Mock, resolve: Mock) -> None:
|
def test_file_upload_success(self, is_file: Mock, resolve: Mock) -> None:
|
||||||
server_reply = dict(result='success', uri='https://file/uri')
|
server_reply = dict(result="success", uri="https://file/uri")
|
||||||
with patch(
|
with patch(
|
||||||
'zulip_bots.test_lib.StubBotHandler.upload_file_from_path', return_value=server_reply
|
"zulip_bots.test_lib.StubBotHandler.upload_file_from_path", return_value=server_reply
|
||||||
):
|
):
|
||||||
self.verify_reply('file.txt', '[file.txt](https://file/uri)')
|
self.verify_reply("file.txt", "[file.txt](https://file/uri)")
|
||||||
|
|
||||||
def test_help(self):
|
def test_help(self):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'help',
|
"help",
|
||||||
(
|
(
|
||||||
'Use this bot with any of the following commands:'
|
"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 <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file"
|
||||||
'\n* `@uploader help` : Display help message'
|
"\n* `@uploader help` : Display help message"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,20 +6,20 @@ from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
from zulip_bots.lib import BotHandler
|
from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
USERS_LIST_URL = 'https://api.flock.co/v1/roster.listContacts'
|
USERS_LIST_URL = "https://api.flock.co/v1/roster.listContacts"
|
||||||
SEND_MESSAGE_URL = 'https://api.flock.co/v1/chat.sendMessage'
|
SEND_MESSAGE_URL = "https://api.flock.co/v1/chat.sendMessage"
|
||||||
|
|
||||||
help_message = '''
|
help_message = """
|
||||||
You can send messages to any Flock user associated with your account from Zulip.
|
You can send messages to any Flock user associated with your account from Zulip.
|
||||||
*Syntax*: **@botname to: message** where `to` is **firstName** of recipient.
|
*Syntax*: **@botname to: message** where `to` is **firstName** of recipient.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# Matches the recipient name provided by user with list of users in his contacts.
|
# Matches the recipient name provided by user with list of users in his contacts.
|
||||||
# If matches, returns the matched User's ID
|
# If matches, returns the matched User's ID
|
||||||
def find_recipient_id(users: List[Any], recipient_name: str) -> str:
|
def find_recipient_id(users: List[Any], recipient_name: str) -> str:
|
||||||
for user in users:
|
for user in users:
|
||||||
if recipient_name == user['firstName']:
|
if recipient_name == user["firstName"]:
|
||||||
return user['id']
|
return user["id"]
|
||||||
|
|
||||||
|
|
||||||
# Make request to given flock URL and return a two-element tuple
|
# Make request to given flock URL and return a two-element tuple
|
||||||
|
@ -42,8 +42,8 @@ right now.\nPlease try again later"
|
||||||
def get_recipient_id(
|
def get_recipient_id(
|
||||||
recipient_name: str, config: Dict[str, str]
|
recipient_name: str, config: Dict[str, str]
|
||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
token = config['token']
|
token = config["token"]
|
||||||
payload = {'token': token}
|
payload = {"token": token}
|
||||||
users, error = make_flock_request(USERS_LIST_URL, payload)
|
users, error = make_flock_request(USERS_LIST_URL, payload)
|
||||||
if users is None:
|
if users is None:
|
||||||
return (None, error)
|
return (None, error)
|
||||||
|
@ -58,8 +58,8 @@ def get_recipient_id(
|
||||||
|
|
||||||
# This handles the message sending work.
|
# This handles the message sending work.
|
||||||
def get_flock_response(content: str, config: Dict[str, str]) -> str:
|
def get_flock_response(content: str, config: Dict[str, str]) -> str:
|
||||||
token = config['token']
|
token = config["token"]
|
||||||
content_pieces = content.split(':')
|
content_pieces = content.split(":")
|
||||||
recipient_name = content_pieces[0].strip()
|
recipient_name = content_pieces[0].strip()
|
||||||
message = content_pieces[1].strip()
|
message = content_pieces[1].strip()
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ def get_flock_response(content: str, config: Dict[str, str]) -> str:
|
||||||
if len(str(recipient_id)) > 30:
|
if len(str(recipient_id)) > 30:
|
||||||
return "Found user is invalid."
|
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)
|
res, error = make_flock_request(SEND_MESSAGE_URL, payload)
|
||||||
if res is None:
|
if res is None:
|
||||||
return error
|
return error
|
||||||
|
@ -83,7 +83,7 @@ def get_flock_response(content: str, config: Dict[str, str]) -> str:
|
||||||
|
|
||||||
def get_flock_bot_response(content: str, config: Dict[str, str]) -> None:
|
def get_flock_bot_response(content: str, config: Dict[str, str]) -> None:
|
||||||
content = content.strip()
|
content = content.strip()
|
||||||
if content == '' or content == 'help':
|
if content == "" or content == "help":
|
||||||
return help_message
|
return help_message
|
||||||
else:
|
else:
|
||||||
result = get_flock_response(content, config)
|
result = get_flock_response(content, config)
|
||||||
|
@ -91,20 +91,20 @@ def get_flock_bot_response(content: str, config: Dict[str, str]) -> None:
|
||||||
|
|
||||||
|
|
||||||
class FlockHandler:
|
class FlockHandler:
|
||||||
'''
|
"""
|
||||||
This is flock bot. Now you can send messages to any of your
|
This is flock bot. Now you can send messages to any of your
|
||||||
flock user without having to leave Zulip.
|
flock user without having to leave Zulip.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('flock')
|
self.config_info = bot_handler.get_config_info("flock")
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''Hello from Flock Bot. You can send messages to any Flock user
|
return """Hello from Flock Bot. You can send messages to any Flock user
|
||||||
right from Zulip.'''
|
right from Zulip."""
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
response = get_flock_bot_response(message['content'], self.config_info)
|
response = get_flock_bot_response(message["content"], self.config_info)
|
||||||
bot_handler.send_reply(message, response)
|
bot_handler.send_reply(message, response)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,74 +11,74 @@ class TestFlockBot(BotTestCase, DefaultTests):
|
||||||
|
|
||||||
message_config = {"token": "12345", "text": "Ricky: test message", "to": "u:somekey"}
|
message_config = {"token": "12345", "text": "Ricky: test message", "to": "u:somekey"}
|
||||||
|
|
||||||
help_message = '''
|
help_message = """
|
||||||
You can send messages to any Flock user associated with your account from Zulip.
|
You can send messages to any Flock user associated with your account from Zulip.
|
||||||
*Syntax*: **@botname to: message** where `to` is **firstName** of recipient.
|
*Syntax*: **@botname to: message** where `to` is **firstName** of recipient.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
self.verify_reply('', self.help_message)
|
self.verify_reply("", self.help_message)
|
||||||
|
|
||||||
def test_help_message(self) -> None:
|
def test_help_message(self) -> None:
|
||||||
self.verify_reply('', self.help_message)
|
self.verify_reply("", self.help_message)
|
||||||
|
|
||||||
def test_fetch_id_connection_error(self) -> None:
|
def test_fetch_id_connection_error(self) -> None:
|
||||||
with self.mock_config_info(self.normal_config), patch(
|
with self.mock_config_info(self.normal_config), patch(
|
||||||
'requests.get', side_effect=ConnectionError()
|
"requests.get", side_effect=ConnectionError()
|
||||||
), patch('logging.exception'):
|
), patch("logging.exception"):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'tyler: Hey tyler',
|
"tyler: Hey tyler",
|
||||||
"Uh-Oh, couldn\'t process the request \
|
"Uh-Oh, couldn't process the request \
|
||||||
right now.\nPlease try again later",
|
right now.\nPlease try again later",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_response_connection_error(self) -> None:
|
def test_response_connection_error(self) -> None:
|
||||||
with self.mock_config_info(self.message_config), patch(
|
with self.mock_config_info(self.message_config), patch(
|
||||||
'requests.get', side_effect=ConnectionError()
|
"requests.get", side_effect=ConnectionError()
|
||||||
), patch('logging.exception'):
|
), patch("logging.exception"):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'Ricky: test message',
|
"Ricky: test message",
|
||||||
"Uh-Oh, couldn\'t process the request \
|
"Uh-Oh, couldn't process the request \
|
||||||
right now.\nPlease try again later",
|
right now.\nPlease try again later",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_no_recipient_found(self) -> None:
|
def test_no_recipient_found(self) -> None:
|
||||||
bot_response = "No user found. Make sure you typed it correctly."
|
bot_response = "No user found. Make sure you typed it correctly."
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_no_recipient_found'
|
"test_no_recipient_found"
|
||||||
):
|
):
|
||||||
self.verify_reply('david: hello', bot_response)
|
self.verify_reply("david: hello", bot_response)
|
||||||
|
|
||||||
def test_found_invalid_recipient(self) -> None:
|
def test_found_invalid_recipient(self) -> None:
|
||||||
bot_response = "Found user is invalid."
|
bot_response = "Found user is invalid."
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_found_invalid_recipient'
|
"test_found_invalid_recipient"
|
||||||
):
|
):
|
||||||
self.verify_reply('david: hello', bot_response)
|
self.verify_reply("david: hello", bot_response)
|
||||||
|
|
||||||
@patch('zulip_bots.bots.flock.flock.get_recipient_id')
|
@patch("zulip_bots.bots.flock.flock.get_recipient_id")
|
||||||
def test_message_send_connection_error(self, get_recipient_id: str) -> None:
|
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"
|
bot_response = "Uh-Oh, couldn't process the request right now.\nPlease try again later"
|
||||||
get_recipient_id.return_value = ["u:userid", None]
|
get_recipient_id.return_value = ["u:userid", None]
|
||||||
with self.mock_config_info(self.normal_config), patch(
|
with self.mock_config_info(self.normal_config), patch(
|
||||||
'requests.get', side_effect=ConnectionError()
|
"requests.get", side_effect=ConnectionError()
|
||||||
), patch('logging.exception'):
|
), patch("logging.exception"):
|
||||||
self.verify_reply('Rishabh: hi there', bot_response)
|
self.verify_reply("Rishabh: hi there", bot_response)
|
||||||
|
|
||||||
@patch('zulip_bots.bots.flock.flock.get_recipient_id')
|
@patch("zulip_bots.bots.flock.flock.get_recipient_id")
|
||||||
def test_message_send_success(self, get_recipient_id: str) -> None:
|
def test_message_send_success(self, get_recipient_id: str) -> None:
|
||||||
bot_response = "Message sent."
|
bot_response = "Message sent."
|
||||||
get_recipient_id.return_value = ["u:userid", None]
|
get_recipient_id.return_value = ["u:userid", None]
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_message_send_success'
|
"test_message_send_success"
|
||||||
):
|
):
|
||||||
self.verify_reply('Rishabh: hi there', bot_response)
|
self.verify_reply("Rishabh: hi there", bot_response)
|
||||||
|
|
||||||
@patch('zulip_bots.bots.flock.flock.get_recipient_id')
|
@patch("zulip_bots.bots.flock.flock.get_recipient_id")
|
||||||
def test_message_send_failed(self, get_recipient_id: str) -> None:
|
def test_message_send_failed(self, get_recipient_id: str) -> None:
|
||||||
bot_response = "Message sending failed :slightly_frowning_face:. Please try again."
|
bot_response = "Message sending failed :slightly_frowning_face:. Please try again."
|
||||||
get_recipient_id.return_value = ["u:invalid", None]
|
get_recipient_id.return_value = ["u:invalid", None]
|
||||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||||
'test_message_send_failed'
|
"test_message_send_failed"
|
||||||
):
|
):
|
||||||
self.verify_reply('Rishabh: hi there', bot_response)
|
self.verify_reply("Rishabh: hi there", bot_response)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
|
|
||||||
class FollowupHandler:
|
class FollowupHandler:
|
||||||
'''
|
"""
|
||||||
This plugin facilitates creating follow-up tasks when
|
This plugin facilitates creating follow-up tasks when
|
||||||
you are using Zulip to conduct a virtual meeting. It
|
you are using Zulip to conduct a virtual meeting. It
|
||||||
looks for messages starting with '@mention-bot'.
|
looks for messages starting with '@mention-bot'.
|
||||||
|
@ -14,45 +14,45 @@ class FollowupHandler:
|
||||||
Zulip stream called "followup," but this code could
|
Zulip stream called "followup," but this code could
|
||||||
be adapted to write follow up items to some kind of
|
be adapted to write follow up items to some kind of
|
||||||
external issue tracker as well.
|
external issue tracker as well.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
This plugin will allow users to flag messages
|
This plugin will allow users to flag messages
|
||||||
as being follow-up items. Users should preface
|
as being follow-up items. Users should preface
|
||||||
messages with "@mention-bot".
|
messages with "@mention-bot".
|
||||||
|
|
||||||
Before running this, make sure to create a stream
|
Before running this, make sure to create a stream
|
||||||
called "followup" that your API user can send to.
|
called "followup" that your API user can send to.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('followup', optional=False)
|
self.config_info = bot_handler.get_config_info("followup", optional=False)
|
||||||
self.stream = self.config_info.get("stream", 'followup')
|
self.stream = self.config_info.get("stream", "followup")
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
if message['content'] == '':
|
if message["content"] == "":
|
||||||
bot_response = (
|
bot_response = (
|
||||||
"Please specify the message you want to send to followup stream after @mention-bot"
|
"Please specify the message you want to send to followup stream after @mention-bot"
|
||||||
)
|
)
|
||||||
bot_handler.send_reply(message, bot_response)
|
bot_handler.send_reply(message, bot_response)
|
||||||
elif message['content'] == 'help':
|
elif message["content"] == "help":
|
||||||
bot_handler.send_reply(message, self.usage())
|
bot_handler.send_reply(message, self.usage())
|
||||||
else:
|
else:
|
||||||
bot_response = self.get_bot_followup_response(message)
|
bot_response = self.get_bot_followup_response(message)
|
||||||
bot_handler.send_message(
|
bot_handler.send_message(
|
||||||
dict(
|
dict(
|
||||||
type='stream',
|
type="stream",
|
||||||
to=self.stream,
|
to=self.stream,
|
||||||
subject=message['sender_email'],
|
subject=message["sender_email"],
|
||||||
content=bot_response,
|
content=bot_response,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_bot_followup_response(self, message: Dict[str, str]) -> str:
|
def get_bot_followup_response(self, message: Dict[str, str]) -> str:
|
||||||
original_content = message['content']
|
original_content = message["content"]
|
||||||
original_sender = message['sender_email']
|
original_sender = message["sender_email"]
|
||||||
temp_content = 'from %s: ' % (original_sender,)
|
temp_content = "from %s: " % (original_sender,)
|
||||||
new_content = temp_content + original_content
|
new_content = temp_content + original_content
|
||||||
|
|
||||||
return new_content
|
return new_content
|
||||||
|
|
|
@ -6,48 +6,48 @@ class TestFollowUpBot(BotTestCase, DefaultTests):
|
||||||
|
|
||||||
def test_followup_stream(self) -> None:
|
def test_followup_stream(self) -> None:
|
||||||
message = dict(
|
message = dict(
|
||||||
content='feed the cat',
|
content="feed the cat",
|
||||||
type='stream',
|
type="stream",
|
||||||
sender_email='foo@example.com',
|
sender_email="foo@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.mock_config_info({'stream': 'followup'}):
|
with self.mock_config_info({"stream": "followup"}):
|
||||||
response = self.get_response(message)
|
response = self.get_response(message)
|
||||||
|
|
||||||
self.assertEqual(response['content'], 'from foo@example.com: feed the cat')
|
self.assertEqual(response["content"], "from foo@example.com: feed the cat")
|
||||||
self.assertEqual(response['to'], 'followup')
|
self.assertEqual(response["to"], "followup")
|
||||||
|
|
||||||
def test_different_stream(self) -> None:
|
def test_different_stream(self) -> None:
|
||||||
message = dict(
|
message = dict(
|
||||||
content='feed the cat',
|
content="feed the cat",
|
||||||
type='stream',
|
type="stream",
|
||||||
sender_email='foo@example.com',
|
sender_email="foo@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.mock_config_info({'stream': 'issue'}):
|
with self.mock_config_info({"stream": "issue"}):
|
||||||
response = self.get_response(message)
|
response = self.get_response(message)
|
||||||
|
|
||||||
self.assertEqual(response['content'], 'from foo@example.com: feed the cat')
|
self.assertEqual(response["content"], "from foo@example.com: feed the cat")
|
||||||
self.assertEqual(response['to'], 'issue')
|
self.assertEqual(response["to"], "issue")
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
bot_response = (
|
bot_response = (
|
||||||
'Please specify the message you want to send to followup stream after @mention-bot'
|
"Please specify the message you want to send to followup stream after @mention-bot"
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.mock_config_info({'stream': 'followup'}):
|
with self.mock_config_info({"stream": "followup"}):
|
||||||
self.verify_reply('', bot_response)
|
self.verify_reply("", bot_response)
|
||||||
|
|
||||||
def test_help_text(self) -> None:
|
def test_help_text(self) -> None:
|
||||||
request = 'help'
|
request = "help"
|
||||||
bot_response = '''
|
bot_response = """
|
||||||
This plugin will allow users to flag messages
|
This plugin will allow users to flag messages
|
||||||
as being follow-up items. Users should preface
|
as being follow-up items. Users should preface
|
||||||
messages with "@mention-bot".
|
messages with "@mention-bot".
|
||||||
|
|
||||||
Before running this, make sure to create a stream
|
Before running this, make sure to create a stream
|
||||||
called "followup" that your API user can send to.
|
called "followup" that your API user can send to.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
with self.mock_config_info({'stream': 'followup'}):
|
with self.mock_config_info({"stream": "followup"}):
|
||||||
self.verify_reply(request, bot_response)
|
self.verify_reply(request, bot_response)
|
||||||
|
|
|
@ -9,24 +9,24 @@ from zulip_bots.lib import BotHandler
|
||||||
class FrontHandler:
|
class FrontHandler:
|
||||||
FRONT_API = "https://api2.frontapp.com/conversations/{}"
|
FRONT_API = "https://api2.frontapp.com/conversations/{}"
|
||||||
COMMANDS = [
|
COMMANDS = [
|
||||||
('archive', "Archive a conversation."),
|
("archive", "Archive a conversation."),
|
||||||
('delete', "Delete a conversation."),
|
("delete", "Delete a conversation."),
|
||||||
('spam', "Mark a conversation as spam."),
|
("spam", "Mark a conversation as spam."),
|
||||||
('open', "Restore a conversation."),
|
("open", "Restore a conversation."),
|
||||||
('comment <text>', "Leave a comment."),
|
("comment <text>", "Leave a comment."),
|
||||||
]
|
]
|
||||||
CNV_ID_REGEXP = 'cnv_(?P<id>[0-9a-z]+)'
|
CNV_ID_REGEXP = "cnv_(?P<id>[0-9a-z]+)"
|
||||||
COMMENT_PREFIX = "comment "
|
COMMENT_PREFIX = "comment "
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
Front Bot uses the Front REST API to interact with Front. In order to use
|
Front Bot uses the Front REST API to interact with Front. In order to use
|
||||||
Front Bot, `front.conf` must be set up. See `doc.md` for more details.
|
Front Bot, `front.conf` must be set up. See `doc.md` for more details.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
config = bot_handler.get_config_info('front')
|
config = bot_handler.get_config_info("front")
|
||||||
api_key = config.get('api_key')
|
api_key = config.get("api_key")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise KeyError("No API key specified.")
|
raise KeyError("No API key specified.")
|
||||||
|
|
||||||
|
@ -100,9 +100,9 @@ class FrontHandler:
|
||||||
return "Comment was sent."
|
return "Comment was sent."
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||||
command = message['content']
|
command = message["content"]
|
||||||
|
|
||||||
result = re.search(self.CNV_ID_REGEXP, message['subject'])
|
result = re.search(self.CNV_ID_REGEXP, message["subject"])
|
||||||
if not result:
|
if not result:
|
||||||
bot_handler.send_reply(
|
bot_handler.send_reply(
|
||||||
message,
|
message,
|
||||||
|
@ -114,25 +114,25 @@ class FrontHandler:
|
||||||
|
|
||||||
self.conversation_id = result.group()
|
self.conversation_id = result.group()
|
||||||
|
|
||||||
if command == 'help':
|
if command == "help":
|
||||||
bot_handler.send_reply(message, self.help(bot_handler))
|
bot_handler.send_reply(message, self.help(bot_handler))
|
||||||
|
|
||||||
elif command == 'archive':
|
elif command == "archive":
|
||||||
bot_handler.send_reply(message, self.archive(bot_handler))
|
bot_handler.send_reply(message, self.archive(bot_handler))
|
||||||
|
|
||||||
elif command == 'delete':
|
elif command == "delete":
|
||||||
bot_handler.send_reply(message, self.delete(bot_handler))
|
bot_handler.send_reply(message, self.delete(bot_handler))
|
||||||
|
|
||||||
elif command == 'spam':
|
elif command == "spam":
|
||||||
bot_handler.send_reply(message, self.spam(bot_handler))
|
bot_handler.send_reply(message, self.spam(bot_handler))
|
||||||
|
|
||||||
elif command == 'open':
|
elif command == "open":
|
||||||
bot_handler.send_reply(message, self.restore(bot_handler))
|
bot_handler.send_reply(message, self.restore(bot_handler))
|
||||||
|
|
||||||
elif command.startswith(self.COMMENT_PREFIX):
|
elif command.startswith(self.COMMENT_PREFIX):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'author_id': "alt:email:" + message['sender_email'],
|
"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))
|
bot_handler.send_reply(message, self.comment(bot_handler, **kwargs))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -4,28 +4,28 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
||||||
|
|
||||||
|
|
||||||
class TestFrontBot(BotTestCase, DefaultTests):
|
class TestFrontBot(BotTestCase, DefaultTests):
|
||||||
bot_name = 'front'
|
bot_name = "front"
|
||||||
|
|
||||||
def make_request_message(self, content: str) -> Dict[str, Any]:
|
def make_request_message(self, content: str) -> Dict[str, Any]:
|
||||||
message = super().make_request_message(content)
|
message = super().make_request_message(content)
|
||||||
message['subject'] = "cnv_kqatm2"
|
message["subject"] = "cnv_kqatm2"
|
||||||
message['sender_email'] = "leela@planet-express.com"
|
message["sender_email"] = "leela@planet-express.com"
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def test_bot_invalid_api_key(self) -> None:
|
def test_bot_invalid_api_key(self) -> None:
|
||||||
invalid_api_key = ''
|
invalid_api_key = ""
|
||||||
with self.mock_config_info({'api_key': invalid_api_key}):
|
with self.mock_config_info({"api_key": invalid_api_key}):
|
||||||
with self.assertRaises(KeyError):
|
with self.assertRaises(KeyError):
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
self.verify_reply("", "Unknown command. Use `help` for instructions.")
|
self.verify_reply("", "Unknown command. Use `help` for instructions.")
|
||||||
|
|
||||||
def test_help(self) -> None:
|
def test_help(self) -> None:
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'help',
|
"help",
|
||||||
"`archive` Archive a conversation.\n"
|
"`archive` Archive a conversation.\n"
|
||||||
"`delete` Delete a conversation.\n"
|
"`delete` Delete a conversation.\n"
|
||||||
"`spam` Mark a conversation as spam.\n"
|
"`spam` Mark a conversation as spam.\n"
|
||||||
|
@ -34,71 +34,71 @@ class TestFrontBot(BotTestCase, DefaultTests):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_archive(self) -> None:
|
def test_archive(self) -> None:
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('archive'):
|
with self.mock_http_conversation("archive"):
|
||||||
self.verify_reply('archive', "Conversation was archived.")
|
self.verify_reply("archive", "Conversation was archived.")
|
||||||
|
|
||||||
def test_archive_error(self) -> None:
|
def test_archive_error(self) -> None:
|
||||||
self._test_command_error('archive')
|
self._test_command_error("archive")
|
||||||
|
|
||||||
def test_delete(self) -> None:
|
def test_delete(self) -> None:
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('delete'):
|
with self.mock_http_conversation("delete"):
|
||||||
self.verify_reply('delete', "Conversation was deleted.")
|
self.verify_reply("delete", "Conversation was deleted.")
|
||||||
|
|
||||||
def test_delete_error(self) -> None:
|
def test_delete_error(self) -> None:
|
||||||
self._test_command_error('delete')
|
self._test_command_error("delete")
|
||||||
|
|
||||||
def test_spam(self) -> None:
|
def test_spam(self) -> None:
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('spam'):
|
with self.mock_http_conversation("spam"):
|
||||||
self.verify_reply('spam', "Conversation was marked as spam.")
|
self.verify_reply("spam", "Conversation was marked as spam.")
|
||||||
|
|
||||||
def test_spam_error(self) -> None:
|
def test_spam_error(self) -> None:
|
||||||
self._test_command_error('spam')
|
self._test_command_error("spam")
|
||||||
|
|
||||||
def test_restore(self) -> None:
|
def test_restore(self) -> None:
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('open'):
|
with self.mock_http_conversation("open"):
|
||||||
self.verify_reply('open', "Conversation was restored.")
|
self.verify_reply("open", "Conversation was restored.")
|
||||||
|
|
||||||
def test_restore_error(self) -> None:
|
def test_restore_error(self) -> None:
|
||||||
self._test_command_error('open')
|
self._test_command_error("open")
|
||||||
|
|
||||||
def test_comment(self) -> None:
|
def test_comment(self) -> None:
|
||||||
body = "@bender, I thought you were supposed to be cooking for this party."
|
body = "@bender, I thought you were supposed to be cooking for this party."
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('comment'):
|
with self.mock_http_conversation("comment"):
|
||||||
self.verify_reply("comment " + body, "Comment was sent.")
|
self.verify_reply("comment " + body, "Comment was sent.")
|
||||||
|
|
||||||
def test_comment_error(self) -> None:
|
def test_comment_error(self) -> None:
|
||||||
body = "@bender, I thought you were supposed to be cooking for this party."
|
body = "@bender, I thought you were supposed to be cooking for this party."
|
||||||
self._test_command_error('comment', body)
|
self._test_command_error("comment", body)
|
||||||
|
|
||||||
def _test_command_error(self, command_name: str, command_arg: Optional[str] = None) -> None:
|
def _test_command_error(self, command_name: str, command_arg: Optional[str] = None) -> None:
|
||||||
bot_command = command_name
|
bot_command = command_name
|
||||||
if command_arg:
|
if command_arg:
|
||||||
bot_command += ' {}'.format(command_arg)
|
bot_command += " {}".format(command_arg)
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
with self.mock_http_conversation('{}_error'.format(command_name)):
|
with self.mock_http_conversation("{}_error".format(command_name)):
|
||||||
self.verify_reply(bot_command, 'Something went wrong.')
|
self.verify_reply(bot_command, "Something went wrong.")
|
||||||
|
|
||||||
|
|
||||||
class TestFrontBotWrongTopic(BotTestCase, DefaultTests):
|
class TestFrontBotWrongTopic(BotTestCase, DefaultTests):
|
||||||
bot_name = 'front'
|
bot_name = "front"
|
||||||
|
|
||||||
def make_request_message(self, content: str) -> Dict[str, Any]:
|
def make_request_message(self, content: str) -> Dict[str, Any]:
|
||||||
message = super().make_request_message(content)
|
message = super().make_request_message(content)
|
||||||
message['subject'] = "kqatm2"
|
message["subject"] = "kqatm2"
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def test_bot_responds_to_empty_message(self) -> None:
|
def test_bot_responds_to_empty_message(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_no_conversation_id(self) -> None:
|
def test_no_conversation_id(self) -> None:
|
||||||
with self.mock_config_info({'api_key': "TEST"}):
|
with self.mock_config_info({"api_key": "TEST"}):
|
||||||
self.verify_reply(
|
self.verify_reply(
|
||||||
'archive',
|
"archive",
|
||||||
"No coversation ID found. Please make "
|
"No coversation ID found. Please make "
|
||||||
"sure that the name of the topic "
|
"sure that the name of the topic "
|
||||||
"contains a valid coversation ID.",
|
"contains a valid coversation ID.",
|
||||||
|
|
|
@ -4,54 +4,54 @@ from zulip_bots.game_handler import BadMoveException, GameAdapter
|
||||||
|
|
||||||
|
|
||||||
class GameHandlerBotMessageHandler:
|
class GameHandlerBotMessageHandler:
|
||||||
tokens = [':blue_circle:', ':red_circle:']
|
tokens = [":blue_circle:", ":red_circle:"]
|
||||||
|
|
||||||
def parse_board(self, board: Any) -> str:
|
def parse_board(self, board: Any) -> str:
|
||||||
return 'foo'
|
return "foo"
|
||||||
|
|
||||||
def get_player_color(self, turn: int) -> str:
|
def get_player_color(self, turn: int) -> str:
|
||||||
return self.tokens[turn]
|
return self.tokens[turn]
|
||||||
|
|
||||||
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
||||||
column_number = move_info.replace('move ', '')
|
column_number = move_info.replace("move ", "")
|
||||||
return original_player + ' moved in column ' + column_number
|
return original_player + " moved in column " + column_number
|
||||||
|
|
||||||
def game_start_message(self) -> str:
|
def game_start_message(self) -> str:
|
||||||
return 'Type `move <column>` to place a token.\n \
|
return "Type `move <column>` to place a token.\n \
|
||||||
The first player to get 4 in a row wins!\n \
|
The first player to get 4 in a row wins!\n \
|
||||||
Good Luck!'
|
Good Luck!"
|
||||||
|
|
||||||
|
|
||||||
class MockModel:
|
class MockModel:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.current_board = 'mock board'
|
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 not is_computer:
|
||||||
if int(move.replace('move ', '')) < 9:
|
if int(move.replace("move ", "")) < 9:
|
||||||
return 'mock board'
|
return "mock board"
|
||||||
else:
|
else:
|
||||||
raise BadMoveException('Invalid Move.')
|
raise BadMoveException("Invalid Move.")
|
||||||
return 'mock board'
|
return "mock board"
|
||||||
|
|
||||||
def determine_game_over(self, players: List[str]) -> None:
|
def determine_game_over(self, players: List[str]) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class GameHandlerBotHandler(GameAdapter):
|
class GameHandlerBotHandler(GameAdapter):
|
||||||
'''
|
"""
|
||||||
DO NOT USE THIS BOT
|
DO NOT USE THIS BOT
|
||||||
This bot is used to test game_handler.py
|
This bot is used to test game_handler.py
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
game_name = 'foo test game'
|
game_name = "foo test game"
|
||||||
bot_name = 'game_handler_bot'
|
bot_name = "game_handler_bot"
|
||||||
move_help_message = '* To make your move during a game, type\n```move <column-number>```'
|
move_help_message = "* To make your move during a game, type\n```move <column-number>```"
|
||||||
move_regex = r'move (\d)$'
|
move_regex = r"move (\d)$"
|
||||||
model = MockModel
|
model = MockModel
|
||||||
gameMessageHandler = GameHandlerBotMessageHandler
|
gameMessageHandler = GameHandlerBotMessageHandler
|
||||||
rules = ''
|
rules = ""
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
game_name,
|
game_name,
|
||||||
|
|
|
@ -7,16 +7,16 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
||||||
|
|
||||||
|
|
||||||
class TestGameHandlerBot(BotTestCase, DefaultTests):
|
class TestGameHandlerBot(BotTestCase, DefaultTests):
|
||||||
bot_name = 'game_handler_bot'
|
bot_name = "game_handler_bot"
|
||||||
|
|
||||||
def make_request_message(
|
def make_request_message(
|
||||||
self,
|
self,
|
||||||
content: str,
|
content: str,
|
||||||
user: str = 'foo@example.com',
|
user: str = "foo@example.com",
|
||||||
user_name: str = 'foo',
|
user_name: str = "foo",
|
||||||
type: str = 'private',
|
type: str = "private",
|
||||||
stream: str = '',
|
stream: str = "",
|
||||||
subject: str = '',
|
subject: str = "",
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
message = dict(
|
message = dict(
|
||||||
sender_email=user,
|
sender_email=user,
|
||||||
|
@ -35,58 +35,58 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
||||||
expected_response: str,
|
expected_response: str,
|
||||||
response_number: int,
|
response_number: int,
|
||||||
bot: Any = None,
|
bot: Any = None,
|
||||||
user_name: str = 'foo',
|
user_name: str = "foo",
|
||||||
stream: str = '',
|
stream: str = "",
|
||||||
subject: str = '',
|
subject: str = "",
|
||||||
max_messages: int = 20,
|
max_messages: int = 20,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
"""
|
||||||
This function serves a similar purpose
|
This function serves a similar purpose
|
||||||
to BotTestCase.verify_dialog, but allows
|
to BotTestCase.verify_dialog, but allows
|
||||||
for multiple responses to be validated,
|
for multiple responses to be validated,
|
||||||
and for mocking of the bot's internal data
|
and for mocking of the bot's internal data
|
||||||
'''
|
"""
|
||||||
if bot is None:
|
if bot is None:
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
else:
|
else:
|
||||||
_b, bot_handler = self._get_handlers()
|
_b, bot_handler = self._get_handlers()
|
||||||
type = 'private' if stream == '' else 'stream'
|
type = "private" if stream == "" else "stream"
|
||||||
message = self.make_request_message(
|
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_handler.reset_transcript()
|
||||||
bot.handle_message(message, bot_handler)
|
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]
|
first_response = responses[response_number]
|
||||||
self.assertEqual(expected_response, first_response['content'])
|
self.assertEqual(expected_response, first_response["content"])
|
||||||
self.assertLessEqual(len(responses), max_messages)
|
self.assertLessEqual(len(responses), max_messages)
|
||||||
|
|
||||||
def add_user_to_cache(self, name: str, bot: Any = None) -> Any:
|
def add_user_to_cache(self, name: str, bot: Any = None) -> Any:
|
||||||
if bot is None:
|
if bot is None:
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
message = {
|
message = {
|
||||||
'sender_email': '{}@example.com'.format(name),
|
"sender_email": "{}@example.com".format(name),
|
||||||
'sender_full_name': '{}'.format(name),
|
"sender_full_name": "{}".format(name),
|
||||||
}
|
}
|
||||||
bot.add_user_to_cache(message)
|
bot.add_user_to_cache(message)
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
def setup_game(
|
def setup_game(
|
||||||
self,
|
self,
|
||||||
id: str = '',
|
id: str = "",
|
||||||
bot: Any = None,
|
bot: Any = None,
|
||||||
players: List[str] = ['foo', 'baz'],
|
players: List[str] = ["foo", "baz"],
|
||||||
subject: str = 'test game',
|
subject: str = "test game",
|
||||||
stream: str = 'test',
|
stream: str = "test",
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if bot is None:
|
if bot is None:
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
for p in players:
|
for p in players:
|
||||||
self.add_user_to_cache(p, bot)
|
self.add_user_to_cache(p, bot)
|
||||||
players_emails = [p + '@example.com' for p in players]
|
players_emails = [p + "@example.com" for p in players]
|
||||||
game_id = 'abc123'
|
game_id = "abc123"
|
||||||
if id != '':
|
if id != "":
|
||||||
game_id = 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})
|
bot.instances.update({game_id: instance})
|
||||||
|
@ -95,18 +95,18 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
def setup_computer_game(self) -> Any:
|
def setup_computer_game(self) -> Any:
|
||||||
bot = self.add_user_to_cache('foo')
|
bot = self.add_user_to_cache("foo")
|
||||||
bot.email = 'test-bot@example.com'
|
bot.email = "test-bot@example.com"
|
||||||
self.add_user_to_cache('test-bot', bot)
|
self.add_user_to_cache("test-bot", bot)
|
||||||
instance = GameInstance(
|
instance = GameInstance(
|
||||||
bot, False, 'test game', 'abc123', ['foo@example.com', 'test-bot@example.com'], 'test'
|
bot, False, "test game", "abc123", ["foo@example.com", "test-bot@example.com"], "test"
|
||||||
)
|
)
|
||||||
bot.instances.update({'abc123': instance})
|
bot.instances.update({"abc123": instance})
|
||||||
instance.start()
|
instance.start()
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
def help_message(self) -> str:
|
def help_message(self) -> str:
|
||||||
return '''** foo test game Bot Help:**
|
return """** foo test game Bot Help:**
|
||||||
*Preface all commands with @**test-bot***
|
*Preface all commands with @**test-bot***
|
||||||
* To start a game in a stream (*recommended*), type
|
* To start a game in a stream (*recommended*), type
|
||||||
`start game`
|
`start game`
|
||||||
|
@ -129,319 +129,319 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
||||||
* To see rules of this game, type
|
* To see rules of this game, type
|
||||||
`rules`
|
`rules`
|
||||||
* To make your move during a game, type
|
* To make your move during a game, type
|
||||||
```move <column-number>```'''
|
```move <column-number>```"""
|
||||||
|
|
||||||
def test_help_message(self) -> None:
|
def test_help_message(self) -> None:
|
||||||
self.verify_response('help', self.help_message(), 0)
|
self.verify_response("help", self.help_message(), 0)
|
||||||
self.verify_response('foo bar baz', self.help_message(), 0)
|
self.verify_response("foo bar baz", self.help_message(), 0)
|
||||||
|
|
||||||
def test_exception_handling(self) -> None:
|
def test_exception_handling(self) -> None:
|
||||||
with patch('logging.exception'), patch(
|
with patch("logging.exception"), patch(
|
||||||
'zulip_bots.game_handler.GameAdapter.command_quit', side_effect=Exception
|
"zulip_bots.game_handler.GameAdapter.command_quit", side_effect=Exception
|
||||||
):
|
):
|
||||||
self.verify_response('quit', 'Error .', 0)
|
self.verify_response("quit", "Error .", 0)
|
||||||
|
|
||||||
def test_not_in_game_messages(self) -> None:
|
def test_not_in_game_messages(self) -> None:
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'You are not in a game at the moment. Type `help` for help.',
|
"You are not in a game at the moment. Type `help` for help.",
|
||||||
0,
|
0,
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
self.verify_response(
|
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:
|
def test_start_game_with_name(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with @**baz**',
|
"start game with @**baz**",
|
||||||
'You\'ve sent an invitation to play foo test game with @**baz**',
|
"You've sent an invitation to play foo test game with @**baz**",
|
||||||
1,
|
1,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
)
|
)
|
||||||
self.assertEqual(len(bot.invites), 1)
|
self.assertEqual(len(bot.invites), 1)
|
||||||
|
|
||||||
def test_start_game_with_email(self) -> None:
|
def test_start_game_with_email(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with baz@example.com',
|
"start game with baz@example.com",
|
||||||
'You\'ve sent an invitation to play foo test game with @**baz**',
|
"You've sent an invitation to play foo test game with @**baz**",
|
||||||
1,
|
1,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
)
|
)
|
||||||
self.assertEqual(len(bot.invites), 1)
|
self.assertEqual(len(bot.invites), 1)
|
||||||
|
|
||||||
def test_join_game_and_start_in_stream(self) -> None:
|
def test_join_game_and_start_in_stream(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
self.add_user_to_cache('foo', bot)
|
self.add_user_to_cache("foo", bot)
|
||||||
bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}}
|
bot.invites = {"abc": {"stream": "test", "subject": "test game", "host": "foo@example.com"}}
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'join',
|
"join",
|
||||||
'@**baz** has joined the game',
|
"@**baz** has joined the game",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
user_name='baz',
|
user_name="baz",
|
||||||
)
|
)
|
||||||
self.assertEqual(len(bot.instances.keys()), 1)
|
self.assertEqual(len(bot.instances.keys()), 1)
|
||||||
|
|
||||||
def test_start_game_in_stream(self) -> None:
|
def test_start_game_in_stream(self) -> None:
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game',
|
"start game",
|
||||||
'**foo** wants to play **foo test game**. Type @**test-bot** join to play them!',
|
"**foo** wants to play **foo test game**. Type @**test-bot** join to play them!",
|
||||||
0,
|
0,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_start_invite_game_in_stream(self) -> None:
|
def test_start_invite_game_in_stream(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with @**baz**',
|
"start game with @**baz**",
|
||||||
'If you were invited, and you\'re here, type "@**test-bot** accept" to accept the invite!',
|
'If you were invited, and you\'re here, type "@**test-bot** accept" to accept the invite!',
|
||||||
2,
|
2,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='game test',
|
subject="game test",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_join_no_game(self) -> None:
|
def test_join_no_game(self) -> None:
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'join',
|
"join",
|
||||||
'There is not a game in this subject. Type `help` for all commands.',
|
"There is not a game in this subject. Type `help` for all commands.",
|
||||||
0,
|
0,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
user_name='baz',
|
user_name="baz",
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_accept_invitation(self) -> None:
|
def test_accept_invitation(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
self.add_user_to_cache('foo', bot)
|
self.add_user_to_cache("foo", bot)
|
||||||
bot.invites = {
|
bot.invites = {
|
||||||
'abc': {
|
"abc": {
|
||||||
'subject': '###private###',
|
"subject": "###private###",
|
||||||
'stream': 'games',
|
"stream": "games",
|
||||||
'host': 'foo@example.com',
|
"host": "foo@example.com",
|
||||||
'baz@example.com': 'p',
|
"baz@example.com": "p",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.verify_response(
|
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:
|
def test_decline_invitation(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
self.add_user_to_cache('foo', bot)
|
self.add_user_to_cache("foo", bot)
|
||||||
bot.invites = {
|
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(
|
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:
|
def test_quit_invite(self) -> None:
|
||||||
bot = self.add_user_to_cache('foo')
|
bot = self.add_user_to_cache("foo")
|
||||||
bot.invites = {'abc': {'subject': '###private###', 'host': 'foo@example.com'}}
|
bot.invites = {"abc": {"subject": "###private###", "host": "foo@example.com"}}
|
||||||
self.verify_response('quit', 'Game cancelled.\n**foo** quit.', 0, bot, 'foo')
|
self.verify_response("quit", "Game cancelled.\n**foo** quit.", 0, bot, "foo")
|
||||||
|
|
||||||
def test_user_already_in_game_errors(self) -> None:
|
def test_user_already_in_game_errors(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with @**baz**',
|
"start game with @**baz**",
|
||||||
'You are already in a game. Type `quit` to leave.',
|
"You are already in a game. Type `quit` to leave.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game',
|
"start game",
|
||||||
'You are already in a game. Type `quit` to leave.',
|
"You are already in a game. Type `quit` to leave.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'accept', '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(
|
self.verify_response(
|
||||||
'decline',
|
"decline",
|
||||||
'You are already in a game. Type `quit` to leave.',
|
"You are already in a game. Type `quit` to leave.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'join', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1
|
"join", "You are already in a game. Type `quit` to leave.", 0, bot=bot, max_messages=1
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_register_command(self) -> None:
|
def test_register_command(self) -> None:
|
||||||
bot = self.add_user_to_cache('foo')
|
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())
|
self.assertIn("foo@example.com", bot.user_cache.keys())
|
||||||
|
|
||||||
def test_no_active_invite_errors(self) -> None:
|
def test_no_active_invite_errors(self) -> None:
|
||||||
self.verify_response('accept', '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)
|
self.verify_response("decline", "No active invites. Type `help` for commands.", 0)
|
||||||
|
|
||||||
def test_wrong_number_of_players_message(self) -> None:
|
def test_wrong_number_of_players_message(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
bot.min_players = 5
|
bot.min_players = 5
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with @**baz**',
|
"start game with @**baz**",
|
||||||
'You must have at least 5 players to play.\nGame cancelled.',
|
"You must have at least 5 players to play.\nGame cancelled.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
)
|
)
|
||||||
bot.min_players = 2
|
bot.min_players = 2
|
||||||
bot.max_players = 1
|
bot.max_players = 1
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with @**baz**',
|
"start game with @**baz**",
|
||||||
'The maximum number of players for this game is 1.',
|
"The maximum number of players for this game is 1.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
)
|
)
|
||||||
bot.max_players = 1
|
bot.max_players = 1
|
||||||
bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}}
|
bot.invites = {"abc": {"stream": "test", "subject": "test game", "host": "foo@example.com"}}
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'join',
|
"join",
|
||||||
'This game is full.',
|
"This game is full.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
user_name='baz',
|
user_name="baz",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_public_accept(self) -> None:
|
def test_public_accept(self) -> None:
|
||||||
bot = self.add_user_to_cache('baz')
|
bot = self.add_user_to_cache("baz")
|
||||||
self.add_user_to_cache('foo', bot)
|
self.add_user_to_cache("foo", bot)
|
||||||
bot.invites = {
|
bot.invites = {
|
||||||
'abc': {
|
"abc": {
|
||||||
'stream': 'test',
|
"stream": "test",
|
||||||
'subject': 'test game',
|
"subject": "test game",
|
||||||
'host': 'baz@example.com',
|
"host": "baz@example.com",
|
||||||
'foo@example.com': 'p',
|
"foo@example.com": "p",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'accept',
|
"accept",
|
||||||
'@**foo** has accepted the invitation.',
|
"@**foo** has accepted the invitation.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_start_game_with_computer(self) -> None:
|
def test_start_game_with_computer(self) -> None:
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with @**test-bot**',
|
"start game with @**test-bot**",
|
||||||
'Wait... That\'s me!',
|
"Wait... That's me!",
|
||||||
4,
|
4,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_sent_by_bot(self) -> None:
|
def test_sent_by_bot(self) -> None:
|
||||||
with self.assertRaises(IndexError):
|
with self.assertRaises(IndexError):
|
||||||
self.verify_response(
|
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:
|
def test_forfeit(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'forfeit', '**foo** forfeited!', 0, bot=bot, stream='test', subject='test game'
|
"forfeit", "**foo** forfeited!", 0, bot=bot, stream="test", subject="test game"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_draw(self) -> None:
|
def test_draw(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'draw',
|
"draw",
|
||||||
'**foo** has voted for a draw!\nType `draw` to accept',
|
"**foo** has voted for a draw!\nType `draw` to accept",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'draw',
|
"draw",
|
||||||
'It was a draw!',
|
"It was a draw!",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
user_name='baz',
|
user_name="baz",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_normal_turns(self) -> None:
|
def test_normal_turns(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'**foo** moved in column 3\n\nfoo\n\nIt\'s **baz**\'s (:red_circle:) turn.',
|
"**foo** moved in column 3\n\nfoo\n\nIt's **baz**'s (:red_circle:) turn.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'**baz** moved in column 3\n\nfoo\n\nIt\'s **foo**\'s (:blue_circle:) turn.',
|
"**baz** moved in column 3\n\nfoo\n\nIt's **foo**'s (:blue_circle:) turn.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
user_name='baz',
|
user_name="baz",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_wrong_turn(self) -> None:
|
def test_wrong_turn(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 5',
|
"move 5",
|
||||||
'It\'s **foo**\'s (:blue_circle:) turn.',
|
"It's **foo**'s (:blue_circle:) turn.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
user_name='baz',
|
user_name="baz",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_private_message_error(self) -> None:
|
def test_private_message_error(self) -> None:
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game',
|
"start game",
|
||||||
'If you are starting a game in private messages, you must invite players. Type `help` for commands.',
|
"If you are starting a game in private messages, you must invite players. Type `help` for commands.",
|
||||||
0,
|
0,
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
bot = self.add_user_to_cache('bar')
|
bot = self.add_user_to_cache("bar")
|
||||||
bot.invites = {
|
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(
|
self.verify_response(
|
||||||
'join',
|
"join",
|
||||||
'You cannot join games in private messages. Type `help` for all commands.',
|
"You cannot join games in private messages. Type `help` for all commands.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_game_already_in_subject(self) -> None:
|
def test_game_already_in_subject(self) -> None:
|
||||||
bot = self.add_user_to_cache('foo')
|
bot = self.add_user_to_cache("foo")
|
||||||
bot.invites = {
|
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(
|
self.verify_response(
|
||||||
'start game',
|
"start game",
|
||||||
'There is already a game in this stream.',
|
"There is already a game in this stream.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
user_name='baz',
|
user_name="baz",
|
||||||
max_messages=1,
|
max_messages=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -452,219 +452,219 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
||||||
|
|
||||||
def test_unknown_user(self) -> None:
|
def test_unknown_user(self) -> None:
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with @**bar**',
|
"start game with @**bar**",
|
||||||
'I don\'t know @**bar**. Tell them to say @**test-bot** register',
|
"I don't know @**bar**. Tell them to say @**test-bot** register",
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'start game with bar@example.com',
|
"start game with bar@example.com",
|
||||||
'I don\'t know bar@example.com. Tell them to use @**test-bot** register',
|
"I don't know bar@example.com. Tell them to use @**test-bot** register",
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_is_user_not_player(self) -> None:
|
def test_is_user_not_player(self) -> None:
|
||||||
bot = self.add_user_to_cache('foo')
|
bot = self.add_user_to_cache("foo")
|
||||||
self.add_user_to_cache('baz', bot)
|
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("foo@example.com"))
|
||||||
self.assertFalse(bot.is_user_not_player('baz@example.com'))
|
self.assertFalse(bot.is_user_not_player("baz@example.com"))
|
||||||
|
|
||||||
def test_move_help_message(self) -> None:
|
def test_move_help_message(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 123',
|
"move 123",
|
||||||
'* To make your move during a game, type\n```move <column-number>```',
|
"* To make your move during a game, type\n```move <column-number>```",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_move_message(self) -> None:
|
def test_invalid_move_message(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 9',
|
"move 9",
|
||||||
'Invalid Move.',
|
"Invalid Move.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
max_messages=2,
|
max_messages=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_game_id_by_email(self) -> None:
|
def test_get_game_id_by_email(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.assertEqual(bot.get_game_id_by_email('foo@example.com'), 'abc123')
|
self.assertEqual(bot.get_game_id_by_email("foo@example.com"), "abc123")
|
||||||
|
|
||||||
def test_game_over_and_leaderboard(self) -> None:
|
def test_game_over_and_leaderboard(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
bot.put_user_cache()
|
bot.put_user_cache()
|
||||||
with patch(
|
with patch(
|
||||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||||
return_value='foo@example.com',
|
return_value="foo@example.com",
|
||||||
):
|
):
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game'
|
"move 3", "**foo** won! :tada:", 1, bot=bot, stream="test", subject="test game"
|
||||||
)
|
)
|
||||||
leaderboard = '**Most wins**\n\n\
|
leaderboard = "**Most wins**\n\n\
|
||||||
Player | Games Won | Games Drawn | Games Lost | Total Games\n\
|
Player | Games Won | Games Drawn | Games Lost | Total Games\n\
|
||||||
--- | --- | --- | --- | --- \n\
|
--- | --- | --- | --- | --- \n\
|
||||||
**foo** | 1 | 0 | 0 | 1\n\
|
**foo** | 1 | 0 | 0 | 1\n\
|
||||||
**baz** | 0 | 0 | 1 | 1\n\
|
**baz** | 0 | 0 | 1 | 1\n\
|
||||||
**test-bot** | 0 | 0 | 0 | 0'
|
**test-bot** | 0 | 0 | 0 | 0"
|
||||||
self.verify_response('leaderboard', leaderboard, 0, bot=bot)
|
self.verify_response("leaderboard", leaderboard, 0, bot=bot)
|
||||||
|
|
||||||
def test_current_turn_winner(self) -> None:
|
def test_current_turn_winner(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
with patch(
|
with patch(
|
||||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||||
return_value='current turn',
|
return_value="current turn",
|
||||||
):
|
):
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game'
|
"move 3", "**foo** won! :tada:", 1, bot=bot, stream="test", subject="test game"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_computer_turn(self) -> None:
|
def test_computer_turn(self) -> None:
|
||||||
bot = self.setup_computer_game()
|
bot = self.setup_computer_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'**foo** moved in column 3\n\nfoo\n\nIt\'s **test-bot**\'s (:red_circle:) turn.',
|
"**foo** moved in column 3\n\nfoo\n\nIt's **test-bot**'s (:red_circle:) turn.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||||
return_value='test-bot@example.com',
|
return_value="test-bot@example.com",
|
||||||
):
|
):
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 5', 'I won! Well Played!', 2, bot=bot, stream='test', subject='test game'
|
"move 5", "I won! Well Played!", 2, bot=bot, stream="test", subject="test game"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_computer_endgame_responses(self) -> None:
|
def test_computer_endgame_responses(self) -> None:
|
||||||
bot = self.setup_computer_game()
|
bot = self.setup_computer_game()
|
||||||
with patch(
|
with patch(
|
||||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||||
return_value='foo@example.com',
|
return_value="foo@example.com",
|
||||||
):
|
):
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 5', 'You won! Nice!', 2, bot=bot, stream='test', subject='test game'
|
"move 5", "You won! Nice!", 2, bot=bot, stream="test", subject="test game"
|
||||||
)
|
)
|
||||||
bot = self.setup_computer_game()
|
bot = self.setup_computer_game()
|
||||||
with patch(
|
with patch(
|
||||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||||
return_value='draw',
|
return_value="draw",
|
||||||
):
|
):
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 5',
|
"move 5",
|
||||||
'It was a draw! Well Played!',
|
"It was a draw! Well Played!",
|
||||||
2,
|
2,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test',
|
stream="test",
|
||||||
subject='test game',
|
subject="test game",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_add_user_statistics(self) -> None:
|
def test_add_user_statistics(self) -> None:
|
||||||
bot = self.add_user_to_cache('foo')
|
bot = self.add_user_to_cache("foo")
|
||||||
bot.add_user_statistics('foo@example.com', {'foo': 3})
|
bot.add_user_statistics("foo@example.com", {"foo": 3})
|
||||||
self.assertEqual(bot.user_cache['foo@example.com']['stats']['foo'], 3)
|
self.assertEqual(bot.user_cache["foo@example.com"]["stats"]["foo"], 3)
|
||||||
|
|
||||||
def test_get_players(self) -> None:
|
def test_get_players(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
players = bot.get_players('abc123')
|
players = bot.get_players("abc123")
|
||||||
self.assertEqual(players, ['foo@example.com', 'baz@example.com'])
|
self.assertEqual(players, ["foo@example.com", "baz@example.com"])
|
||||||
|
|
||||||
def test_none_function_responses(self) -> None:
|
def test_none_function_responses(self) -> None:
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
self.assertEqual(bot.get_players('abc'), [])
|
self.assertEqual(bot.get_players("abc"), [])
|
||||||
self.assertEqual(bot.get_user_by_name('no one'), {})
|
self.assertEqual(bot.get_user_by_name("no one"), {})
|
||||||
self.assertEqual(bot.get_user_by_email('no one'), {})
|
self.assertEqual(bot.get_user_by_email("no one"), {})
|
||||||
|
|
||||||
def test_get_game_info(self) -> None:
|
def test_get_game_info(self) -> None:
|
||||||
bot = self.add_user_to_cache('foo')
|
bot = self.add_user_to_cache("foo")
|
||||||
self.add_user_to_cache('baz', bot)
|
self.add_user_to_cache("baz", bot)
|
||||||
bot.invites = {
|
bot.invites = {
|
||||||
'abcdefg': {
|
"abcdefg": {
|
||||||
'host': 'foo@example.com',
|
"host": "foo@example.com",
|
||||||
'baz@example.com': 'a',
|
"baz@example.com": "a",
|
||||||
'stream': 'test',
|
"stream": "test",
|
||||||
'subject': 'test game',
|
"subject": "test game",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bot.get_game_info('abcdefg'),
|
bot.get_game_info("abcdefg"),
|
||||||
{
|
{
|
||||||
'game_id': 'abcdefg',
|
"game_id": "abcdefg",
|
||||||
'type': 'invite',
|
"type": "invite",
|
||||||
'stream': 'test',
|
"stream": "test",
|
||||||
'subject': 'test game',
|
"subject": "test game",
|
||||||
'players': ['foo@example.com', 'baz@example.com'],
|
"players": ["foo@example.com", "baz@example.com"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_parse_message(self) -> None:
|
def test_parse_message(self) -> None:
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'Join your game using the link below!\n\n> **Game `abc123`**\n\
|
"Join your game using the link below!\n\n> **Game `abc123`**\n\
|
||||||
> foo test game\n\
|
> foo test game\n\
|
||||||
> 2/2 players\n\
|
> 2/2 players\n\
|
||||||
> **[Join Game](/#narrow/stream/test/topic/test game)**',
|
> **[Join Game](/#narrow/stream/test/topic/test game)**",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
)
|
)
|
||||||
bot = self.setup_game()
|
bot = self.setup_game()
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'''Your current game is not in this subject. \n\
|
"""Your current game is not in this subject. \n\
|
||||||
To move subjects, send your message again, otherwise join the game using the link below.
|
To move subjects, send your message again, otherwise join the game using the link below.
|
||||||
|
|
||||||
> **Game `abc123`**
|
> **Game `abc123`**
|
||||||
> foo test game
|
> foo test game
|
||||||
> 2/2 players
|
> 2/2 players
|
||||||
> **[Join Game](/#narrow/stream/test/topic/test game)**''',
|
> **[Join Game](/#narrow/stream/test/topic/test game)**""",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
stream='test 2',
|
stream="test 2",
|
||||||
subject='game 2',
|
subject="game 2",
|
||||||
)
|
)
|
||||||
self.verify_response('move 3', 'foo', 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:
|
def test_change_game_subject(self) -> None:
|
||||||
bot = self.setup_game('abc123')
|
bot = self.setup_game("abc123")
|
||||||
self.setup_game('abcdefg', bot, ['bar', 'abc'], 'test game 2', 'test2')
|
self.setup_game("abcdefg", bot, ["bar", "abc"], "test game 2", "test2")
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'''Your current game is not in this subject. \n\
|
"""Your current game is not in this subject. \n\
|
||||||
To move subjects, send your message again, otherwise join the game using the link below.
|
To move subjects, send your message again, otherwise join the game using the link below.
|
||||||
|
|
||||||
> **Game `abcdefg`**
|
> **Game `abcdefg`**
|
||||||
> foo test game
|
> foo test game
|
||||||
> 2/2 players
|
> 2/2 players
|
||||||
> **[Join Game](/#narrow/stream/test2/topic/test game 2)**''',
|
> **[Join Game](/#narrow/stream/test2/topic/test game 2)**""",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
user_name='bar',
|
user_name="bar",
|
||||||
stream='test game',
|
stream="test game",
|
||||||
subject='test2',
|
subject="test2",
|
||||||
)
|
)
|
||||||
self.verify_response(
|
self.verify_response(
|
||||||
'move 3',
|
"move 3",
|
||||||
'There is already a game in this subject.',
|
"There is already a game in this subject.",
|
||||||
0,
|
0,
|
||||||
bot=bot,
|
bot=bot,
|
||||||
user_name='bar',
|
user_name="bar",
|
||||||
stream='test game',
|
stream="test game",
|
||||||
subject='test',
|
subject="test",
|
||||||
)
|
)
|
||||||
bot.invites = {
|
bot.invites = {
|
||||||
'foo bar baz': {
|
"foo bar baz": {
|
||||||
'host': 'foo@example.com',
|
"host": "foo@example.com",
|
||||||
'baz@example.com': 'a',
|
"baz@example.com": "a",
|
||||||
'stream': 'test',
|
"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')
|
self.assertEqual(bot.invites["foo bar baz"]["stream"], "test2")
|
||||||
|
|
|
@ -31,8 +31,8 @@ class GameOfFifteenModel:
|
||||||
|
|
||||||
def determine_game_over(self, players: List[str]) -> str:
|
def determine_game_over(self, players: List[str]) -> str:
|
||||||
if self.won(self.current_board):
|
if self.won(self.current_board):
|
||||||
return 'current turn'
|
return "current turn"
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def won(self, board: Any) -> bool:
|
def won(self, board: Any) -> bool:
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
|
@ -52,16 +52,16 @@ class GameOfFifteenModel:
|
||||||
def make_move(self, move: str, player_number: int, computer_move: bool = False) -> Any:
|
def make_move(self, move: str, player_number: int, computer_move: bool = False) -> Any:
|
||||||
board = self.current_board
|
board = self.current_board
|
||||||
move = move.strip()
|
move = move.strip()
|
||||||
move = move.split(' ')
|
move = move.split(" ")
|
||||||
|
|
||||||
if '' in move:
|
if "" in move:
|
||||||
raise BadMoveException('You should enter space separated digits.')
|
raise BadMoveException("You should enter space separated digits.")
|
||||||
moves = len(move)
|
moves = len(move)
|
||||||
for m in range(1, moves):
|
for m in range(1, moves):
|
||||||
tile = int(move[m])
|
tile = int(move[m])
|
||||||
coordinates = self.get_coordinates(board)
|
coordinates = self.get_coordinates(board)
|
||||||
if tile not in coordinates:
|
if tile not in coordinates:
|
||||||
raise BadMoveException('You can only move tiles which exist in the board.')
|
raise BadMoveException("You can only move tiles which exist in the board.")
|
||||||
i, j = coordinates[tile]
|
i, j = coordinates[tile]
|
||||||
if (j - 1) > -1 and board[i][j - 1] == 0:
|
if (j - 1) > -1 and board[i][j - 1] == 0:
|
||||||
board[i][j - 1] = tile
|
board[i][j - 1] = tile
|
||||||
|
@ -77,7 +77,7 @@ class GameOfFifteenModel:
|
||||||
board[i][j] = 0
|
board[i][j] = 0
|
||||||
else:
|
else:
|
||||||
raise BadMoveException(
|
raise BadMoveException(
|
||||||
'You can only move tiles which are adjacent to :grey_question:.'
|
"You can only move tiles which are adjacent to :grey_question:."
|
||||||
)
|
)
|
||||||
if m == moves - 1:
|
if m == moves - 1:
|
||||||
return board
|
return board
|
||||||
|
@ -86,30 +86,30 @@ class GameOfFifteenModel:
|
||||||
class GameOfFifteenMessageHandler:
|
class GameOfFifteenMessageHandler:
|
||||||
|
|
||||||
tiles = {
|
tiles = {
|
||||||
'0': ':grey_question:',
|
"0": ":grey_question:",
|
||||||
'1': ':one:',
|
"1": ":one:",
|
||||||
'2': ':two:',
|
"2": ":two:",
|
||||||
'3': ':three:',
|
"3": ":three:",
|
||||||
'4': ':four:',
|
"4": ":four:",
|
||||||
'5': ':five:',
|
"5": ":five:",
|
||||||
'6': ':six:',
|
"6": ":six:",
|
||||||
'7': ':seven:',
|
"7": ":seven:",
|
||||||
'8': ':eight:',
|
"8": ":eight:",
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_board(self, board: Any) -> str:
|
def parse_board(self, board: Any) -> str:
|
||||||
# Header for the top of the board
|
# Header for the top of the board
|
||||||
board_str = ''
|
board_str = ""
|
||||||
|
|
||||||
for row in range(3):
|
for row in range(3):
|
||||||
board_str += '\n\n'
|
board_str += "\n\n"
|
||||||
for column in range(3):
|
for column in range(3):
|
||||||
board_str += self.tiles[str(board[row][column])]
|
board_str += self.tiles[str(board[row][column])]
|
||||||
return board_str
|
return board_str
|
||||||
|
|
||||||
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
||||||
tile = move_info.replace('move ', '')
|
tile = move_info.replace("move ", "")
|
||||||
return original_player + ' moved ' + tile
|
return original_player + " moved " + tile
|
||||||
|
|
||||||
def game_start_message(self) -> str:
|
def game_start_message(self) -> str:
|
||||||
return (
|
return (
|
||||||
|
@ -119,23 +119,23 @@ class GameOfFifteenMessageHandler:
|
||||||
|
|
||||||
|
|
||||||
class GameOfFifteenBotHandler(GameAdapter):
|
class GameOfFifteenBotHandler(GameAdapter):
|
||||||
'''
|
"""
|
||||||
Bot that uses the Game Adapter class
|
Bot that uses the Game Adapter class
|
||||||
to allow users to play Game of Fifteen
|
to allow users to play Game of Fifteen
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
game_name = 'Game of Fifteen'
|
game_name = "Game of Fifteen"
|
||||||
bot_name = 'Game of Fifteen'
|
bot_name = "Game of Fifteen"
|
||||||
move_help_message = (
|
move_help_message = (
|
||||||
'* To make your move during a game, type\n```move <tile1> <tile2> ...```'
|
"* To make your move during a game, type\n```move <tile1> <tile2> ...```"
|
||||||
)
|
)
|
||||||
move_regex = r'move [\d{1}\s]+$'
|
move_regex = r"move [\d{1}\s]+$"
|
||||||
model = GameOfFifteenModel
|
model = GameOfFifteenModel
|
||||||
gameMessageHandler = GameOfFifteenMessageHandler
|
gameMessageHandler = GameOfFifteenMessageHandler
|
||||||
rules = '''Arrange the board’s tiles from smallest to largest, left to right,
|
rules = """Arrange the board’s tiles from smallest to largest, left to right,
|
||||||
top to bottom, and tiles adjacent to :grey_question: can only be moved.
|
top to bottom, and tiles adjacent to :grey_question: can only be moved.
|
||||||
Final configuration will have :grey_question: in top left.'''
|
Final configuration will have :grey_question: in top left."""
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
game_name,
|
game_name,
|
||||||
|
|
|
@ -6,10 +6,10 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
||||||
|
|
||||||
|
|
||||||
class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||||
bot_name = 'game_of_fifteen'
|
bot_name = "game_of_fifteen"
|
||||||
|
|
||||||
def make_request_message(
|
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]:
|
) -> 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
|
return message
|
||||||
|
@ -20,14 +20,14 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||||
request: str,
|
request: str,
|
||||||
expected_response: str,
|
expected_response: str,
|
||||||
response_number: int,
|
response_number: int,
|
||||||
user: str = 'foo@example.com',
|
user: str = "foo@example.com",
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
"""
|
||||||
This function serves a similar purpose
|
This function serves a similar purpose
|
||||||
to BotTestCase.verify_dialog, but allows
|
to BotTestCase.verify_dialog, but allows
|
||||||
for multiple responses to be validated,
|
for multiple responses to be validated,
|
||||||
and for mocking of the bot's internal data
|
and for mocking of the bot's internal data
|
||||||
'''
|
"""
|
||||||
|
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
message = self.make_request_message(request, user)
|
message = self.make_request_message(request, user)
|
||||||
|
@ -38,10 +38,10 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||||
responses = [message for (method, message) in bot_handler.transcript]
|
responses = [message for (method, message) in bot_handler.transcript]
|
||||||
|
|
||||||
first_response = responses[response_number]
|
first_response = responses[response_number]
|
||||||
self.assertEqual(expected_response, first_response['content'])
|
self.assertEqual(expected_response, first_response["content"])
|
||||||
|
|
||||||
def help_message(self) -> str:
|
def help_message(self) -> str:
|
||||||
return '''** Game of Fifteen Bot Help:**
|
return """** Game of Fifteen Bot Help:**
|
||||||
*Preface all commands with @**test-bot***
|
*Preface all commands with @**test-bot***
|
||||||
* To start a game in a stream, type
|
* To start a game in a stream, type
|
||||||
`start game`
|
`start game`
|
||||||
|
@ -50,16 +50,16 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||||
* To see rules of this game, type
|
* To see rules of this game, type
|
||||||
`rules`
|
`rules`
|
||||||
* To make your move during a game, type
|
* To make your move during a game, type
|
||||||
```move <tile1> <tile2> ...```'''
|
```move <tile1> <tile2> ...```"""
|
||||||
|
|
||||||
def test_static_responses(self) -> None:
|
def test_static_responses(self) -> None:
|
||||||
self.verify_response('help', self.help_message(), 0)
|
self.verify_response("help", self.help_message(), 0)
|
||||||
|
|
||||||
def test_game_message_handler_responses(self) -> None:
|
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:'
|
board = "\n\n:grey_question::one::two:\n\n:three::four::five:\n\n:six::seven::eight:"
|
||||||
bot, bot_handler = self._get_handlers()
|
bot, bot_handler = self._get_handlers()
|
||||||
self.assertEqual(bot.gameMessageHandler.parse_board(self.winning_board), board)
|
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.alert_move_message("foo", "move 1"), "foo moved 1")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bot.gameMessageHandler.game_start_message(),
|
bot.gameMessageHandler.game_start_message(),
|
||||||
"Welcome to Game of Fifteen!"
|
"Welcome to Game of Fifteen!"
|
||||||
|
@ -86,13 +86,13 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||||
final_board: List[List[int]],
|
final_board: List[List[int]],
|
||||||
) -> None:
|
) -> None:
|
||||||
gameOfFifteenModel.update_board(initial_board)
|
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)
|
self.assertEqual(test_board, final_board)
|
||||||
|
|
||||||
def confirmGameOver(board: List[List[int]], result: str) -> None:
|
def confirmGameOver(board: List[List[int]], result: str) -> None:
|
||||||
gameOfFifteenModel.update_board(board)
|
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)
|
self.assertEqual(game_over, result)
|
||||||
|
|
||||||
|
@ -114,17 +114,17 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||||
confirmAvailableMoves([1, 2, 3, 4, 5, 6, 7, 8], [0, 9, -1], initial_board)
|
confirmAvailableMoves([1, 2, 3, 4, 5, 6, 7, 8], [0, 9, -1], initial_board)
|
||||||
|
|
||||||
# Test Move Logic
|
# 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:
|
# Test coordinates logic:
|
||||||
confirm_coordinates(
|
confirm_coordinates(
|
||||||
|
@ -143,16 +143,16 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test Game Over Logic:
|
# Test Game Over Logic:
|
||||||
confirmGameOver(winning_board, 'current turn')
|
confirmGameOver(winning_board, "current turn")
|
||||||
confirmGameOver(sample_board, '')
|
confirmGameOver(sample_board, "")
|
||||||
|
|
||||||
def test_invalid_moves(self) -> None:
|
def test_invalid_moves(self) -> None:
|
||||||
model = GameOfFifteenModel()
|
model = GameOfFifteenModel()
|
||||||
move1 = 'move 2'
|
move1 = "move 2"
|
||||||
move2 = 'move 5'
|
move2 = "move 5"
|
||||||
move3 = 'move 23'
|
move3 = "move 23"
|
||||||
move4 = 'move 0'
|
move4 = "move 0"
|
||||||
move5 = 'move 1 2'
|
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)
|
model.update_board(initial_board)
|
||||||
|
|
|
@ -7,8 +7,8 @@ from requests.exceptions import ConnectionError, HTTPError
|
||||||
from zulip_bots.custom_exceptions import ConfigValidationError
|
from zulip_bots.custom_exceptions import ConfigValidationError
|
||||||
from zulip_bots.lib import BotHandler
|
from zulip_bots.lib import BotHandler
|
||||||
|
|
||||||
GIPHY_TRANSLATE_API = 'http://api.giphy.com/v1/gifs/translate'
|
GIPHY_TRANSLATE_API = "http://api.giphy.com/v1/gifs/translate"
|
||||||
GIPHY_RANDOM_API = 'http://api.giphy.com/v1/gifs/random'
|
GIPHY_RANDOM_API = "http://api.giphy.com/v1/gifs/random"
|
||||||
|
|
||||||
|
|
||||||
class GiphyHandler:
|
class GiphyHandler:
|
||||||
|
@ -21,15 +21,15 @@ class GiphyHandler:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def usage(self) -> str:
|
def usage(self) -> str:
|
||||||
return '''
|
return """
|
||||||
This plugin allows users to post GIFs provided by Giphy.
|
This plugin allows users to post GIFs provided by Giphy.
|
||||||
Users should preface keywords with the Giphy-bot @mention.
|
Users should preface keywords with the Giphy-bot @mention.
|
||||||
The bot responds also to private messages.
|
The bot responds also to private messages.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_config(config_info: Dict[str, str]) -> None:
|
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:
|
try:
|
||||||
data = requests.get(GIPHY_TRANSLATE_API, params=query)
|
data = requests.get(GIPHY_TRANSLATE_API, params=query)
|
||||||
data.raise_for_status()
|
data.raise_for_status()
|
||||||
|
@ -39,13 +39,13 @@ class GiphyHandler:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
if data.status_code == 403:
|
if data.status_code == 403:
|
||||||
error_message += (
|
error_message += (
|
||||||
'This is likely due to an invalid key.\n'
|
"This is likely due to an invalid key.\n"
|
||||||
'Follow the instructions in doc.md for setting an API key.'
|
"Follow the instructions in doc.md for setting an API key."
|
||||||
)
|
)
|
||||||
raise ConfigValidationError(error_message)
|
raise ConfigValidationError(error_message)
|
||||||
|
|
||||||
def initialize(self, bot_handler: BotHandler) -> None:
|
def initialize(self, bot_handler: BotHandler) -> None:
|
||||||
self.config_info = bot_handler.get_config_info('giphy')
|
self.config_info = bot_handler.get_config_info("giphy")
|
||||||
|
|
||||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
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)
|
||||||
|
@ -60,9 +60,9 @@ def get_url_gif_giphy(keyword: str, api_key: str) -> Union[int, str]:
|
||||||
# Return a URL for a Giphy GIF based on keywords given.
|
# Return a URL for a Giphy GIF based on keywords given.
|
||||||
# In case of error, e.g. failure to fetch a GIF URL, it will
|
# In case of error, e.g. failure to fetch a GIF URL, it will
|
||||||
# return a number.
|
# return a number.
|
||||||
query = {'api_key': api_key}
|
query = {"api_key": api_key}
|
||||||
if len(keyword) > 0:
|
if len(keyword) > 0:
|
||||||
query['s'] = keyword
|
query["s"] = keyword
|
||||||
url = GIPHY_TRANSLATE_API
|
url = GIPHY_TRANSLATE_API
|
||||||
else:
|
else:
|
||||||
url = GIPHY_RANDOM_API
|
url = GIPHY_RANDOM_API
|
||||||
|
@ -70,12 +70,12 @@ def get_url_gif_giphy(keyword: str, api_key: str) -> Union[int, str]:
|
||||||
try:
|
try:
|
||||||
data = requests.get(url, params=query)
|
data = requests.get(url, params=query)
|
||||||
except requests.exceptions.ConnectionError: # Usually triggered by bad connection.
|
except requests.exceptions.ConnectionError: # Usually triggered by bad connection.
|
||||||
logging.exception('Bad connection')
|
logging.exception("Bad connection")
|
||||||
raise
|
raise
|
||||||
data.raise_for_status()
|
data.raise_for_status()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
gif_url = data.json()['data']['images']['original']['url']
|
gif_url = data.json()["data"]["images"]["original"]["url"]
|
||||||
except (TypeError, KeyError): # Usually triggered by no result in Giphy.
|
except (TypeError, KeyError): # Usually triggered by no result in Giphy.
|
||||||
raise GiphyNoResultException()
|
raise GiphyNoResultException()
|
||||||
return gif_url
|
return gif_url
|
||||||
|
@ -86,20 +86,20 @@ def get_bot_giphy_response(
|
||||||
) -> str:
|
) -> str:
|
||||||
# Each exception has a specific reply should "gif_url" return a number.
|
# Each exception has a specific reply should "gif_url" return a number.
|
||||||
# The bot will post the appropriate message for the error.
|
# The bot will post the appropriate message for the error.
|
||||||
keyword = message['content']
|
keyword = message["content"]
|
||||||
try:
|
try:
|
||||||
gif_url = get_url_gif_giphy(keyword, config_info['key'])
|
gif_url = get_url_gif_giphy(keyword, config_info["key"])
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
return (
|
return (
|
||||||
'Uh oh, sorry :slightly_frowning_face:, I '
|
"Uh oh, sorry :slightly_frowning_face:, I "
|
||||||
'cannot process your request right now. But, '
|
"cannot process your request right now. But, "
|
||||||
'let\'s try again later! :grin:'
|
"let's try again later! :grin:"
|
||||||
)
|
)
|
||||||
except GiphyNoResultException:
|
except GiphyNoResultException:
|
||||||
return 'Sorry, I don\'t have a GIF for "%s"! ' ':astonished:' % (keyword,)
|
return 'Sorry, I don\'t have a GIF for "%s"! ' ":astonished:" % (keyword,)
|
||||||
return (
|
return (
|
||||||
'[Click to enlarge](%s)'
|
"[Click to enlarge](%s)"
|
||||||
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)' % (gif_url,)
|
"[](/static/images/interactive-bot/giphy/powered-by-giphy.png)" % (gif_url,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue