black: Reformat without skipping string normalization.

This commit is contained in:
PIG208 2021-05-28 17:05:11 +08:00 committed by Tim Abbott
parent fba21bb00d
commit 6f3f9bf7e4
178 changed files with 5242 additions and 5242 deletions

View file

@ -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,
) )

View file

@ -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()

View file

@ -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:

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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]

View file

@ -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:

View file

@ -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()

View file

@ -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"])

View file

@ -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"])

View file

@ -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"])

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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()

View file

@ -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>"))

View file

@ -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")

View file

@ -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")

View file

@ -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",

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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.")

View file

@ -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)

View file

@ -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)

View file

@ -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,
} }
) )

View file

@ -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"

View file

@ -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}`.

View file

@ -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:

View file

@ -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",

View file

@ -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]]

View file

@ -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,))

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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,
) )

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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,
} }
) )
) )

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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,
} }
) )
) )

View file

@ -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))

View file

@ -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,
} }
) )
) )

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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}
) )
) )

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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}
) )
) )

View file

@ -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))

View file

@ -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()

View file

@ -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())

View file

@ -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)

View file

@ -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

View file

@ -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.")

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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),
] ]
) )

View file

@ -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,

View file

@ -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 ""

View file

@ -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])

View file

@ -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

View file

@ -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"),

View file

@ -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."

View file

@ -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&nbsp;&nbsp;{}'.format( response += "\n" + "* (**{}**) {}\n&nbsp;&nbsp;{}".format(
d['type'], d['definition'], html2text.html2text(example) d["type"], d["definition"], html2text.html2text(example)
) )
except Exception: except Exception:

View file

@ -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&nbsp;&nbsp;their pet cat\n\n" "developed.\n&nbsp;&nbsp;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"
"&nbsp;&nbsp;Help! I'm drowning!\n\n" "&nbsp;&nbsp;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.")

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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"),

View file

@ -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)

View file

@ -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"
), ),
) )

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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.",

View file

@ -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,

View file

@ -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")

View file

@ -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 boards tiles from smallest to largest, left to right, rules = """Arrange the boards 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,

View file

@ -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)

View file

@ -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