black: Reformat without skipping string normalization.
This commit is contained in:
parent
fba21bb00d
commit
6f3f9bf7e4
|
@ -8,96 +8,96 @@ if MYPY:
|
|||
|
||||
whitespace_rules = [
|
||||
# This linter should be first since bash_rules depends on it.
|
||||
{'pattern': r'\s+$', 'strip': '\n', 'description': 'Fix trailing whitespace'},
|
||||
{'pattern': '\t', 'strip': '\n', 'description': 'Fix tab-based whitespace'},
|
||||
{"pattern": r"\s+$", "strip": "\n", "description": "Fix trailing whitespace"},
|
||||
{"pattern": "\t", "strip": "\n", "description": "Fix tab-based whitespace"},
|
||||
] # type: List[Rule]
|
||||
|
||||
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.
|
||||
# This rule finds one space trailing a non-space, three or more trailing spaces, and
|
||||
# spaces on an empty line.
|
||||
{
|
||||
'pattern': r'((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)',
|
||||
'strip': '\n',
|
||||
'description': 'Fix trailing whitespace',
|
||||
"pattern": r"((?<!\s)\s$)|(\s\s\s+$)|(^\s+$)",
|
||||
"strip": "\n",
|
||||
"description": "Fix trailing whitespace",
|
||||
},
|
||||
{
|
||||
'pattern': r'^#+[A-Za-z0-9]',
|
||||
'strip': '\n',
|
||||
'description': 'Missing space after # in heading',
|
||||
"pattern": r"^#+[A-Za-z0-9]",
|
||||
"strip": "\n",
|
||||
"description": "Missing space after # in heading",
|
||||
},
|
||||
]
|
||||
|
||||
python_rules = RuleList(
|
||||
langs=['py'],
|
||||
langs=["py"],
|
||||
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
|
||||
{'pattern': r" =" + r'[^ =>~"]', '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" =" + r'[^ =>~"]', "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"assertEquals[(]",
|
||||
'description': 'Use assertEqual, not assertEquals (which is deprecated).',
|
||||
"pattern": r"assertEquals[(]",
|
||||
"description": "Use assertEqual, not assertEquals (which is deprecated).",
|
||||
},
|
||||
{
|
||||
'pattern': r'self: Any',
|
||||
'description': 'you can omit Any annotation for self',
|
||||
'good_lines': ['def foo (self):'],
|
||||
'bad_lines': ['def foo(self: Any):'],
|
||||
"pattern": r"self: Any",
|
||||
"description": "you can omit Any annotation for self",
|
||||
"good_lines": ["def foo (self):"],
|
||||
"bad_lines": ["def foo(self: Any):"],
|
||||
},
|
||||
{'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 : after type in type annotation'},
|
||||
{'pattern': r"#type", 'description': 'Missing whitespace after "#" in type annotation'},
|
||||
{'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"== 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 : after type in type annotation"},
|
||||
{"pattern": r"#type", "description": 'Missing whitespace after "#" in type annotation'},
|
||||
{"pattern": r"if[(]", "description": "Missing space between if 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
|
||||
# rare; if we find any, they can be added to the exclude list for
|
||||
# this rule.
|
||||
{
|
||||
'pattern': r' % [a-zA-Z0-9_.]*\)?$',
|
||||
'description': 'Used % comprehension without a tuple',
|
||||
"pattern": r" % [a-zA-Z0-9_.]*\)?$",
|
||||
"description": "Used % comprehension without a tuple",
|
||||
},
|
||||
{
|
||||
'pattern': r'.*%s.* % \([a-zA-Z0-9_.]*\)$',
|
||||
'description': 'Used % comprehension without a tuple',
|
||||
"pattern": r".*%s.* % \([a-zA-Z0-9_.]*\)$",
|
||||
"description": "Used % comprehension without a tuple",
|
||||
},
|
||||
{
|
||||
'pattern': r'__future__',
|
||||
'include_only': {'zulip_bots/zulip_bots/bots/'},
|
||||
'description': 'Bots no longer need __future__ imports.',
|
||||
"pattern": r"__future__",
|
||||
"include_only": {"zulip_bots/zulip_bots/bots/"},
|
||||
"description": "Bots no longer need __future__ imports.",
|
||||
},
|
||||
{
|
||||
'pattern': r'#!/usr/bin/env python$',
|
||||
'include_only': {'zulip_bots/'},
|
||||
'description': 'Python shebangs must be python3',
|
||||
"pattern": r"#!/usr/bin/env python$",
|
||||
"include_only": {"zulip_bots/"},
|
||||
"description": "Python shebangs must be python3",
|
||||
},
|
||||
{
|
||||
'pattern': r'(^|\s)open\s*\(',
|
||||
'description': 'open() should not be used in Zulip\'s bots. Use functions'
|
||||
' provided by the bots framework to access the filesystem.',
|
||||
'include_only': {'zulip_bots/zulip_bots/bots/'},
|
||||
"pattern": r"(^|\s)open\s*\(",
|
||||
"description": "open() should not be used in Zulip's bots. Use functions"
|
||||
" provided by the bots framework to access the filesystem.",
|
||||
"include_only": {"zulip_bots/zulip_bots/bots/"},
|
||||
},
|
||||
{
|
||||
'pattern': r'pprint',
|
||||
'description': 'Used pprint, which is most likely a debugging leftover. For user output, use print().',
|
||||
"pattern": r"pprint",
|
||||
"description": "Used pprint, which is most likely a debugging leftover. For user output, use print().",
|
||||
},
|
||||
{
|
||||
'pattern': r'\(BotTestCase\)',
|
||||
'bad_lines': ['class TestSomeBot(BotTestCase):'],
|
||||
'description': 'Bot test cases should directly inherit from BotTestCase *and* DefaultTests.',
|
||||
"pattern": r"\(BotTestCase\)",
|
||||
"bad_lines": ["class TestSomeBot(BotTestCase):"],
|
||||
"description": "Bot test cases should directly inherit from BotTestCase *and* DefaultTests.",
|
||||
},
|
||||
{
|
||||
'pattern': r'\(DefaultTests, BotTestCase\)',
|
||||
'bad_lines': ['class TestSomeBot(DefaultTests, BotTestCase):'],
|
||||
'good_lines': ['class TestSomeBot(BotTestCase, DefaultTests):'],
|
||||
'description': 'Bot test cases should inherit from BotTestCase before DefaultTests.',
|
||||
"pattern": r"\(DefaultTests, BotTestCase\)",
|
||||
"bad_lines": ["class TestSomeBot(DefaultTests, BotTestCase):"],
|
||||
"good_lines": ["class TestSomeBot(BotTestCase, DefaultTests):"],
|
||||
"description": "Bot test cases should inherit from BotTestCase before DefaultTests.",
|
||||
},
|
||||
*whitespace_rules,
|
||||
],
|
||||
|
@ -105,12 +105,12 @@ python_rules = RuleList(
|
|||
)
|
||||
|
||||
bash_rules = RuleList(
|
||||
langs=['sh'],
|
||||
langs=["sh"],
|
||||
rules=[
|
||||
{
|
||||
'pattern': r'#!.*sh [-xe]',
|
||||
'description': 'Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches'
|
||||
' to set -x|set -e',
|
||||
"pattern": r"#!.*sh [-xe]",
|
||||
"description": "Fix shebang line with proper call to /usr/bin/env for Bash path, change -x|-e switches"
|
||||
" to set -x|set -e",
|
||||
},
|
||||
*whitespace_rules[0:1],
|
||||
],
|
||||
|
@ -118,7 +118,7 @@ bash_rules = RuleList(
|
|||
|
||||
|
||||
json_rules = RuleList(
|
||||
langs=['json'],
|
||||
langs=["json"],
|
||||
# Here, we don't check tab-based whitespace, because the tab-based
|
||||
# whitespace rule flags a lot of third-party JSON fixtures
|
||||
# under zerver/webhooks that we want preserved verbatim. So
|
||||
|
@ -131,21 +131,21 @@ json_rules = RuleList(
|
|||
|
||||
prose_style_rules = [
|
||||
{
|
||||
'pattern': r'[^\/\#\-"]([jJ]avascript)', # exclude usage in hrefs/divs
|
||||
'description': "javascript should be spelled JavaScript",
|
||||
"pattern": r'[^\/\#\-"]([jJ]avascript)', # exclude usage in hrefs/divs
|
||||
"description": "javascript should be spelled JavaScript",
|
||||
},
|
||||
{
|
||||
'pattern': r'''[^\/\-\."'\_\=\>]([gG]ithub)[^\.\-\_"\<]''', # exclude usage in hrefs/divs
|
||||
'description': "github should be spelled GitHub",
|
||||
"pattern": r"""[^\/\-\."'\_\=\>]([gG]ithub)[^\.\-\_"\<]""", # exclude usage in hrefs/divs
|
||||
"description": "github should be spelled GitHub",
|
||||
},
|
||||
{
|
||||
'pattern': r'[oO]rganisation', # exclude usage in hrefs/divs
|
||||
'description': "Organization is spelled with a z",
|
||||
"pattern": r"[oO]rganisation", # exclude usage in hrefs/divs
|
||||
"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',
|
||||
'description': "Use Botserver instead of botserver or Botserver.",
|
||||
"pattern": r"[^-_]botserver(?!rc)|bot server",
|
||||
"description": "Use Botserver instead of botserver or Botserver.",
|
||||
},
|
||||
] # type: List[Rule]
|
||||
|
||||
|
@ -154,13 +154,13 @@ markdown_docs_length_exclude = {
|
|||
}
|
||||
|
||||
markdown_rules = RuleList(
|
||||
langs=['md'],
|
||||
langs=["md"],
|
||||
rules=[
|
||||
*markdown_whitespace_rules,
|
||||
*prose_style_rules,
|
||||
{
|
||||
'pattern': r'\[(?P<url>[^\]]+)\]\((?P=url)\)',
|
||||
'description': 'Linkified markdown URLs should use cleaner <http://example.com> syntax.',
|
||||
"pattern": r"\[(?P<url>[^\]]+)\]\((?P=url)\)",
|
||||
"description": "Linkified markdown URLs should use cleaner <http://example.com> syntax.",
|
||||
},
|
||||
],
|
||||
max_length=120,
|
||||
|
@ -168,7 +168,7 @@ markdown_rules = RuleList(
|
|||
)
|
||||
|
||||
txt_rules = RuleList(
|
||||
langs=['txt'],
|
||||
langs=["txt"],
|
||||
rules=whitespace_rules,
|
||||
)
|
||||
|
||||
|
|
204
tools/deploy
204
tools/deploy
|
@ -11,69 +11,69 @@ from typing import Any, Callable, Dict, List
|
|||
import requests
|
||||
from requests import Response
|
||||
|
||||
red = '\033[91m' # type: str
|
||||
green = '\033[92m' # type: str
|
||||
end_format = '\033[0m' # type: str
|
||||
bold = '\033[1m' # type: str
|
||||
red = "\033[91m" # type: str
|
||||
green = "\033[92m" # type: str
|
||||
end_format = "\033[0m" # type: str
|
||||
bold = "\033[1m" # type: str
|
||||
|
||||
bots_dir = '.bots' # type: str
|
||||
bots_dir = ".bots" # type: str
|
||||
|
||||
|
||||
def pack(options: argparse.Namespace) -> None:
|
||||
# Basic sanity checks for input.
|
||||
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)
|
||||
if not options.config:
|
||||
print('tools/deploy: Path to zuliprc not specified.')
|
||||
print("tools/deploy: Path to zuliprc not specified.")
|
||||
sys.exit(1)
|
||||
if not options.main:
|
||||
print('tools/deploy: No main bot file specified.')
|
||||
print("tools/deploy: No main bot file specified.")
|
||||
sys.exit(1)
|
||||
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)
|
||||
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)
|
||||
main_path = os.path.join(options.path, options.main)
|
||||
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)
|
||||
|
||||
# Main logic for packing the bot.
|
||||
if not os.path.exists(bots_dir):
|
||||
os.makedirs(bots_dir)
|
||||
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
|
||||
for root, dirs, files in os.walk(options.path):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
zip_file.write(file_path, os.path.relpath(file_path, options.path))
|
||||
# Pack the zuliprc
|
||||
zip_file.write(options.config, 'zuliprc')
|
||||
zip_file.write(options.config, "zuliprc")
|
||||
# Pack the config file for the botfarm.
|
||||
bot_config = textwrap.dedent(
|
||||
'''\
|
||||
"""\
|
||||
[deploy]
|
||||
bot={}
|
||||
zuliprc=zuliprc
|
||||
'''.format(
|
||||
""".format(
|
||||
options.main
|
||||
)
|
||||
)
|
||||
zip_file.writestr('config.ini', bot_config)
|
||||
zip_file.writestr("config.ini", bot_config)
|
||||
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:
|
||||
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)
|
||||
if not options.token:
|
||||
print('tools/deploy: Botfarm deploy token not specified.')
|
||||
print("tools/deploy: Botfarm deploy token not specified.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
@ -83,7 +83,7 @@ def handle_common_response_without_data(
|
|||
return handle_common_response(
|
||||
response=response,
|
||||
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:
|
||||
if response.status_code == requests.codes.ok:
|
||||
response_data = response.json()
|
||||
if response_data['status'] == 'success':
|
||||
if response_data["status"] == "success":
|
||||
success_handler(response_data)
|
||||
return True
|
||||
elif response_data['status'] == 'error':
|
||||
print('{}: {}'.format(operation, response_data['message']))
|
||||
elif response_data["status"] == "error":
|
||||
print("{}: {}".format(operation, response_data["message"]))
|
||||
return False
|
||||
else:
|
||||
print('{}: Unexpected success response format'.format(operation))
|
||||
print("{}: Unexpected success response format".format(operation))
|
||||
return False
|
||||
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:
|
||||
print('{}: Error {}. Aborting.'.format(operation, response.status_code))
|
||||
print("{}: Error {}. Aborting.".format(operation, response.status_code))
|
||||
return False
|
||||
|
||||
|
||||
def upload(options: argparse.Namespace) -> None:
|
||||
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):
|
||||
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)
|
||||
files = {'file': open(file_path, 'rb')}
|
||||
headers = {'key': options.token}
|
||||
url = urllib.parse.urljoin(options.server, 'bots/upload')
|
||||
files = {"file": open(file_path, "rb")}
|
||||
headers = {"key": options.token}
|
||||
url = urllib.parse.urljoin(options.server, "bots/upload")
|
||||
response = requests.post(url, files=files, headers=headers)
|
||||
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:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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):
|
||||
os.remove(file_path)
|
||||
print('clean: Removed {}.'.format(file_path))
|
||||
print("clean: Removed {}.".format(file_path))
|
||||
else:
|
||||
print('clean: File \'{}\' not found.'.format(file_path))
|
||||
print("clean: File '{}' not found.".format(file_path))
|
||||
|
||||
|
||||
def process(options: argparse.Namespace) -> None:
|
||||
check_common_options(options)
|
||||
headers = {'key': options.token}
|
||||
url = urllib.parse.urljoin(options.server, 'bots/process')
|
||||
payload = {'name': options.botname}
|
||||
headers = {"key": options.token}
|
||||
url = urllib.parse.urljoin(options.server, "bots/process")
|
||||
payload = {"name": options.botname}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
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:
|
||||
sys.exit(1)
|
||||
|
@ -149,12 +149,12 @@ def process(options: argparse.Namespace) -> None:
|
|||
|
||||
def start(options: argparse.Namespace) -> None:
|
||||
check_common_options(options)
|
||||
headers = {'key': options.token}
|
||||
url = urllib.parse.urljoin(options.server, 'bots/start')
|
||||
payload = {'name': options.botname}
|
||||
headers = {"key": options.token}
|
||||
url = urllib.parse.urljoin(options.server, "bots/start")
|
||||
payload = {"name": options.botname}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
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:
|
||||
sys.exit(1)
|
||||
|
@ -162,12 +162,12 @@ def start(options: argparse.Namespace) -> None:
|
|||
|
||||
def stop(options: argparse.Namespace) -> None:
|
||||
check_common_options(options)
|
||||
headers = {'key': options.token}
|
||||
url = urllib.parse.urljoin(options.server, 'bots/stop')
|
||||
payload = {'name': options.botname}
|
||||
headers = {"key": options.token}
|
||||
url = urllib.parse.urljoin(options.server, "bots/stop")
|
||||
payload = {"name": options.botname}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
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:
|
||||
sys.exit(1)
|
||||
|
@ -182,27 +182,27 @@ def prepare(options: argparse.Namespace) -> None:
|
|||
|
||||
def log(options: argparse.Namespace) -> None:
|
||||
check_common_options(options)
|
||||
headers = {'key': options.token}
|
||||
headers = {"key": options.token}
|
||||
if options.lines:
|
||||
lines = options.lines
|
||||
else:
|
||||
lines = None
|
||||
payload = {'name': options.botname, 'lines': lines}
|
||||
url = urllib.parse.urljoin(options.server, 'bots/logs/' + options.botname)
|
||||
payload = {"name": options.botname, "lines": lines}
|
||||
url = urllib.parse.urljoin(options.server, "bots/logs/" + options.botname)
|
||||
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:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def delete(options: argparse.Namespace) -> None:
|
||||
check_common_options(options)
|
||||
headers = {'key': options.token}
|
||||
url = urllib.parse.urljoin(options.server, 'bots/delete')
|
||||
payload = {'name': options.botname}
|
||||
headers = {"key": options.token}
|
||||
url = urllib.parse.urljoin(options.server, "bots/delete")
|
||||
payload = {"name": options.botname}
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
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:
|
||||
sys.exit(1)
|
||||
|
@ -210,15 +210,15 @@ def delete(options: argparse.Namespace) -> None:
|
|||
|
||||
def list_bots(options: argparse.Namespace) -> None:
|
||||
check_common_options(options)
|
||||
headers = {'key': options.token}
|
||||
headers = {"key": options.token}
|
||||
if options.format:
|
||||
pretty_print = True
|
||||
else:
|
||||
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)
|
||||
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:
|
||||
sys.exit(1)
|
||||
|
@ -229,36 +229,36 @@ def print_bots(bots: List[Any], pretty_print: bool) -> None:
|
|||
print_bots_pretty(bots)
|
||||
else:
|
||||
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:
|
||||
if len(bots) == 0:
|
||||
print('ls: No bots found on the botfarm')
|
||||
print("ls: No bots found on the botfarm")
|
||||
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
|
||||
row_format = '{0} {1} {2} {3}'
|
||||
row_format = "{0} {1} {2} {3}"
|
||||
header = row_format.format(
|
||||
'NAME'.rjust(name_col_len),
|
||||
'STATUS'.rjust(status_col_len),
|
||||
'EMAIL'.rjust(email_col_len),
|
||||
'SITE'.rjust(site_col_len),
|
||||
"NAME".rjust(name_col_len),
|
||||
"STATUS".rjust(status_col_len),
|
||||
"EMAIL".rjust(email_col_len),
|
||||
"SITE".rjust(site_col_len),
|
||||
)
|
||||
header_bottom = row_format.format(
|
||||
'-' * name_col_len,
|
||||
'-' * status_col_len,
|
||||
'-' * email_col_len,
|
||||
'-' * site_col_len,
|
||||
"-" * name_col_len,
|
||||
"-" * status_col_len,
|
||||
"-" * email_col_len,
|
||||
"-" * site_col_len,
|
||||
)
|
||||
print(header)
|
||||
print(header_bottom)
|
||||
for bot in bots:
|
||||
row = row_format.format(
|
||||
bot['name'].rjust(name_col_len),
|
||||
bot['status'].rjust(status_col_len),
|
||||
bot['email'].rjust(email_col_len),
|
||||
bot['site'].rjust(site_col_len),
|
||||
bot["name"].rjust(name_col_len),
|
||||
bot["status"].rjust(status_col_len),
|
||||
bot["email"].rjust(email_col_len),
|
||||
bot["site"].rjust(site_col_len),
|
||||
)
|
||||
print(row)
|
||||
|
||||
|
@ -297,52 +297,52 @@ To list user's bots, use:
|
|||
|
||||
"""
|
||||
parser = argparse.ArgumentParser(usage=usage)
|
||||
parser.add_argument('command', help='Command to run.')
|
||||
parser.add_argument('botname', nargs='?', help='Name of bot to operate on.')
|
||||
parser.add_argument("command", help="Command to run.")
|
||||
parser.add_argument("botname", nargs="?", help="Name of bot to operate on.")
|
||||
parser.add_argument(
|
||||
'--server',
|
||||
'-s',
|
||||
metavar='SERVERURL',
|
||||
default=os.environ.get('SERVER', ''),
|
||||
help='Url of the Zulip Botfarm server.',
|
||||
"--server",
|
||||
"-s",
|
||||
metavar="SERVERURL",
|
||||
default=os.environ.get("SERVER", ""),
|
||||
help="Url of the Zulip Botfarm server.",
|
||||
)
|
||||
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('--config', '-c', help='Path to the zuliprc file.')
|
||||
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(
|
||||
'--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(
|
||||
'--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()
|
||||
if not options.command:
|
||||
print('tools/deploy: No command specified.')
|
||||
print("tools/deploy: No command specified.")
|
||||
sys.exit(1)
|
||||
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\'')
|
||||
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'")
|
||||
sys.exit(1)
|
||||
|
||||
commands = {
|
||||
'pack': pack,
|
||||
'upload': upload,
|
||||
'clean': clean,
|
||||
'prepare': prepare,
|
||||
'process': process,
|
||||
'start': start,
|
||||
'stop': stop,
|
||||
'log': log,
|
||||
'delete': delete,
|
||||
'ls': list_bots,
|
||||
"pack": pack,
|
||||
"upload": upload,
|
||||
"clean": clean,
|
||||
"prepare": prepare,
|
||||
"process": process,
|
||||
"start": start,
|
||||
"stop": stop,
|
||||
"log": log,
|
||||
"delete": delete,
|
||||
"ls": list_bots,
|
||||
}
|
||||
if options.command in commands:
|
||||
commands[options.command](options)
|
||||
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()
|
||||
|
|
|
@ -11,246 +11,246 @@ from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
|
|||
# License: MIT
|
||||
# Ref: fit_commit/validators/tense.rb
|
||||
WORD_SET = {
|
||||
'adds',
|
||||
'adding',
|
||||
'added',
|
||||
'allows',
|
||||
'allowing',
|
||||
'allowed',
|
||||
'amends',
|
||||
'amending',
|
||||
'amended',
|
||||
'bumps',
|
||||
'bumping',
|
||||
'bumped',
|
||||
'calculates',
|
||||
'calculating',
|
||||
'calculated',
|
||||
'changes',
|
||||
'changing',
|
||||
'changed',
|
||||
'cleans',
|
||||
'cleaning',
|
||||
'cleaned',
|
||||
'commits',
|
||||
'committing',
|
||||
'committed',
|
||||
'corrects',
|
||||
'correcting',
|
||||
'corrected',
|
||||
'creates',
|
||||
'creating',
|
||||
'created',
|
||||
'darkens',
|
||||
'darkening',
|
||||
'darkened',
|
||||
'disables',
|
||||
'disabling',
|
||||
'disabled',
|
||||
'displays',
|
||||
'displaying',
|
||||
'displayed',
|
||||
'documents',
|
||||
'documenting',
|
||||
'documented',
|
||||
'drys',
|
||||
'drying',
|
||||
'dryed',
|
||||
'ends',
|
||||
'ending',
|
||||
'ended',
|
||||
'enforces',
|
||||
'enforcing',
|
||||
'enforced',
|
||||
'enqueues',
|
||||
'enqueuing',
|
||||
'enqueued',
|
||||
'extracts',
|
||||
'extracting',
|
||||
'extracted',
|
||||
'finishes',
|
||||
'finishing',
|
||||
'finished',
|
||||
'fixes',
|
||||
'fixing',
|
||||
'fixed',
|
||||
'formats',
|
||||
'formatting',
|
||||
'formatted',
|
||||
'guards',
|
||||
'guarding',
|
||||
'guarded',
|
||||
'handles',
|
||||
'handling',
|
||||
'handled',
|
||||
'hides',
|
||||
'hiding',
|
||||
'hid',
|
||||
'increases',
|
||||
'increasing',
|
||||
'increased',
|
||||
'ignores',
|
||||
'ignoring',
|
||||
'ignored',
|
||||
'implements',
|
||||
'implementing',
|
||||
'implemented',
|
||||
'improves',
|
||||
'improving',
|
||||
'improved',
|
||||
'keeps',
|
||||
'keeping',
|
||||
'kept',
|
||||
'kills',
|
||||
'killing',
|
||||
'killed',
|
||||
'makes',
|
||||
'making',
|
||||
'made',
|
||||
'merges',
|
||||
'merging',
|
||||
'merged',
|
||||
'moves',
|
||||
'moving',
|
||||
'moved',
|
||||
'permits',
|
||||
'permitting',
|
||||
'permitted',
|
||||
'prevents',
|
||||
'preventing',
|
||||
'prevented',
|
||||
'pushes',
|
||||
'pushing',
|
||||
'pushed',
|
||||
'rebases',
|
||||
'rebasing',
|
||||
'rebased',
|
||||
'refactors',
|
||||
'refactoring',
|
||||
'refactored',
|
||||
'removes',
|
||||
'removing',
|
||||
'removed',
|
||||
'renames',
|
||||
'renaming',
|
||||
'renamed',
|
||||
'reorders',
|
||||
'reordering',
|
||||
'reordered',
|
||||
'replaces',
|
||||
'replacing',
|
||||
'replaced',
|
||||
'requires',
|
||||
'requiring',
|
||||
'required',
|
||||
'restores',
|
||||
'restoring',
|
||||
'restored',
|
||||
'sends',
|
||||
'sending',
|
||||
'sent',
|
||||
'sets',
|
||||
'setting',
|
||||
'separates',
|
||||
'separating',
|
||||
'separated',
|
||||
'shows',
|
||||
'showing',
|
||||
'showed',
|
||||
'simplifies',
|
||||
'simplifying',
|
||||
'simplified',
|
||||
'skips',
|
||||
'skipping',
|
||||
'skipped',
|
||||
'sorts',
|
||||
'sorting',
|
||||
'speeds',
|
||||
'speeding',
|
||||
'sped',
|
||||
'starts',
|
||||
'starting',
|
||||
'started',
|
||||
'supports',
|
||||
'supporting',
|
||||
'supported',
|
||||
'takes',
|
||||
'taking',
|
||||
'took',
|
||||
'testing',
|
||||
'tested', # 'tests' excluded to reduce false negative
|
||||
'truncates',
|
||||
'truncating',
|
||||
'truncated',
|
||||
'updates',
|
||||
'updating',
|
||||
'updated',
|
||||
'uses',
|
||||
'using',
|
||||
'used',
|
||||
"adds",
|
||||
"adding",
|
||||
"added",
|
||||
"allows",
|
||||
"allowing",
|
||||
"allowed",
|
||||
"amends",
|
||||
"amending",
|
||||
"amended",
|
||||
"bumps",
|
||||
"bumping",
|
||||
"bumped",
|
||||
"calculates",
|
||||
"calculating",
|
||||
"calculated",
|
||||
"changes",
|
||||
"changing",
|
||||
"changed",
|
||||
"cleans",
|
||||
"cleaning",
|
||||
"cleaned",
|
||||
"commits",
|
||||
"committing",
|
||||
"committed",
|
||||
"corrects",
|
||||
"correcting",
|
||||
"corrected",
|
||||
"creates",
|
||||
"creating",
|
||||
"created",
|
||||
"darkens",
|
||||
"darkening",
|
||||
"darkened",
|
||||
"disables",
|
||||
"disabling",
|
||||
"disabled",
|
||||
"displays",
|
||||
"displaying",
|
||||
"displayed",
|
||||
"documents",
|
||||
"documenting",
|
||||
"documented",
|
||||
"drys",
|
||||
"drying",
|
||||
"dryed",
|
||||
"ends",
|
||||
"ending",
|
||||
"ended",
|
||||
"enforces",
|
||||
"enforcing",
|
||||
"enforced",
|
||||
"enqueues",
|
||||
"enqueuing",
|
||||
"enqueued",
|
||||
"extracts",
|
||||
"extracting",
|
||||
"extracted",
|
||||
"finishes",
|
||||
"finishing",
|
||||
"finished",
|
||||
"fixes",
|
||||
"fixing",
|
||||
"fixed",
|
||||
"formats",
|
||||
"formatting",
|
||||
"formatted",
|
||||
"guards",
|
||||
"guarding",
|
||||
"guarded",
|
||||
"handles",
|
||||
"handling",
|
||||
"handled",
|
||||
"hides",
|
||||
"hiding",
|
||||
"hid",
|
||||
"increases",
|
||||
"increasing",
|
||||
"increased",
|
||||
"ignores",
|
||||
"ignoring",
|
||||
"ignored",
|
||||
"implements",
|
||||
"implementing",
|
||||
"implemented",
|
||||
"improves",
|
||||
"improving",
|
||||
"improved",
|
||||
"keeps",
|
||||
"keeping",
|
||||
"kept",
|
||||
"kills",
|
||||
"killing",
|
||||
"killed",
|
||||
"makes",
|
||||
"making",
|
||||
"made",
|
||||
"merges",
|
||||
"merging",
|
||||
"merged",
|
||||
"moves",
|
||||
"moving",
|
||||
"moved",
|
||||
"permits",
|
||||
"permitting",
|
||||
"permitted",
|
||||
"prevents",
|
||||
"preventing",
|
||||
"prevented",
|
||||
"pushes",
|
||||
"pushing",
|
||||
"pushed",
|
||||
"rebases",
|
||||
"rebasing",
|
||||
"rebased",
|
||||
"refactors",
|
||||
"refactoring",
|
||||
"refactored",
|
||||
"removes",
|
||||
"removing",
|
||||
"removed",
|
||||
"renames",
|
||||
"renaming",
|
||||
"renamed",
|
||||
"reorders",
|
||||
"reordering",
|
||||
"reordered",
|
||||
"replaces",
|
||||
"replacing",
|
||||
"replaced",
|
||||
"requires",
|
||||
"requiring",
|
||||
"required",
|
||||
"restores",
|
||||
"restoring",
|
||||
"restored",
|
||||
"sends",
|
||||
"sending",
|
||||
"sent",
|
||||
"sets",
|
||||
"setting",
|
||||
"separates",
|
||||
"separating",
|
||||
"separated",
|
||||
"shows",
|
||||
"showing",
|
||||
"showed",
|
||||
"simplifies",
|
||||
"simplifying",
|
||||
"simplified",
|
||||
"skips",
|
||||
"skipping",
|
||||
"skipped",
|
||||
"sorts",
|
||||
"sorting",
|
||||
"speeds",
|
||||
"speeding",
|
||||
"sped",
|
||||
"starts",
|
||||
"starting",
|
||||
"started",
|
||||
"supports",
|
||||
"supporting",
|
||||
"supported",
|
||||
"takes",
|
||||
"taking",
|
||||
"took",
|
||||
"testing",
|
||||
"tested", # 'tests' excluded to reduce false negative
|
||||
"truncates",
|
||||
"truncating",
|
||||
"truncated",
|
||||
"updates",
|
||||
"updating",
|
||||
"updated",
|
||||
"uses",
|
||||
"using",
|
||||
"used",
|
||||
}
|
||||
|
||||
imperative_forms = [
|
||||
'add',
|
||||
'allow',
|
||||
'amend',
|
||||
'bump',
|
||||
'calculate',
|
||||
'change',
|
||||
'clean',
|
||||
'commit',
|
||||
'correct',
|
||||
'create',
|
||||
'darken',
|
||||
'disable',
|
||||
'display',
|
||||
'document',
|
||||
'dry',
|
||||
'end',
|
||||
'enforce',
|
||||
'enqueue',
|
||||
'extract',
|
||||
'finish',
|
||||
'fix',
|
||||
'format',
|
||||
'guard',
|
||||
'handle',
|
||||
'hide',
|
||||
'ignore',
|
||||
'implement',
|
||||
'improve',
|
||||
'increase',
|
||||
'keep',
|
||||
'kill',
|
||||
'make',
|
||||
'merge',
|
||||
'move',
|
||||
'permit',
|
||||
'prevent',
|
||||
'push',
|
||||
'rebase',
|
||||
'refactor',
|
||||
'remove',
|
||||
'rename',
|
||||
'reorder',
|
||||
'replace',
|
||||
'require',
|
||||
'restore',
|
||||
'send',
|
||||
'separate',
|
||||
'set',
|
||||
'show',
|
||||
'simplify',
|
||||
'skip',
|
||||
'sort',
|
||||
'speed',
|
||||
'start',
|
||||
'support',
|
||||
'take',
|
||||
'test',
|
||||
'truncate',
|
||||
'update',
|
||||
'use',
|
||||
"add",
|
||||
"allow",
|
||||
"amend",
|
||||
"bump",
|
||||
"calculate",
|
||||
"change",
|
||||
"clean",
|
||||
"commit",
|
||||
"correct",
|
||||
"create",
|
||||
"darken",
|
||||
"disable",
|
||||
"display",
|
||||
"document",
|
||||
"dry",
|
||||
"end",
|
||||
"enforce",
|
||||
"enqueue",
|
||||
"extract",
|
||||
"finish",
|
||||
"fix",
|
||||
"format",
|
||||
"guard",
|
||||
"handle",
|
||||
"hide",
|
||||
"ignore",
|
||||
"implement",
|
||||
"improve",
|
||||
"increase",
|
||||
"keep",
|
||||
"kill",
|
||||
"make",
|
||||
"merge",
|
||||
"move",
|
||||
"permit",
|
||||
"prevent",
|
||||
"push",
|
||||
"rebase",
|
||||
"refactor",
|
||||
"remove",
|
||||
"rename",
|
||||
"reorder",
|
||||
"replace",
|
||||
"require",
|
||||
"restore",
|
||||
"send",
|
||||
"separate",
|
||||
"set",
|
||||
"show",
|
||||
"simplify",
|
||||
"skip",
|
||||
"sort",
|
||||
"speed",
|
||||
"start",
|
||||
"support",
|
||||
"take",
|
||||
"test",
|
||||
"truncate",
|
||||
"update",
|
||||
"use",
|
||||
]
|
||||
imperative_forms.sort()
|
||||
|
||||
|
@ -260,8 +260,8 @@ def head_binary_search(key: str, words: List[str]) -> str:
|
|||
3 characters."""
|
||||
|
||||
# Edge case: 'disable' and 'display' have the same 3 starting letters.
|
||||
if key in ['displays', 'displaying', 'displayed']:
|
||||
return 'display'
|
||||
if key in ["displays", "displaying", "displayed"]:
|
||||
return "display"
|
||||
|
||||
lower = 0
|
||||
upper = len(words) - 1
|
||||
|
@ -292,7 +292,7 @@ class ImperativeMood(LineRule):
|
|||
target = CommitMessageTitle
|
||||
|
||||
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}"'
|
||||
)
|
||||
|
||||
|
@ -300,7 +300,7 @@ class ImperativeMood(LineRule):
|
|||
violations = []
|
||||
|
||||
# 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()
|
||||
|
||||
if first_word in WORD_SET:
|
||||
|
|
16
tools/lint
16
tools/lint
|
@ -9,7 +9,7 @@ from custom_check import non_py_rules, python_rules
|
|||
|
||||
EXCLUDED_FILES = [
|
||||
# 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)
|
||||
|
||||
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(
|
||||
'mypy',
|
||||
[sys.executable, 'tools/run-mypy'],
|
||||
['py'],
|
||||
"mypy",
|
||||
[sys.executable, "tools/run-mypy"],
|
||||
["py"],
|
||||
pass_targets=False,
|
||||
description="Static type checker for Python (config: mypy.ini)",
|
||||
)
|
||||
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(
|
||||
'gitlint', ['tools/lint-commits'], description="Git Lint for commit messages"
|
||||
"gitlint", ["tools/lint-commits"], description="Git Lint for commit messages"
|
||||
)
|
||||
|
||||
@linter_config.lint
|
||||
|
@ -55,5 +55,5 @@ def run() -> None:
|
|||
linter_config.do_lint()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
|
|
@ -7,13 +7,13 @@ import subprocess
|
|||
import sys
|
||||
|
||||
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)
|
||||
|
||||
red = '\033[91m'
|
||||
green = '\033[92m'
|
||||
end_format = '\033[0m'
|
||||
bold = '\033[1m'
|
||||
red = "\033[91m"
|
||||
green = "\033[92m"
|
||||
end_format = "\033[0m"
|
||||
bold = "\033[1m"
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -23,25 +23,25 @@ Creates a Python virtualenv. Its Python version is equal to
|
|||
the Python version this command is executed with."""
|
||||
parser = argparse.ArgumentParser(usage=usage)
|
||||
parser.add_argument(
|
||||
'--python-interpreter',
|
||||
'-p',
|
||||
metavar='PATH_TO_PYTHON_INTERPRETER',
|
||||
"--python-interpreter",
|
||||
"-p",
|
||||
metavar="PATH_TO_PYTHON_INTERPRETER",
|
||||
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(
|
||||
'--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()
|
||||
|
||||
base_dir = os.path.abspath(os.path.join(__file__, '..', '..'))
|
||||
base_dir = os.path.abspath(os.path.join(__file__, "..", ".."))
|
||||
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"
|
||||
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])
|
||||
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):
|
||||
print(
|
||||
|
@ -53,7 +53,7 @@ the Python version this command is executed with."""
|
|||
venv_dir = os.path.join(base_dir, venv_name)
|
||||
if not os.path.isdir(venv_dir):
|
||||
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:
|
||||
print(
|
||||
"{red}Installation with venv failed. Probable errors are: "
|
||||
|
@ -77,34 +77,34 @@ the Python version this command is executed with."""
|
|||
else:
|
||||
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
|
||||
# venv uses /Scripts instead of /bin on Windows cmd and Power Shell.
|
||||
# Read https://docs.python.org/3/library/venv.html
|
||||
venv_exec_dir = 'Scripts'
|
||||
venv_exec_dir = "Scripts"
|
||||
else:
|
||||
venv_exec_dir = 'bin'
|
||||
venv_exec_dir = "bin"
|
||||
|
||||
# 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
|
||||
# https://stackoverflow.com/questions/26323852/whats-the-meaning-of-pyvenv-launcher-environment-variable
|
||||
if '__PYVENV_LAUNCHER__' in os.environ:
|
||||
del os.environ['__PYVENV_LAUNCHER__']
|
||||
if "__PYVENV_LAUNCHER__" in os.environ:
|
||||
del os.environ["__PYVENV_LAUNCHER__"]
|
||||
|
||||
# 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
|
||||
# in the right place.
|
||||
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
|
||||
subprocess.call([pip_path, 'install', 'pip>=9.0'])
|
||||
subprocess.call([pip_path, "install", "pip>=9.0"])
|
||||
if subprocess.call(
|
||||
[
|
||||
pip_path,
|
||||
'install',
|
||||
'--prefix',
|
||||
"install",
|
||||
"--prefix",
|
||||
venv_dir,
|
||||
'-r',
|
||||
"-r",
|
||||
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()
|
||||
# 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)
|
||||
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
|
||||
# are likely to be running in a bash shell.
|
||||
activate_command = activate_command.replace(os.sep, '/')
|
||||
print('\nRun the following to enter into the virtualenv:\n')
|
||||
print(bold + ' source ' + activate_command + end_format + "\n")
|
||||
activate_command = activate_command.replace(os.sep, "/")
|
||||
print("\nRun the following to enter into the virtualenv:\n")
|
||||
print(bold + " source " + activate_command + end_format + "\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -25,7 +25,7 @@ def cd(newdir):
|
|||
|
||||
|
||||
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,
|
||||
package_name=package_name,
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ def _generate_dist(dist_type, setup_file, package_name, setup_args):
|
|||
with cd(setup_dir):
|
||||
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,
|
||||
package_name=package_name,
|
||||
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):
|
||||
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:
|
||||
_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):
|
||||
message = 'Uploading distributions under the following directories:'
|
||||
message = "Uploading distributions under the following directories:"
|
||||
print(crayons.green(message, bold=True))
|
||||
for dist_dir in dist_dirs:
|
||||
print(crayons.yellow(dist_dir))
|
||||
|
@ -59,14 +59,14 @@ def twine_upload(dist_dirs):
|
|||
|
||||
|
||||
def cleanup(package_dir):
|
||||
build_dir = os.path.join(package_dir, 'build')
|
||||
temp_dir = os.path.join(package_dir, 'temp')
|
||||
dist_dir = os.path.join(package_dir, 'dist')
|
||||
egg_info = os.path.join(package_dir, '{}.egg-info'.format(os.path.basename(package_dir)))
|
||||
build_dir = os.path.join(package_dir, "build")
|
||||
temp_dir = os.path.join(package_dir, "temp")
|
||||
dist_dir = os.path.join(package_dir, "dist")
|
||||
egg_info = os.path.join(package_dir, "{}.egg-info".format(os.path.basename(package_dir)))
|
||||
|
||||
def _rm_if_it_exists(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)
|
||||
|
||||
_rm_if_it_exists(build_dir)
|
||||
|
@ -77,11 +77,11 @@ def cleanup(package_dir):
|
|||
|
||||
def set_variable(fp, variable, value):
|
||||
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:
|
||||
if line.startswith(variable):
|
||||
if isinstance(value, bool):
|
||||
template = '{variable} = {value}\n'
|
||||
template = "{variable} = {value}\n"
|
||||
else:
|
||||
template = '{variable} = "{value}"\n'
|
||||
new_file.write(template.format(variable=variable, value=value))
|
||||
|
@ -91,22 +91,22 @@ def set_variable(fp, variable, value):
|
|||
os.remove(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))
|
||||
|
||||
|
||||
def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag):
|
||||
common = os.path.join(zulip_repo_dir, 'requirements', 'common.in')
|
||||
prod = os.path.join(zulip_repo_dir, 'requirements', 'prod.txt')
|
||||
dev = os.path.join(zulip_repo_dir, 'requirements', 'dev.txt')
|
||||
common = os.path.join(zulip_repo_dir, "requirements", "common.in")
|
||||
prod = os.path.join(zulip_repo_dir, "requirements", "prod.txt")
|
||||
dev = os.path.join(zulip_repo_dir, "requirements", "dev.txt")
|
||||
|
||||
def _edit_reqs_file(reqs, zulip_bots_line, zulip_line):
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
else:
|
||||
new_file.write(line)
|
||||
|
@ -114,10 +114,10 @@ def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag):
|
|||
os.remove(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_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_line = url_zulip.format(tag=hash_or_tag, name='zulip', version=version)
|
||||
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"
|
||||
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)
|
||||
|
||||
_edit_reqs_file(prod, 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(
|
||||
common,
|
||||
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_bots.format(tag=hash_or_tag, name="zulip_bots", 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))
|
||||
|
||||
|
||||
|
@ -177,39 +177,39 @@ And you're done! Congrats!
|
|||
parser = argparse.ArgumentParser(usage=usage)
|
||||
|
||||
parser.add_argument(
|
||||
'--cleanup',
|
||||
'-c',
|
||||
action='store_true',
|
||||
"--cleanup",
|
||||
"-c",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help='Remove build directories (dist/, build/, egg-info/, etc).',
|
||||
help="Remove build directories (dist/, build/, egg-info/, etc).",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--build',
|
||||
'-b',
|
||||
metavar='VERSION_NUM',
|
||||
"--build",
|
||||
"-b",
|
||||
metavar="VERSION_NUM",
|
||||
help=(
|
||||
'Build sdists and wheels for all packages with the'
|
||||
'specified version number.'
|
||||
' sdists and wheels are stored in <package_name>/dist/*.'
|
||||
"Build sdists and wheels for all packages with the"
|
||||
"specified version number."
|
||||
" sdists and wheels are stored in <package_name>/dist/*."
|
||||
),
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--release',
|
||||
'-r',
|
||||
action='store_true',
|
||||
"--release",
|
||||
"-r",
|
||||
action="store_true",
|
||||
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(
|
||||
'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('version', metavar='version number of the packages')
|
||||
parser_main_repo.add_argument('--hash', metavar='COMMIT_HASH')
|
||||
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("--hash", metavar="COMMIT_HASH")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
@ -217,7 +217,7 @@ And you're done! Congrats!
|
|||
def main():
|
||||
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)
|
||||
|
||||
if options.cleanup:
|
||||
|
@ -230,30 +230,30 @@ def main():
|
|||
for package_dir in package_dirs:
|
||||
cleanup(package_dir)
|
||||
|
||||
zulip_init = os.path.join(REPO_DIR, 'zulip', 'zulip', '__init__.py')
|
||||
set_variable(zulip_init, '__version__', options.build)
|
||||
bots_setup = os.path.join(REPO_DIR, 'zulip_bots', 'setup.py')
|
||||
set_variable(bots_setup, 'ZULIP_BOTS_VERSION', options.build)
|
||||
set_variable(bots_setup, 'IS_PYPA_PACKAGE', True)
|
||||
botserver_setup = os.path.join(REPO_DIR, 'zulip_botserver', 'setup.py')
|
||||
set_variable(botserver_setup, 'ZULIP_BOTSERVER_VERSION', options.build)
|
||||
zulip_init = os.path.join(REPO_DIR, "zulip", "zulip", "__init__.py")
|
||||
set_variable(zulip_init, "__version__", options.build)
|
||||
bots_setup = os.path.join(REPO_DIR, "zulip_bots", "setup.py")
|
||||
set_variable(bots_setup, "ZULIP_BOTS_VERSION", options.build)
|
||||
set_variable(bots_setup, "IS_PYPA_PACKAGE", True)
|
||||
botserver_setup = os.path.join(REPO_DIR, "zulip_botserver", "setup.py")
|
||||
set_variable(botserver_setup, "ZULIP_BOTSERVER_VERSION", options.build)
|
||||
|
||||
for setup_file in setup_py_files:
|
||||
package_name = os.path.basename(os.path.dirname(setup_file))
|
||||
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:
|
||||
dist_dirs = glob.glob(os.path.join(REPO_DIR, '*', 'dist', '*'))
|
||||
dist_dirs = glob.glob(os.path.join(REPO_DIR, "*", "dist", "*"))
|
||||
twine_upload(dist_dirs)
|
||||
|
||||
if options.subcommand == 'update-main-repo':
|
||||
if options.subcommand == "update-main-repo":
|
||||
if options.hash:
|
||||
update_requirements_in_zulip_repo(options.repo, options.version, options.hash)
|
||||
else:
|
||||
update_requirements_in_zulip_repo(options.repo, options.version, options.version)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
38
tools/review
38
tools/review
|
@ -5,63 +5,63 @@ import sys
|
|||
|
||||
|
||||
def exit(message: str) -> None:
|
||||
print('PROBLEM!')
|
||||
print("PROBLEM!")
|
||||
print(message)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run(command: str) -> None:
|
||||
print('\n>>> ' + command)
|
||||
print("\n>>> " + command)
|
||||
subprocess.check_call(command.split())
|
||||
|
||||
|
||||
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:
|
||||
command = 'git rev-parse --abbrev-ref HEAD'
|
||||
command = "git rev-parse --abbrev-ref HEAD"
|
||||
output = check_output(command)
|
||||
return output.strip()
|
||||
|
||||
|
||||
def check_git_pristine() -> None:
|
||||
command = 'git status --porcelain'
|
||||
command = "git status --porcelain"
|
||||
output = check_output(command)
|
||||
if output.strip():
|
||||
exit('Git is not pristine:\n' + output)
|
||||
exit("Git is not pristine:\n" + output)
|
||||
|
||||
|
||||
def ensure_on_clean_master() -> None:
|
||||
branch = get_git_branch()
|
||||
if branch != 'master':
|
||||
exit('You are still on a feature branch: %s' % (branch,))
|
||||
if branch != "master":
|
||||
exit("You are still on a feature branch: %s" % (branch,))
|
||||
check_git_pristine()
|
||||
run('git fetch upstream master')
|
||||
run('git rebase upstream/master')
|
||||
run("git fetch upstream master")
|
||||
run("git rebase upstream/master")
|
||||
|
||||
|
||||
def create_pull_branch(pull_id: int) -> None:
|
||||
run('git fetch upstream pull/%d/head' % (pull_id,))
|
||||
run('git checkout -B review-%s FETCH_HEAD' % (pull_id,))
|
||||
run('git rebase upstream/master')
|
||||
run('git log upstream/master.. --oneline')
|
||||
run('git diff upstream/master.. --name-status')
|
||||
run("git fetch upstream pull/%d/head" % (pull_id,))
|
||||
run("git checkout -B review-%s FETCH_HEAD" % (pull_id,))
|
||||
run("git rebase upstream/master")
|
||||
run("git log upstream/master.. --oneline")
|
||||
run("git diff upstream/master.. --name-status")
|
||||
|
||||
print()
|
||||
print('PR: %d' % (pull_id,))
|
||||
print(subprocess.check_output(['git', 'log', 'HEAD~..', '--pretty=format:Author: %an']))
|
||||
print("PR: %d" % (pull_id,))
|
||||
print(subprocess.check_output(["git", "log", "HEAD~..", "--pretty=format:Author: %an"]))
|
||||
|
||||
|
||||
def review_pr() -> None:
|
||||
try:
|
||||
pull_id = int(sys.argv[1])
|
||||
except Exception:
|
||||
exit('please provide an integer pull request id')
|
||||
exit("please provide an integer pull request id")
|
||||
|
||||
ensure_on_clean_master()
|
||||
create_pull_branch(pull_id)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
review_pr()
|
||||
|
|
|
@ -104,54 +104,54 @@ force_include = [
|
|||
|
||||
parser = argparse.ArgumentParser(description="Run mypy on files tracked by git.")
|
||||
parser.add_argument(
|
||||
'targets',
|
||||
nargs='*',
|
||||
"targets",
|
||||
nargs="*",
|
||||
default=[],
|
||||
help="""files and directories to include in the result.
|
||||
If this is not specified, the current directory is used""",
|
||||
)
|
||||
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(
|
||||
'-a',
|
||||
'--all',
|
||||
dest='all',
|
||||
action='store_true',
|
||||
"-a",
|
||||
"--all",
|
||||
dest="all",
|
||||
action="store_true",
|
||||
default=False,
|
||||
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.""",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-disallow-untyped-defs',
|
||||
dest='disallow_untyped_defs',
|
||||
action='store_false',
|
||||
"--no-disallow-untyped-defs",
|
||||
dest="disallow_untyped_defs",
|
||||
action="store_false",
|
||||
default=True,
|
||||
help="""Don't throw errors when functions are not annotated""",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--scripts-only',
|
||||
dest='scripts_only',
|
||||
action='store_true',
|
||||
"--scripts-only",
|
||||
dest="scripts_only",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="""Only type check extensionless python scripts""",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--warn-unused-ignores',
|
||||
dest='warn_unused_ignores',
|
||||
action='store_true',
|
||||
"--warn-unused-ignores",
|
||||
dest="warn_unused_ignores",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="""Use the --warn-unused-ignores flag with mypy""",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-ignore-missing-imports',
|
||||
dest='ignore_missing_imports',
|
||||
action='store_false',
|
||||
"--no-ignore-missing-imports",
|
||||
dest="ignore_missing_imports",
|
||||
action="store_false",
|
||||
default=True,
|
||||
help="""Don't use the --ignore-missing-imports flag with mypy""",
|
||||
)
|
||||
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()
|
||||
|
||||
|
@ -163,10 +163,10 @@ files_dict = cast(
|
|||
Dict[str, List[str]],
|
||||
lister.list_files(
|
||||
targets=args.targets,
|
||||
ftypes=['py', 'pyi'],
|
||||
ftypes=["py", "pyi"],
|
||||
use_shebang=True,
|
||||
modified_only=args.modified,
|
||||
exclude=exclude + ['stubs'],
|
||||
exclude=exclude + ["stubs"],
|
||||
group_by_ftype=True,
|
||||
extless_only=args.scripts_only,
|
||||
),
|
||||
|
@ -174,18 +174,18 @@ files_dict = cast(
|
|||
|
||||
for inpath in force_include:
|
||||
try:
|
||||
ext = os.path.splitext(inpath)[1].split('.')[1]
|
||||
ext = os.path.splitext(inpath)[1].split(".")[1]
|
||||
except IndexError:
|
||||
ext = 'py' # type: str
|
||||
ext = "py" # type: str
|
||||
files_dict[ext].append(inpath)
|
||||
|
||||
pyi_files = set(files_dict['pyi'])
|
||||
pyi_files = set(files_dict["pyi"])
|
||||
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(
|
||||
[('zulip', []), ('zulip_bots', []), ('zulip_botserver', []), ('tools', [])]
|
||||
[("zulip", []), ("zulip_bots", []), ("zulip_botserver", []), ("tools", [])]
|
||||
)
|
||||
for file_path in python_files:
|
||||
repo = PurePath(file_path).parts[0]
|
||||
|
|
|
@ -13,45 +13,45 @@ os.chdir(os.path.dirname(TOOLS_DIR))
|
|||
def handle_input_and_run_tests_for_package(package_name, path_list):
|
||||
parser = argparse.ArgumentParser(description="Run tests for {}.".format(package_name))
|
||||
parser.add_argument(
|
||||
'--coverage',
|
||||
nargs='?',
|
||||
"--coverage",
|
||||
nargs="?",
|
||||
const=True,
|
||||
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(
|
||||
'--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(
|
||||
'--verbose',
|
||||
'-v',
|
||||
"--verbose",
|
||||
"-v",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='show verbose output (with pytest)',
|
||||
action="store_true",
|
||||
help="show verbose output (with pytest)",
|
||||
)
|
||||
options = parser.parse_args()
|
||||
|
||||
test_session_title = ' Running tests for {} '.format(package_name)
|
||||
header = test_session_title.center(shutil.get_terminal_size().columns, '#')
|
||||
test_session_title = " Running tests for {} ".format(package_name)
|
||||
header = test_session_title.center(shutil.get_terminal_size().columns, "#")
|
||||
print(header)
|
||||
|
||||
if options.coverage:
|
||||
import coverage
|
||||
|
||||
cov = coverage.Coverage(config_file="tools/.coveragerc")
|
||||
if options.coverage == 'combine':
|
||||
if options.coverage == "combine":
|
||||
cov.load()
|
||||
cov.start()
|
||||
|
||||
if options.pytest:
|
||||
location_to_run_in = os.path.join(TOOLS_DIR, '..', *path_list)
|
||||
paths_to_test = ['.']
|
||||
location_to_run_in = os.path.join(TOOLS_DIR, "..", *path_list)
|
||||
paths_to_test = ["."]
|
||||
pytest_options = [
|
||||
'-s', # show output from tests; this hides the progress bar though
|
||||
'-x', # stop on first test failure
|
||||
'--ff', # runs last failure first
|
||||
"-s", # show output from tests; this hides the progress bar though
|
||||
"-x", # stop on first test failure
|
||||
"--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)
|
||||
result = pytest.main(paths_to_test + pytest_options)
|
||||
if result != 0:
|
||||
|
|
|
@ -32,35 +32,35 @@ the tests for xkcd and wikipedia bots):
|
|||
parser = argparse.ArgumentParser(description=description)
|
||||
|
||||
parser.add_argument(
|
||||
'bots_to_test',
|
||||
metavar='bot',
|
||||
nargs='*',
|
||||
"bots_to_test",
|
||||
metavar="bot",
|
||||
nargs="*",
|
||||
default=[],
|
||||
help='specific bots to test (default is all)',
|
||||
help="specific bots to test (default is all)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--coverage',
|
||||
nargs='?',
|
||||
"--coverage",
|
||||
nargs="?",
|
||||
const=True,
|
||||
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(
|
||||
'--error-on-no-init',
|
||||
"--error-on-no-init",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="whether to exit if a bot has tests which won't run due to no __init__.py",
|
||||
)
|
||||
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(
|
||||
'--verbose',
|
||||
'-v',
|
||||
"--verbose",
|
||||
"-v",
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='show verbose output (with pytest)',
|
||||
action="store_true",
|
||||
help="show verbose output (with pytest)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
@ -69,8 +69,8 @@ def main():
|
|||
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(os.path.dirname(TOOLS_DIR))
|
||||
sys.path.insert(0, TOOLS_DIR)
|
||||
bots_dir = os.path.join(TOOLS_DIR, '..', 'zulip_bots/zulip_bots/bots')
|
||||
glob_pattern = bots_dir + '/*/test_*.py'
|
||||
bots_dir = os.path.join(TOOLS_DIR, "..", "zulip_bots/zulip_bots/bots")
|
||||
glob_pattern = bots_dir + "/*/test_*.py"
|
||||
test_modules = glob.glob(glob_pattern)
|
||||
|
||||
# get only the names of bots that have tests
|
||||
|
@ -82,7 +82,7 @@ def main():
|
|||
import coverage
|
||||
|
||||
cov = coverage.Coverage(config_file="tools/.coveragerc")
|
||||
if options.coverage == 'combine':
|
||||
if options.coverage == "combine":
|
||||
cov.load()
|
||||
cov.start()
|
||||
|
||||
|
@ -96,14 +96,14 @@ def main():
|
|||
bots_to_test = {bot for bot in specified_bots if bot not in options.exclude}
|
||||
|
||||
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_options = [
|
||||
'-s', # show output from tests; this hides the progress bar though
|
||||
'-x', # stop on first test failure
|
||||
'--ff', # runs last failure first
|
||||
"-s", # show output from tests; this hides the progress bar though
|
||||
"-x", # stop on first test failure
|
||||
"--ff", # runs last failure first
|
||||
]
|
||||
pytest_options += ['-v'] if options.verbose else []
|
||||
pytest_options += ["-v"] if options.verbose else []
|
||||
os.chdir(bots_dir)
|
||||
result = pytest.main(pytest_bots_to_test + pytest_options)
|
||||
if result != 0:
|
||||
|
@ -142,5 +142,5 @@ def main():
|
|||
print("HTML report saved under directory 'htmlcov'.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
||||
|
||||
if __name__ == '__main__':
|
||||
handle_input_and_run_tests_for_package('Botserver', ['zulip_botserver'])
|
||||
if __name__ == "__main__":
|
||||
handle_input_and_run_tests_for_package("Botserver", ["zulip_botserver"])
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
||||
|
||||
if __name__ == '__main__':
|
||||
handle_input_and_run_tests_for_package('Bot library', ['zulip_bots', 'zulip_bots', 'tests'])
|
||||
if __name__ == "__main__":
|
||||
handle_input_and_run_tests_for_package("Bot library", ["zulip_bots", "zulip_bots", "tests"])
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
from server_lib.test_handler import handle_input_and_run_tests_for_package
|
||||
|
||||
if __name__ == '__main__':
|
||||
handle_input_and_run_tests_for_package('API', ['zulip'])
|
||||
if __name__ == "__main__":
|
||||
handle_input_and_run_tests_for_package("API", ["zulip"])
|
||||
|
|
|
@ -71,10 +71,10 @@ if __name__ == "__main__":
|
|||
all topics within the stream are mirrored as-is without
|
||||
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.add_argument('--stream', action='store_true', help="", default=False)
|
||||
parser.add_argument("--stream", action="store_true", help="", default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
options = interrealm_bridge_config.config
|
||||
|
|
|
@ -29,13 +29,13 @@ if __name__ == "__main__":
|
|||
parser = zulip.add_default_arguments(
|
||||
argparse.ArgumentParser(usage=usage), allow_provisioning=True
|
||||
)
|
||||
parser.add_argument('--irc-server', default=None)
|
||||
parser.add_argument('--port', default=6667)
|
||||
parser.add_argument('--nick-prefix', default=None)
|
||||
parser.add_argument('--channel', default=None)
|
||||
parser.add_argument('--stream', default="general")
|
||||
parser.add_argument('--topic', default="IRC")
|
||||
parser.add_argument('--nickserv-pw', default='')
|
||||
parser.add_argument("--irc-server", default=None)
|
||||
parser.add_argument("--port", default=6667)
|
||||
parser.add_argument("--nick-prefix", default=None)
|
||||
parser.add_argument("--channel", default=None)
|
||||
parser.add_argument("--stream", default="general")
|
||||
parser.add_argument("--topic", default="IRC")
|
||||
parser.add_argument("--nickserv-pw", default="")
|
||||
|
||||
options = parser.parse_args()
|
||||
# Setting the client to irc_mirror is critical for this to work
|
||||
|
|
|
@ -18,7 +18,7 @@ class IRCBot(irc.bot.SingleServerIRCBot):
|
|||
channel: irc.bot.Channel,
|
||||
nickname: str,
|
||||
server: str,
|
||||
nickserv_password: str = '',
|
||||
nickserv_password: str = "",
|
||||
port: int = 6667,
|
||||
) -> None:
|
||||
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:
|
||||
if len(self.nickserv_password) > 0:
|
||||
msg = 'identify %s' % (self.nickserv_password,)
|
||||
c.privmsg('NickServ', msg)
|
||||
msg = "identify %s" % (self.nickserv_password,)
|
||||
c.privmsg("NickServ", msg)
|
||||
c.join(self.channel)
|
||||
|
||||
def forward_to_irc(msg: Dict[str, Any]) -> None:
|
||||
|
|
|
@ -17,8 +17,8 @@ from requests.exceptions import MissingSchema
|
|||
|
||||
import zulip
|
||||
|
||||
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'
|
||||
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"
|
||||
|
||||
# change these templates to change the format of displayed message
|
||||
ZULIP_MESSAGE_TEMPLATE = "**{username}**: {message}"
|
||||
|
@ -77,10 +77,10 @@ def matrix_to_zulip(
|
|||
"""
|
||||
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
|
||||
# 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:
|
||||
try:
|
||||
|
@ -95,31 +95,31 @@ def matrix_to_zulip(
|
|||
except Exception as exception: # XXX This should be more specific
|
||||
# Generally raised when user is forbidden
|
||||
raise Bridge_ZulipFatalException(exception)
|
||||
if result['result'] != 'success':
|
||||
if result["result"] != "success":
|
||||
# Generally raised when API key is invalid
|
||||
raise Bridge_ZulipFatalException(result['msg'])
|
||||
raise Bridge_ZulipFatalException(result["msg"])
|
||||
|
||||
return _matrix_to_zulip
|
||||
|
||||
|
||||
def get_message_content_from_event(event: Dict[str, Any], no_noise: bool) -> Optional[str]:
|
||||
irc_nick = shorten_irc_nick(event['sender'])
|
||||
if event['type'] == "m.room.member":
|
||||
irc_nick = shorten_irc_nick(event["sender"])
|
||||
if event["type"] == "m.room.member":
|
||||
if no_noise:
|
||||
return None
|
||||
# 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
|
||||
if event['membership'] == "join":
|
||||
if event["membership"] == "join":
|
||||
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")
|
||||
elif event['type'] == "m.room.message":
|
||||
if event['content']['msgtype'] == "m.text" or event['content']['msgtype'] == "m.emote":
|
||||
elif event["type"] == "m.room.message":
|
||||
if event["content"]["msgtype"] == "m.text" or event["content"]["msgtype"] == "m.emote":
|
||||
content = ZULIP_MESSAGE_TEMPLATE.format(
|
||||
username=irc_nick, message=event['content']['body']
|
||||
username=irc_nick, message=event["content"]["body"]
|
||||
)
|
||||
else:
|
||||
content = event['type']
|
||||
content = event["type"]
|
||||
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)
|
||||
if message_valid:
|
||||
matrix_username = msg["sender_full_name"].replace(' ', '')
|
||||
matrix_username = msg["sender_full_name"].replace(" ", "")
|
||||
matrix_text = MATRIX_MESSAGE_TEMPLATE.format(
|
||||
username=matrix_username, message=msg["content"]
|
||||
)
|
||||
|
@ -186,25 +186,25 @@ def generate_parser() -> argparse.ArgumentParser:
|
|||
description=description, formatter_class=argparse.RawTextHelpFormatter
|
||||
)
|
||||
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(
|
||||
'--write-sample-config',
|
||||
metavar='PATH',
|
||||
dest='sample_config',
|
||||
"--write-sample-config",
|
||||
metavar="PATH",
|
||||
dest="sample_config",
|
||||
help="Generate a configuration template at the specified location.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--from-zuliprc',
|
||||
metavar='ZULIPRC',
|
||||
dest='zuliprc',
|
||||
"--from-zuliprc",
|
||||
metavar="ZULIPRC",
|
||||
dest="zuliprc",
|
||||
help="Optional path to zuliprc file for bot, when using --write-sample-config",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--show-join-leave',
|
||||
dest='no_noise',
|
||||
"--show-join-leave",
|
||||
dest="no_noise",
|
||||
default=True,
|
||||
action='store_false',
|
||||
action="store_false",
|
||||
help="Enable IRC join/leave events.",
|
||||
)
|
||||
return parser
|
||||
|
@ -218,7 +218,7 @@ def read_configuration(config_file: str) -> Dict[str, Dict[str, str]]:
|
|||
except configparser.Error as 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.")
|
||||
|
||||
# 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(
|
||||
(
|
||||
(
|
||||
'matrix',
|
||||
"matrix",
|
||||
OrderedDict(
|
||||
(
|
||||
('host', 'https://matrix.org'),
|
||||
('username', 'username'),
|
||||
('password', 'password'),
|
||||
('room_id', '#zulip:matrix.org'),
|
||||
("host", "https://matrix.org"),
|
||||
("username", "username"),
|
||||
("password", "password"),
|
||||
("room_id", "#zulip:matrix.org"),
|
||||
)
|
||||
),
|
||||
),
|
||||
(
|
||||
'zulip',
|
||||
"zulip",
|
||||
OrderedDict(
|
||||
(
|
||||
('email', 'glitch-bot@chat.zulip.org'),
|
||||
('api_key', 'aPiKeY'),
|
||||
('site', 'https://chat.zulip.org'),
|
||||
('stream', 'test here'),
|
||||
('topic', 'matrix'),
|
||||
("email", "glitch-bot@chat.zulip.org"),
|
||||
("api_key", "aPiKeY"),
|
||||
("site", "https://chat.zulip.org"),
|
||||
("stream", "test here"),
|
||||
("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
|
||||
|
||||
sample_dict['zulip']['email'] = zuliprc_config['api']['email']
|
||||
sample_dict['zulip']['site'] = zuliprc_config['api']['site']
|
||||
sample_dict['zulip']['api_key'] = zuliprc_config['api']['key']
|
||||
sample_dict["zulip"]["email"] = zuliprc_config["api"]["email"]
|
||||
sample_dict["zulip"]["site"] = zuliprc_config["api"]["site"]
|
||||
sample_dict["zulip"]["api_key"] = zuliprc_config["api"]["key"]
|
||||
|
||||
sample = configparser.ConfigParser()
|
||||
sample.read_dict(sample_dict)
|
||||
with open(target_path, 'w') as target:
|
||||
with open(target_path, "w") as target:
|
||||
sample.write(target)
|
||||
|
||||
|
||||
|
@ -357,5 +357,5 @@ def main() -> None:
|
|||
backoff.fail()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -59,7 +59,7 @@ class MatrixBridgeScriptTests(TestCase):
|
|||
usage = "usage: {} [-h]".format(script_file)
|
||||
description = "Script to bridge"
|
||||
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
|
||||
self.assertTrue(blank_lines)
|
||||
# There should be finite output
|
||||
|
@ -79,9 +79,9 @@ class MatrixBridgeScriptTests(TestCase):
|
|||
def test_write_sample_config_from_zuliprc(self) -> None:
|
||||
zuliprc_template = ["[api]", "email={email}", "key={key}", "site={site}"]
|
||||
zulip_params = {
|
||||
'email': 'foo@bar',
|
||||
'key': 'some_api_key',
|
||||
'site': 'https://some.chat.serverplace',
|
||||
"email": "foo@bar",
|
||||
"key": "some_api_key",
|
||||
"site": "https://some.chat.serverplace",
|
||||
}
|
||||
with new_temp_dir() as tempdir:
|
||||
path = os.path.join(tempdir, sample_config_path)
|
||||
|
@ -103,9 +103,9 @@ class MatrixBridgeScriptTests(TestCase):
|
|||
with open(path) as sample_file:
|
||||
sample_lines = [line.strip() for line in sample_file.readlines()]
|
||||
expected_lines = sample_config_text.split("\n")
|
||||
expected_lines[7] = 'email = {}'.format(zulip_params['email'])
|
||||
expected_lines[8] = 'api_key = {}'.format(zulip_params['key'])
|
||||
expected_lines[9] = 'site = {}'.format(zulip_params['site'])
|
||||
expected_lines[7] = "email = {}".format(zulip_params["email"])
|
||||
expected_lines[8] = "api_key = {}".format(zulip_params["key"])
|
||||
expected_lines[9] = "site = {}".format(zulip_params["site"])
|
||||
self.assertEqual(sample_lines, expected_lines[:-1])
|
||||
|
||||
def test_detect_zuliprc_does_not_exist(self) -> None:
|
||||
|
@ -131,31 +131,31 @@ class MatrixBridgeZulipToMatrixTests(TestCase):
|
|||
valid_msg = dict(
|
||||
sender_email="John@Smith.smith", # must not be equal to config:email
|
||||
type="stream", # Can only mirror Zulip streams
|
||||
display_recipient=valid_zulip_config['stream'],
|
||||
subject=valid_zulip_config['topic'],
|
||||
display_recipient=valid_zulip_config["stream"],
|
||||
subject=valid_zulip_config["topic"],
|
||||
)
|
||||
|
||||
def test_zulip_message_validity_success(self) -> None:
|
||||
zulip_config = self.valid_zulip_config
|
||||
msg = self.valid_msg
|
||||
# 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))
|
||||
|
||||
def test_zulip_message_validity_failure(self) -> None:
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
msg_not_stream = dict(self.valid_msg, type="private")
|
||||
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))
|
||||
|
||||
def test_zulip_to_matrix(self) -> None:
|
||||
|
@ -166,14 +166,14 @@ class MatrixBridgeZulipToMatrixTests(TestCase):
|
|||
msg = dict(self.valid_msg, sender_full_name="John Smith")
|
||||
|
||||
expected = {
|
||||
'hi': '{} hi',
|
||||
'*hi*': '{} *hi*',
|
||||
'**hi**': '{} **hi**',
|
||||
"hi": "{} hi",
|
||||
"*hi*": "{} *hi*",
|
||||
"**hi**": "{} **hi**",
|
||||
}
|
||||
|
||||
for content in expected:
|
||||
send_msg(dict(msg, content=content))
|
||||
|
||||
for (method, params, _), expect in zip(room.method_calls, expected.values()):
|
||||
self.assertEqual(method, 'send_text')
|
||||
self.assertEqual(params[0], expect.format('<JohnSmith>'))
|
||||
self.assertEqual(method, "send_text")
|
||||
self.assertEqual(params[0], expect.format("<JohnSmith>"))
|
||||
|
|
|
@ -55,17 +55,17 @@ class SlackBridge:
|
|||
self.slack_webclient = slack_sdk.WebClient(token=self.slack_config["token"])
|
||||
|
||||
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:
|
||||
if w.startswith('@'):
|
||||
zulip_msg["content"] = zulip_msg["content"].replace(w, '<' + w + '>')
|
||||
if w.startswith("@"):
|
||||
zulip_msg["content"] = zulip_msg["content"].replace(w, "<" + w + ">")
|
||||
|
||||
def replace_slack_id_with_name(self, msg: Dict[str, Any]) -> None:
|
||||
words = msg['text'].split(' ')
|
||||
words = msg["text"].split(" ")
|
||||
for w in words:
|
||||
if w.startswith('<@') and w.endswith('>'):
|
||||
if w.startswith("<@") and w.endswith(">"):
|
||||
_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(msg: Dict[str, Any]) -> None:
|
||||
|
@ -83,25 +83,25 @@ class SlackBridge:
|
|||
return _zulip_to_slack
|
||||
|
||||
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
|
||||
self.slack_id_to_name = {
|
||||
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()}
|
||||
|
||||
@RTMClient.run_on(event='message')
|
||||
@RTMClient.run_on(event="message")
|
||||
def slack_to_zulip(**payload: Any) -> None:
|
||||
msg = payload['data']
|
||||
if msg['channel'] != self.channel:
|
||||
msg = payload["data"]
|
||||
if msg["channel"] != self.channel:
|
||||
return
|
||||
user_id = msg['user']
|
||||
user_id = msg["user"]
|
||||
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:
|
||||
return
|
||||
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(
|
||||
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.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
print("Starting slack mirroring bot")
|
||||
|
|
|
@ -44,7 +44,7 @@ client = zulip.Client(
|
|||
user_agent = "Codebase To Zulip Mirroring script (zulip-devel@googlegroups.com)"
|
||||
|
||||
# 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):
|
||||
try:
|
||||
|
@ -58,7 +58,7 @@ def make_api_call(path: str) -> Optional[List[Dict[str, Any]]]:
|
|||
response = requests.get(
|
||||
"https://api3.codebasehq.com/%s" % (path,),
|
||||
auth=(config.CODEBASE_API_USERNAME, config.CODEBASE_API_KEY),
|
||||
params={'raw': 'True'},
|
||||
params={"raw": "True"},
|
||||
headers={
|
||||
"User-Agent": user_agent,
|
||||
"Content-Type": "application/json",
|
||||
|
@ -86,36 +86,36 @@ def make_url(path: str) -> str:
|
|||
|
||||
|
||||
def handle_event(event: Dict[str, Any]) -> None:
|
||||
event = event['event']
|
||||
event_type = event['type']
|
||||
actor_name = event['actor_name']
|
||||
event = event["event"]
|
||||
event_type = event["type"]
|
||||
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
|
||||
content = None
|
||||
if event_type == 'repository_creation':
|
||||
if event_type == "repository_creation":
|
||||
stream = config.ZULIP_COMMITS_STREAM_NAME
|
||||
|
||||
project_name = raw_props.get('name')
|
||||
project_repo_type = raw_props.get('scm_type')
|
||||
project_name = raw_props.get("name")
|
||||
project_repo_type = raw_props.get("scm_type")
|
||||
|
||||
url = make_url("projects/%s" % (project_link,))
|
||||
scm = "of type %s" % (project_repo_type,) if project_repo_type else ""
|
||||
|
||||
subject = "Repository %s Created" % (project_name,)
|
||||
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
|
||||
|
||||
num_commits = raw_props.get('commits_count')
|
||||
branch = raw_props.get('ref_name')
|
||||
project = raw_props.get('project_name')
|
||||
repo_link = raw_props.get('repository_permalink')
|
||||
deleted_ref = raw_props.get('deleted_ref')
|
||||
new_ref = raw_props.get('new_ref')
|
||||
num_commits = raw_props.get("commits_count")
|
||||
branch = raw_props.get("ref_name")
|
||||
project = raw_props.get("project_name")
|
||||
repo_link = raw_props.get("repository_permalink")
|
||||
deleted_ref = raw_props.get("deleted_ref")
|
||||
new_ref = raw_props.get("new_ref")
|
||||
|
||||
subject = "Push to %s on %s" % (branch, project)
|
||||
|
||||
|
@ -130,20 +130,20 @@ def handle_event(event: Dict[str, Any]) -> None:
|
|||
branch,
|
||||
project,
|
||||
)
|
||||
for commit in raw_props.get('commits'):
|
||||
ref = commit.get('ref')
|
||||
for commit in raw_props.get("commits"):
|
||||
ref = commit.get("ref")
|
||||
url = make_url(
|
||||
"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)
|
||||
elif event_type == 'ticketing_ticket':
|
||||
elif event_type == "ticketing_ticket":
|
||||
stream = config.ZULIP_TICKETS_STREAM_NAME
|
||||
|
||||
num = raw_props.get('number')
|
||||
name = raw_props.get('subject')
|
||||
assignee = raw_props.get('assignee')
|
||||
priority = raw_props.get('priority')
|
||||
num = raw_props.get("number")
|
||||
name = raw_props.get("subject")
|
||||
assignee = raw_props.get("assignee")
|
||||
priority = raw_props.get("priority")
|
||||
url = make_url("projects/%s/tickets/%s" % (project_link, num))
|
||||
|
||||
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"""
|
||||
% (actor_name, num, url, priority, assignee, name)
|
||||
)
|
||||
elif event_type == 'ticketing_note':
|
||||
elif event_type == "ticketing_note":
|
||||
stream = config.ZULIP_TICKETS_STREAM_NAME
|
||||
|
||||
num = raw_props.get('number')
|
||||
name = raw_props.get('subject')
|
||||
body = raw_props.get('content')
|
||||
changes = raw_props.get('changes')
|
||||
num = raw_props.get("number")
|
||||
name = raw_props.get("subject")
|
||||
body = raw_props.get("content")
|
||||
changes = raw_props.get("changes")
|
||||
|
||||
url = make_url("projects/%s/tickets/%s" % (project_link, num))
|
||||
subject = "#%s: %s" % (num, name)
|
||||
|
@ -173,33 +173,33 @@ def handle_event(event: Dict[str, Any]) -> None:
|
|||
body,
|
||||
)
|
||||
|
||||
if 'status_id' in changes:
|
||||
status_change = changes.get('status_id')
|
||||
if "status_id" in changes:
|
||||
status_change = changes.get("status_id")
|
||||
content += "Status changed from **%s** to **%s**\n\n" % (
|
||||
status_change[0],
|
||||
status_change[1],
|
||||
)
|
||||
elif event_type == 'ticketing_milestone':
|
||||
elif event_type == "ticketing_milestone":
|
||||
stream = config.ZULIP_TICKETS_STREAM_NAME
|
||||
|
||||
name = raw_props.get('name')
|
||||
identifier = raw_props.get('identifier')
|
||||
name = raw_props.get("name")
|
||||
identifier = raw_props.get("identifier")
|
||||
url = make_url("projects/%s/milestone/%s" % (project_link, identifier))
|
||||
|
||||
subject = name
|
||||
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
|
||||
|
||||
comment = raw_props.get('content')
|
||||
commit = raw_props.get('commit_ref')
|
||||
comment = raw_props.get("content")
|
||||
commit = raw_props.get("commit_ref")
|
||||
|
||||
# If there's a commit id, it's a comment to a commit
|
||||
if commit:
|
||||
repo_link = raw_props.get('repository_permalink')
|
||||
repo_link = raw_props.get("repository_permalink")
|
||||
|
||||
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)
|
||||
|
@ -223,14 +223,14 @@ def handle_event(event: Dict[str, Any]) -> None:
|
|||
else:
|
||||
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
|
||||
|
||||
start_ref = raw_props.get('start_ref')
|
||||
end_ref = raw_props.get('end_ref')
|
||||
environment = raw_props.get('environment')
|
||||
servers = raw_props.get('servers')
|
||||
repo_link = raw_props.get('repository_permalink')
|
||||
start_ref = raw_props.get("start_ref")
|
||||
end_ref = raw_props.get("end_ref")
|
||||
environment = raw_props.get("environment")
|
||||
servers = raw_props.get("servers")
|
||||
repo_link = raw_props.get("repository_permalink")
|
||||
|
||||
start_ref_url = make_url(
|
||||
"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])
|
||||
)
|
||||
|
||||
elif event_type == 'named_tree':
|
||||
elif event_type == "named_tree":
|
||||
# Docs say named_tree type used for new/deleting branches and tags,
|
||||
# but experimental testing showed that they were all sent as 'push' events
|
||||
pass
|
||||
elif event_type == 'wiki_page':
|
||||
elif event_type == "wiki_page":
|
||||
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")
|
||||
elif event_type == 'sprint_ended':
|
||||
elif event_type == "sprint_ended":
|
||||
logging.warn("Sprint notifications not yet implemented")
|
||||
else:
|
||||
logging.info("Unknown event type %s, ignoring!" % (event_type,))
|
||||
|
||||
if subject and content:
|
||||
if len(subject) > 60:
|
||||
subject = subject[:57].rstrip() + '...'
|
||||
subject = subject[:57].rstrip() + "..."
|
||||
|
||||
res = client.send_message(
|
||||
{"type": "stream", "to": stream, "subject": subject, "content": content}
|
||||
)
|
||||
if res['result'] == 'success':
|
||||
logging.info("Successfully sent Zulip with id: %s" % (res['id'],))
|
||||
if res["result"] == "success":
|
||||
logging.info("Successfully sent Zulip with id: %s" % (res["id"],))
|
||||
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
|
||||
|
@ -295,7 +295,7 @@ def run_mirror() -> None:
|
|||
try:
|
||||
with open(config.RESUME_FILE) as f:
|
||||
timestamp = f.read()
|
||||
if timestamp == '':
|
||||
if timestamp == "":
|
||||
since = default_since()
|
||||
else:
|
||||
since = datetime.fromtimestamp(float(timestamp), tz=pytz.utc)
|
||||
|
@ -310,7 +310,7 @@ def run_mirror() -> None:
|
|||
if events is not None:
|
||||
sleepInterval = 1
|
||||
for event in events[::-1]:
|
||||
timestamp = event.get('event', {}).get('timestamp', '')
|
||||
timestamp = event.get("event", {}).get("timestamp", "")
|
||||
event_date = dateutil.parser.parse(timestamp)
|
||||
if event_date > since:
|
||||
handle_event(event)
|
||||
|
@ -322,7 +322,7 @@ def run_mirror() -> None:
|
|||
time.sleep(sleepInterval)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
|
|
@ -43,19 +43,19 @@ def git_repository_name() -> Text:
|
|||
|
||||
def git_commit_range(oldrev: str, newrev: str) -> str:
|
||||
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():
|
||||
author_email, commit_id, subject = ln.split(None, 2)
|
||||
if hasattr(config, "format_commit_message"):
|
||||
commits += config.format_commit_message(author_email, subject, commit_id)
|
||||
else:
|
||||
commits += '!avatar(%s) %s\n' % (author_email, subject)
|
||||
commits += "!avatar(%s) %s\n" % (author_email, subject)
|
||||
return commits
|
||||
|
||||
|
||||
def send_bot_message(oldrev: str, newrev: str, refname: str) -> None:
|
||||
repo_name = git_repository_name()
|
||||
branch = refname.replace('refs/heads/', '')
|
||||
branch = refname.replace("refs/heads/", "")
|
||||
destination = config.commit_notice_destination(repo_name, branch, newrev)
|
||||
if destination is None:
|
||||
# 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]
|
||||
|
||||
if (
|
||||
oldrev == '0000000000000000000000000000000000000000'
|
||||
or newrev == '0000000000000000000000000000000000000000'
|
||||
oldrev == "0000000000000000000000000000000000000000"
|
||||
or newrev == "0000000000000000000000000000000000000000"
|
||||
):
|
||||
# New branch pushed or old branch removed
|
||||
added = ''
|
||||
removed = ''
|
||||
added = ""
|
||||
removed = ""
|
||||
else:
|
||||
added = git_commit_range(oldrev, newrev)
|
||||
removed = git_commit_range(newrev, oldrev)
|
||||
|
||||
if oldrev == '0000000000000000000000000000000000000000':
|
||||
message = '`%s` was pushed to new branch `%s`' % (new_head, branch)
|
||||
elif newrev == '0000000000000000000000000000000000000000':
|
||||
message = 'branch `%s` was removed (was `%s`)' % (branch, old_head)
|
||||
if oldrev == "0000000000000000000000000000000000000000":
|
||||
message = "`%s` was pushed to new branch `%s`" % (new_head, branch)
|
||||
elif newrev == "0000000000000000000000000000000000000000":
|
||||
message = "branch `%s` was removed (was `%s`)" % (branch, old_head)
|
||||
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:
|
||||
message += '\n**and adding**:\n\n' + added
|
||||
message += '\n**A HISTORY REWRITE HAS OCCURRED!**'
|
||||
message += '\n@everyone: Please check your local branches to deal with this.'
|
||||
message += "\n**and adding**:\n\n" + added
|
||||
message += "\n**A HISTORY REWRITE HAS OCCURRED!**"
|
||||
message += "\n@everyone: Please check your local branches to deal with this."
|
||||
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:
|
||||
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 = {
|
||||
"type": "stream",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from typing import Dict, Optional, Text
|
||||
|
||||
# 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
|
||||
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)
|
||||
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
|
||||
|
|
|
@ -18,12 +18,12 @@ except ImportError:
|
|||
# at zulip/bots/gcal/
|
||||
# NOTE: When adding more scopes, add them after the previous one in the same field, with a space
|
||||
# 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 client's data.
|
||||
CLIENT_SECRET_FILE = 'client_secret.json'
|
||||
APPLICATION_NAME = 'Zulip Calendar Bot'
|
||||
HOME_DIR = os.path.expanduser('~')
|
||||
CLIENT_SECRET_FILE = "client_secret.json"
|
||||
APPLICATION_NAME = "Zulip Calendar Bot"
|
||||
HOME_DIR = os.path.expanduser("~")
|
||||
|
||||
|
||||
def get_credentials() -> client.Credentials:
|
||||
|
@ -36,7 +36,7 @@ def get_credentials() -> client.Credentials:
|
|||
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)
|
||||
credentials = store.get()
|
||||
|
@ -50,7 +50,7 @@ def get_credentials() -> client.Credentials:
|
|||
credentials = tools.run_flow(flow, store, flags)
|
||||
else: # Needed only for compatibility with Python 2.6
|
||||
credentials = tools.run(flow, store)
|
||||
print('Storing credentials to ' + credential_path)
|
||||
print("Storing credentials to " + credential_path)
|
||||
|
||||
|
||||
get_credentials()
|
||||
|
|
|
@ -20,15 +20,15 @@ from oauth2client.file import Storage
|
|||
try:
|
||||
from googleapiclient import discovery
|
||||
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
|
||||
|
||||
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
|
||||
CLIENT_SECRET_FILE = 'client_secret.json'
|
||||
APPLICATION_NAME = 'Zulip'
|
||||
HOME_DIR = os.path.expanduser('~')
|
||||
SCOPES = "https://www.googleapis.com/auth/calendar.readonly"
|
||||
CLIENT_SECRET_FILE = "client_secret.json"
|
||||
APPLICATION_NAME = "Zulip"
|
||||
HOME_DIR = os.path.expanduser("~")
|
||||
|
||||
# Our cached view of the calendar, updated periodically.
|
||||
events = [] # type: List[Tuple[int, datetime.datetime, str]]
|
||||
|
@ -61,28 +61,28 @@ google-calendar --calendar calendarID@example.calendar.google.com
|
|||
|
||||
|
||||
parser.add_argument(
|
||||
'--interval',
|
||||
dest='interval',
|
||||
"--interval",
|
||||
dest="interval",
|
||||
default=30,
|
||||
type=int,
|
||||
action='store',
|
||||
help='Minutes before event for reminder [default: 30]',
|
||||
metavar='MINUTES',
|
||||
action="store",
|
||||
help="Minutes before event for reminder [default: 30]",
|
||||
metavar="MINUTES",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--calendar',
|
||||
dest='calendarID',
|
||||
default='primary',
|
||||
"--calendar",
|
||||
dest="calendarID",
|
||||
default="primary",
|
||||
type=str,
|
||||
action='store',
|
||||
help='Calendar ID for the calendar you want to receive reminders from.',
|
||||
action="store",
|
||||
help="Calendar ID for the calendar you want to receive reminders from.",
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
if not (options.zulip_email):
|
||||
parser.error('You must specify --user')
|
||||
parser.error("You must specify --user")
|
||||
|
||||
zulip_client = zulip.init_from_options(options)
|
||||
|
||||
|
@ -98,14 +98,14 @@ def get_credentials() -> client.Credentials:
|
|||
Credentials, the obtained credential.
|
||||
"""
|
||||
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)
|
||||
credentials = store.get()
|
||||
|
||||
return credentials
|
||||
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:
|
||||
logging.error("Run the get-google-credentials script from this directory first.")
|
||||
|
||||
|
@ -115,7 +115,7 @@ def populate_events() -> Optional[None]:
|
|||
|
||||
credentials = get_credentials()
|
||||
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()
|
||||
feed = (
|
||||
|
@ -125,7 +125,7 @@ def populate_events() -> Optional[None]:
|
|||
timeMin=now,
|
||||
maxResults=5,
|
||||
singleEvents=True,
|
||||
orderBy='startTime',
|
||||
orderBy="startTime",
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
@ -174,10 +174,10 @@ def send_reminders() -> Optional[None]:
|
|||
key = (id, start)
|
||||
if key not in sent:
|
||||
if start.hour == 0 and start.minute == 0:
|
||||
line = '%s is today.' % (summary,)
|
||||
line = "%s is today." % (summary,)
|
||||
else:
|
||||
line = '%s starts at %s' % (summary, start.strftime('%H:%M'))
|
||||
print('Sending reminder:', line)
|
||||
line = "%s starts at %s" % (summary, start.strftime("%H:%M"))
|
||||
print("Sending reminder:", line)
|
||||
messages.append(line)
|
||||
keys.add(key)
|
||||
|
||||
|
@ -185,12 +185,12 @@ def send_reminders() -> Optional[None]:
|
|||
return
|
||||
|
||||
if len(messages) == 1:
|
||||
message = 'Reminder: ' + messages[0]
|
||||
message = "Reminder: " + messages[0]
|
||||
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(
|
||||
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)
|
||||
|
|
|
@ -94,7 +94,7 @@ def send_zulip(
|
|||
def get_config(ui: ui, item: str) -> str:
|
||||
try:
|
||||
# config returns configuration value.
|
||||
return ui.config('zulip', item)
|
||||
return ui.config("zulip", item)
|
||||
except IndexError:
|
||||
ui.warn("Zulip: Could not find required item {} in hg config.".format(item))
|
||||
sys.exit(1)
|
||||
|
|
|
@ -62,7 +62,7 @@ def stream_to_room(stream: str) -> str:
|
|||
|
||||
|
||||
def jid_to_zulip(jid: JID) -> str:
|
||||
suffix = ''
|
||||
suffix = ""
|
||||
if not jid.username.endswith("-bot"):
|
||||
suffix = options.zulip_email_suffix
|
||||
return "%s%s@%s" % (jid.username, suffix, options.zulip_domain)
|
||||
|
@ -94,10 +94,10 @@ class JabberToZulipBot(ClientXMPP):
|
|||
self.zulip = None
|
||||
self.use_ipv6 = False
|
||||
|
||||
self.register_plugin('xep_0045') # Jabber chatrooms
|
||||
self.register_plugin('xep_0199') # XMPP Ping
|
||||
self.register_plugin("xep_0045") # Jabber chatrooms
|
||||
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
|
||||
|
||||
def session_start(self, event: Dict[str, Any]) -> None:
|
||||
|
@ -112,7 +112,7 @@ class JabberToZulipBot(ClientXMPP):
|
|||
logging.debug("Joining " + room)
|
||||
self.rooms.add(room)
|
||||
muc_jid = JID(local=room, domain=options.conference_domain)
|
||||
xep0045 = self.plugin['xep_0045']
|
||||
xep0045 = self.plugin["xep_0045"]
|
||||
try:
|
||||
xep0045.joinMUC(muc_jid, self.nick, wait=True)
|
||||
except InvalidJID:
|
||||
|
@ -137,7 +137,7 @@ class JabberToZulipBot(ClientXMPP):
|
|||
logging.debug("Leaving " + room)
|
||||
self.rooms.remove(room)
|
||||
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:
|
||||
try:
|
||||
|
@ -152,7 +152,7 @@ class JabberToZulipBot(ClientXMPP):
|
|||
logging.exception("Error forwarding Jabber => Zulip")
|
||||
|
||||
def private(self, msg: JabberMessage) -> None:
|
||||
if options.mode == 'public' or msg['thread'] == '\u1FFFE':
|
||||
if options.mode == "public" or msg["thread"] == "\u1FFFE":
|
||||
return
|
||||
sender = jid_to_zulip(msg["from"])
|
||||
recipient = jid_to_zulip(msg["to"])
|
||||
|
@ -168,13 +168,13 @@ class JabberToZulipBot(ClientXMPP):
|
|||
logging.error(str(ret))
|
||||
|
||||
def group(self, msg: JabberMessage) -> None:
|
||||
if options.mode == 'personal' or msg["thread"] == '\u1FFFE':
|
||||
if options.mode == "personal" or msg["thread"] == "\u1FFFE":
|
||||
return
|
||||
|
||||
subject = msg["subject"]
|
||||
if len(subject) == 0:
|
||||
subject = "(no topic)"
|
||||
stream = room_to_stream(msg['from'].local)
|
||||
stream = room_to_stream(msg["from"].local)
|
||||
sender_nick = msg.get_mucnick()
|
||||
if not sender_nick:
|
||||
# Messages from the room itself have no nickname. We should not try
|
||||
|
@ -195,9 +195,9 @@ class JabberToZulipBot(ClientXMPP):
|
|||
logging.error(str(ret))
|
||||
|
||||
def nickname_to_jid(self, room: str, nick: str) -> JID:
|
||||
jid = self.plugin['xep_0045'].getJidProperty(room, nick, "jid")
|
||||
if jid is None or jid == '':
|
||||
return JID(local=nick.replace(' ', ''), domain=self.boundjid.domain)
|
||||
jid = self.plugin["xep_0045"].getJidProperty(room, nick, "jid")
|
||||
if jid is None or jid == "":
|
||||
return JID(local=nick.replace(" ", ""), domain=self.boundjid.domain)
|
||||
else:
|
||||
return jid
|
||||
|
||||
|
@ -211,59 +211,59 @@ class ZulipToJabberBot:
|
|||
self.jabber = client
|
||||
|
||||
def process_event(self, event: Dict[str, Any]) -> None:
|
||||
if event['type'] == 'message':
|
||||
if event["type"] == "message":
|
||||
message = event["message"]
|
||||
if message['sender_email'] != self.client.email:
|
||||
if message["sender_email"] != self.client.email:
|
||||
return
|
||||
|
||||
try:
|
||||
if message['type'] == 'stream':
|
||||
if message["type"] == "stream":
|
||||
self.stream_message(message)
|
||||
elif message['type'] == 'private':
|
||||
elif message["type"] == "private":
|
||||
self.private_message(message)
|
||||
except Exception:
|
||||
logging.exception("Exception forwarding Zulip => Jabber")
|
||||
elif event['type'] == 'subscription':
|
||||
elif event["type"] == "subscription":
|
||||
self.process_subscription(event)
|
||||
|
||||
def stream_message(self, msg: Dict[str, str]) -> None:
|
||||
assert self.jabber is not None
|
||||
stream = msg['display_recipient']
|
||||
stream = msg["display_recipient"]
|
||||
if not stream.endswith("/xmpp"):
|
||||
return
|
||||
|
||||
room = stream_to_room(stream)
|
||||
jabber_recipient = JID(local=room, domain=options.conference_domain)
|
||||
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()
|
||||
|
||||
def private_message(self, msg: Dict[str, Any]) -> 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:
|
||||
continue
|
||||
if not recipient["is_mirror_dummy"]:
|
||||
continue
|
||||
recip_email = recipient['email']
|
||||
recip_email = recipient["email"]
|
||||
jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain)
|
||||
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()
|
||||
|
||||
def process_subscription(self, event: Dict[str, Any]) -> None:
|
||||
assert self.jabber is not None
|
||||
if event['op'] == 'add':
|
||||
streams = [s['name'].lower() for s in event['subscriptions']]
|
||||
if event["op"] == "add":
|
||||
streams = [s["name"].lower() for s in event["subscriptions"]]
|
||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||
for stream in streams:
|
||||
self.jabber.join_muc(stream_to_room(stream))
|
||||
if event['op'] == 'remove':
|
||||
streams = [s['name'].lower() for s in event['subscriptions']]
|
||||
if event["op"] == "remove":
|
||||
streams = [s["name"].lower() for s in event["subscriptions"]]
|
||||
streams = [s for s in streams if s.endswith("/xmpp")]
|
||||
for stream in streams:
|
||||
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,))
|
||||
return ret[key]
|
||||
|
||||
if options.mode == 'public':
|
||||
if options.mode == "public":
|
||||
stream_infos = get_stream_infos("streams", zulipToJabber.client.get_streams)
|
||||
else:
|
||||
stream_infos = get_stream_infos("subscriptions", zulipToJabber.client.get_subscriptions)
|
||||
|
||||
rooms = [] # type: List[str]
|
||||
for stream_info in stream_infos:
|
||||
stream = stream_info['name']
|
||||
stream = stream_info["name"]
|
||||
if stream.endswith("/xmpp"):
|
||||
rooms.append(stream_to_room(stream))
|
||||
return rooms
|
||||
|
@ -295,20 +295,20 @@ def config_error(msg: str) -> None:
|
|||
sys.exit(2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
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
|
||||
in their help sections). Keys have the same name as options with hyphens
|
||||
replaced with underscores. Zulip configuration options go in the api section,
|
||||
as normal.'''.replace(
|
||||
as normal.""".replace(
|
||||
"\n", " "
|
||||
)
|
||||
)
|
||||
parser.add_option(
|
||||
'--mode',
|
||||
"--mode",
|
||||
default=None,
|
||||
action='store',
|
||||
action="store",
|
||||
help='''Which mode to run in. Valid options are "personal" and "public". In
|
||||
"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
|
||||
|
@ -319,33 +319,33 @@ user and mirrors messages sent to Jabber rooms to Zulip. Defaults to
|
|||
),
|
||||
)
|
||||
parser.add_option(
|
||||
'--zulip-email-suffix',
|
||||
"--zulip-email-suffix",
|
||||
default=None,
|
||||
action='store',
|
||||
help='''Add the specified suffix to the local part of email addresses constructed
|
||||
action="store",
|
||||
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
|
||||
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
|
||||
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", " "
|
||||
),
|
||||
)
|
||||
parser.add_option(
|
||||
'-d',
|
||||
'--debug',
|
||||
help='set logging to DEBUG. Can not be set via config file.',
|
||||
action='store_const',
|
||||
dest='log_level',
|
||||
"-d",
|
||||
"--debug",
|
||||
help="set logging to DEBUG. Can not be set via config file.",
|
||||
action="store_const",
|
||||
dest="log_level",
|
||||
const=logging.DEBUG,
|
||||
default=logging.INFO,
|
||||
)
|
||||
|
||||
jabber_group = optparse.OptionGroup(parser, "Jabber configuration")
|
||||
jabber_group.add_option(
|
||||
'--jid',
|
||||
"--jid",
|
||||
default=None,
|
||||
action='store',
|
||||
action="store",
|
||||
help="Your Jabber JID. If a resource is specified, "
|
||||
"it will be used as the nickname when joining MUCs. "
|
||||
"Specifying the nickname is mostly useful if you want "
|
||||
|
@ -353,27 +353,27 @@ option does not affect login credentials.'''.replace(
|
|||
"from a dedicated account.",
|
||||
)
|
||||
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(
|
||||
'--conference-domain',
|
||||
"--conference-domain",
|
||||
default=None,
|
||||
action='store',
|
||||
action="store",
|
||||
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-server-address',
|
||||
"--jabber-server-address",
|
||||
default=None,
|
||||
action='store',
|
||||
action="store",
|
||||
help="The hostname of your Jabber server. This is only needed if "
|
||||
"your server is missing SRV records",
|
||||
)
|
||||
jabber_group.add_option(
|
||||
'--jabber-server-port',
|
||||
default='5222',
|
||||
action='store',
|
||||
"--jabber-server-port",
|
||||
default="5222",
|
||||
action="store",
|
||||
help="The port of your Jabber server. This is only needed if "
|
||||
"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-"))
|
||||
(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:
|
||||
default_config_file = zulip.get_default_config_filename()
|
||||
|
@ -422,9 +422,9 @@ option does not affect login credentials.'''.replace(
|
|||
options.mode = "personal"
|
||||
|
||||
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'")
|
||||
|
||||
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__)
|
||||
)
|
||||
# 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:
|
||||
jid = JID(options.jid)
|
||||
|
@ -460,10 +460,10 @@ option does not affect login credentials.'''.replace(
|
|||
zulipToJabber.set_jabber_client(xmpp)
|
||||
|
||||
xmpp.process(block=False)
|
||||
if options.mode == 'public':
|
||||
event_types = ['stream']
|
||||
if options.mode == "public":
|
||||
event_types = ["stream"]
|
||||
else:
|
||||
event_types = ['message', 'subscription']
|
||||
event_types = ["message", "subscription"]
|
||||
|
||||
try:
|
||||
logging.info("Connecting to Zulip.")
|
||||
|
|
|
@ -96,7 +96,7 @@ def process_logs() -> None:
|
|||
# immediately after rotation, this tool won't notice.
|
||||
file_data["last"] = 1
|
||||
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:
|
||||
process_lines(new_lines, log_file)
|
||||
file_data["last"] += len(new_lines)
|
||||
|
|
|
@ -9,19 +9,19 @@ VERSION = "0.9"
|
|||
# In Nagios, "output" means "first line of output", and "long
|
||||
# output" means "other lines of output".
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser()) # type: argparse.ArgumentParser
|
||||
parser.add_argument('--output', default='')
|
||||
parser.add_argument('--long-output', default='')
|
||||
parser.add_argument('--stream', default='nagios')
|
||||
parser.add_argument('--config', default='/etc/nagios3/zuliprc')
|
||||
for opt in ('type', 'host', 'service', 'state'):
|
||||
parser.add_argument('--' + opt)
|
||||
parser.add_argument("--output", default="")
|
||||
parser.add_argument("--long-output", default="")
|
||||
parser.add_argument("--stream", default="nagios")
|
||||
parser.add_argument("--config", default="/etc/nagios3/zuliprc")
|
||||
for opt in ("type", "host", "service", "state"):
|
||||
parser.add_argument("--" + opt)
|
||||
opts = parser.parse_args()
|
||||
|
||||
client = zulip.Client(
|
||||
config_file=opts.config, client="ZulipNagios/" + VERSION
|
||||
) # 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
|
||||
# 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.
|
||||
if opts.service is None:
|
||||
# Host notification
|
||||
thing = 'host' # type: Text
|
||||
msg['subject'] = 'host %s' % (opts.host,)
|
||||
thing = "host" # type: Text
|
||||
msg["subject"] = "host %s" % (opts.host,)
|
||||
else:
|
||||
# Service notification
|
||||
thing = 'service'
|
||||
msg['subject'] = 'service %s on %s' % (opts.service, opts.host)
|
||||
thing = "service"
|
||||
msg["subject"] = "service %s on %s" % (opts.service, opts.host)
|
||||
|
||||
if len(msg['subject']) > 60:
|
||||
msg['subject'] = msg['subject'][0:57].rstrip() + "..."
|
||||
if len(msg["subject"]) > 60:
|
||||
msg["subject"] = msg["subject"][0:57].rstrip() + "..."
|
||||
# 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 Nagios mail command uses /usr/bin/printf "%b" to expand these.
|
||||
# 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:
|
||||
# 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)
|
||||
|
|
|
@ -10,7 +10,7 @@ from typing import Dict
|
|||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
import zulip_openshift_config as config
|
||||
|
||||
VERSION = '0.1'
|
||||
VERSION = "0.1"
|
||||
|
||||
if config.ZULIP_API_PATH is not None:
|
||||
sys.path.append(config.ZULIP_API_PATH)
|
||||
|
@ -21,7 +21,7 @@ client = zulip.Client(
|
|||
email=config.ZULIP_USER,
|
||||
site=config.ZULIP_SITE,
|
||||
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:
|
||||
# Activation time - Deployment ID - Git Ref - Git SHA1
|
||||
# 2017-01-07 15:40:30 -0500 - 9e2b7143 - master - b9ce57c - ACTIVE
|
||||
dep = subprocess.check_output(['gear', 'deployments'], universal_newlines=True).splitlines()[1]
|
||||
splits = dep.split(' - ')
|
||||
dep = subprocess.check_output(["gear", "deployments"], universal_newlines=True).splitlines()[1]
|
||||
splits = dep.split(" - ")
|
||||
|
||||
return dict(
|
||||
app_name=os.environ['OPENSHIFT_APP_NAME'],
|
||||
url=os.environ['OPENSHIFT_APP_DNS'],
|
||||
app_name=os.environ["OPENSHIFT_APP_NAME"],
|
||||
url=os.environ["OPENSHIFT_APP_DNS"],
|
||||
branch=splits[2],
|
||||
commit_id=splits[3],
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
# No message should be sent
|
||||
return
|
||||
|
@ -49,10 +49,10 @@ def send_bot_message(deployment: Dict[str, str]) -> None:
|
|||
|
||||
client.send_message(
|
||||
{
|
||||
'type': 'stream',
|
||||
'to': destination['stream'],
|
||||
'subject': destination['subject'],
|
||||
'content': message,
|
||||
"type": "stream",
|
||||
"to": destination["stream"],
|
||||
"subject": destination["subject"],
|
||||
"content": message,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
from typing import Dict, Optional, Text
|
||||
|
||||
# Change these values to configure authentication for the plugin
|
||||
ZULIP_USER = 'openshift-bot@example.com'
|
||||
ZULIP_API_KEY = '0123456789abcdef0123456789abcdef'
|
||||
ZULIP_USER = "openshift-bot@example.com"
|
||||
ZULIP_API_KEY = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
# deployment_notice_destination() lets you customize where deployment notices
|
||||
# are sent to with the full power of a Python function.
|
||||
|
@ -20,8 +20,8 @@ ZULIP_API_KEY = '0123456789abcdef0123456789abcdef'
|
|||
# * topic "master"
|
||||
# And similarly for branch "test-post-receive" (for use when testing).
|
||||
def deployment_notice_destination(branch: str) -> Optional[Dict[str, Text]]:
|
||||
if branch in ['master', 'test-post-receive']:
|
||||
return dict(stream='deployments', subject='%s' % (branch,))
|
||||
if branch in ["master", "test-post-receive"]:
|
||||
return dict(stream="deployments", subject="%s" % (branch,))
|
||||
|
||||
# Return None for cases where you don't want a notice sent
|
||||
return None
|
||||
|
@ -39,14 +39,14 @@ def deployment_notice_destination(branch: str) -> Optional[Dict[str, Text]]:
|
|||
# * dep_id = deployment id
|
||||
# * dep_time = deployment timestamp
|
||||
def format_deployment_message(
|
||||
app_name: str = '',
|
||||
url: str = '',
|
||||
branch: str = '',
|
||||
commit_id: str = '',
|
||||
dep_id: str = '',
|
||||
dep_time: str = '',
|
||||
app_name: str = "",
|
||||
url: str = "",
|
||||
branch: str = "",
|
||||
commit_id: str = "",
|
||||
dep_id: str = "",
|
||||
dep_time: 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
|
||||
|
@ -54,4 +54,4 @@ def format_deployment_message(
|
|||
ZULIP_API_PATH = None # type: Optional[str]
|
||||
|
||||
# Set this to your Zulip server's API URI
|
||||
ZULIP_SITE = 'https://zulip.example.com'
|
||||
ZULIP_SITE = "https://zulip.example.com"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
'''Zulip notification change-commit hook.
|
||||
"""Zulip notification change-commit hook.
|
||||
|
||||
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
|
||||
|
@ -12,7 +12,7 @@ This specific trigger expects command-line arguments in the form:
|
|||
For example:
|
||||
1234 //depot/security/src/
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
|
@ -43,11 +43,11 @@ try:
|
|||
changelist = int(sys.argv[1]) # type: int
|
||||
changeroot = sys.argv[2] # type: str
|
||||
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)
|
||||
sys.exit(-1)
|
||||
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)
|
||||
sys.exit(-1)
|
||||
|
||||
|
@ -79,7 +79,7 @@ if hasattr(config, "P4_WEB"):
|
|||
|
||||
if p4web is not None:
|
||||
# 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}`.
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ P4_WEB: Optional[str] = None
|
|||
# * stream "depot_subdirectory-commits"
|
||||
# * subject "change_root"
|
||||
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 ("*", "..."):
|
||||
directory = dirs[3]
|
||||
else:
|
||||
|
|
|
@ -21,7 +21,7 @@ import feedparser
|
|||
import zulip
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
) # type: argparse.ArgumentParser
|
||||
parser.add_argument(
|
||||
'--stream',
|
||||
dest='stream',
|
||||
help='The stream to which to send RSS messages.',
|
||||
"--stream",
|
||||
dest="stream",
|
||||
help="The stream to which to send RSS messages.",
|
||||
default="rss",
|
||||
action='store',
|
||||
action="store",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--data-dir',
|
||||
dest='data_dir',
|
||||
help='The directory where feed metadata is stored',
|
||||
"--data-dir",
|
||||
dest="data_dir",
|
||||
help="The directory where feed metadata is stored",
|
||||
default=os.path.join(RSS_DATA_DIR),
|
||||
action='store',
|
||||
action="store",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--feed-file',
|
||||
dest='feed_file',
|
||||
help='The file containing a list of RSS feed URLs to follow, one URL per line',
|
||||
"--feed-file",
|
||||
dest="feed_file",
|
||||
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"),
|
||||
action='store',
|
||||
action="store",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--unwrap',
|
||||
dest='unwrap',
|
||||
action='store_true',
|
||||
help='Convert word-wrapped paragraphs into single lines',
|
||||
"--unwrap",
|
||||
dest="unwrap",
|
||||
action="store_true",
|
||||
help="Convert word-wrapped paragraphs into single lines",
|
||||
default=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
'--math',
|
||||
dest='math',
|
||||
action='store_true',
|
||||
help='Convert $ to $$ (for KaTeX processing)',
|
||||
"--math",
|
||||
dest="math",
|
||||
action="store_true",
|
||||
help="Convert $ to $$ (for KaTeX processing)",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
@ -137,7 +137,7 @@ class MLStripper(HTMLParser):
|
|||
self.fed.append(data)
|
||||
|
||||
def get_data(self) -> str:
|
||||
return ''.join(self.fed)
|
||||
return "".join(self.fed)
|
||||
|
||||
|
||||
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:
|
||||
# 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'
|
||||
return re.sub('(?<=[^\n])\n(?=[^\n])', ' ', body)
|
||||
return re.sub("(?<=[^\n])\n(?=[^\n])", " ", body)
|
||||
|
||||
|
||||
def elide_subject(subject: str) -> str:
|
||||
MAX_TOPIC_LENGTH = 60
|
||||
if len(subject) > MAX_TOPIC_LENGTH:
|
||||
subject = subject[: MAX_TOPIC_LENGTH - 3].rstrip() + '...'
|
||||
subject = subject[: MAX_TOPIC_LENGTH - 3].rstrip() + "..."
|
||||
return subject
|
||||
|
||||
|
||||
|
@ -178,7 +178,7 @@ def send_zulip(entry: Any, feed_name: str) -> Dict[str, Any]:
|
|||
) # type: str
|
||||
|
||||
if opts.math:
|
||||
content = content.replace('$', '$$')
|
||||
content = content.replace("$", "$$")
|
||||
|
||||
message = {
|
||||
"type": "stream",
|
||||
|
|
|
@ -43,7 +43,7 @@ entry = svn.log(path, revision_end=pysvn.Revision(pysvn.opt_revision_kind.number
|
|||
0
|
||||
] # type: Dict[Text, Any]
|
||||
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
|
||||
|
||||
destination = config.commit_notice_destination(path, rev) # type: Optional[Dict[Text, Text]]
|
||||
|
|
|
@ -19,7 +19,7 @@ ZULIP_API_KEY = "0123456789abcdef0123456789abcdef"
|
|||
# * stream "commits"
|
||||
# * topic "branch_name"
|
||||
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"]:
|
||||
return dict(stream="commits", subject="%s" % (repo,))
|
||||
|
||||
|
|
|
@ -100,24 +100,24 @@ class ZulipPlugin(Component):
|
|||
|
||||
content = "%s updated %s" % (author, markdown_ticket_url(ticket))
|
||||
if comment:
|
||||
content += ' with comment: %s\n\n' % (markdown_block(comment),)
|
||||
content += " with comment: %s\n\n" % (markdown_block(comment),)
|
||||
else:
|
||||
content += ":\n\n"
|
||||
field_changes = []
|
||||
for key, value in old_values.items():
|
||||
if key == "description":
|
||||
content += '- Changed %s from %s\n\nto %s' % (
|
||||
content += "- Changed %s from %s\n\nto %s" % (
|
||||
key,
|
||||
markdown_block(value),
|
||||
markdown_block(ticket.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) == "":
|
||||
field_changes.append('%s: **%s** => ""' % (key, old_values.get(key)))
|
||||
else:
|
||||
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)
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
'key': options.trello_api_key,
|
||||
'token': options.trello_token,
|
||||
"key": options.trello_api_key,
|
||||
"token": options.trello_token,
|
||||
}
|
||||
|
||||
trello_response = requests.get(trello_api_url, params=params)
|
||||
|
||||
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)
|
||||
|
||||
board_info_json = trello_response.json()
|
||||
|
||||
return board_info_json['id']
|
||||
return board_info_json["id"]
|
||||
|
||||
|
||||
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 = {
|
||||
'key': options.trello_api_key,
|
||||
'token': options.trello_token,
|
||||
'description': 'Webhook for Zulip integration (From Trello {} to Zulip)'.format(
|
||||
"key": options.trello_api_key,
|
||||
"token": options.trello_token,
|
||||
"description": "Webhook for Zulip integration (From Trello {} to Zulip)".format(
|
||||
options.trello_board_name,
|
||||
),
|
||||
'callbackURL': options.zulip_webhook_url,
|
||||
'idModel': id_model,
|
||||
"callbackURL": options.zulip_webhook_url,
|
||||
"idModel": id_model,
|
||||
}
|
||||
|
||||
trello_response = requests.post(trello_api_url, data=data)
|
||||
|
||||
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)
|
||||
|
||||
webhook_info_json = trello_response.json()
|
||||
|
||||
return webhook_info_json['id']
|
||||
return webhook_info_json["id"]
|
||||
|
||||
|
||||
def create_webhook(options):
|
||||
|
@ -88,20 +88,20 @@ def create_webhook(options):
|
|||
"""
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
if id_webhook:
|
||||
print('Success! The webhook ID is', id_webhook)
|
||||
print("Success! The webhook ID is", id_webhook)
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
|
@ -118,36 +118,36 @@ at <https://zulip.com/integrations/doc/trello>.
|
|||
"""
|
||||
|
||||
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(
|
||||
'--trello-board-id',
|
||||
"--trello-board-id",
|
||||
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(
|
||||
'--trello-api-key',
|
||||
"--trello-api-key",
|
||||
required=True,
|
||||
help=(
|
||||
'Visit https://trello.com/1/appkey/generate to generate '
|
||||
'an APPLICATION_KEY (need to be logged into Trello).'
|
||||
"Visit https://trello.com/1/appkey/generate to generate "
|
||||
"an APPLICATION_KEY (need to be logged into Trello)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--trello-token',
|
||||
"--trello-token",
|
||||
required=True,
|
||||
help=(
|
||||
'Visit https://trello.com/1/appkey/generate and under '
|
||||
'`Developer API Keys`, click on `Token` and generate '
|
||||
'a Trello access token.'
|
||||
"Visit https://trello.com/1/appkey/generate and under "
|
||||
"`Developer API Keys`, click on `Token` and generate "
|
||||
"a Trello access token."
|
||||
),
|
||||
)
|
||||
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()
|
||||
create_webhook(options)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -69,32 +69,32 @@ access token" as well. Fill in the values displayed.
|
|||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser("Fetch tweets from Twitter."))
|
||||
parser.add_argument(
|
||||
'--instructions',
|
||||
action='store_true',
|
||||
help='Show instructions for the twitter bot setup and exit',
|
||||
"--instructions",
|
||||
action="store_true",
|
||||
help="Show instructions for the twitter bot setup and exit",
|
||||
)
|
||||
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(
|
||||
'--stream',
|
||||
dest='stream',
|
||||
help='The stream to which to send tweets',
|
||||
"--stream",
|
||||
dest="stream",
|
||||
help="The stream to which to send tweets",
|
||||
default="twitter",
|
||||
action='store',
|
||||
action="store",
|
||||
)
|
||||
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-users', dest='excluded_users', help='Users 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")
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
|
@ -103,15 +103,15 @@ if opts.instructions:
|
|||
sys.exit()
|
||||
|
||||
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:
|
||||
client_type = 'ZulipTwitterSearch/'
|
||||
client_type = "ZulipTwitterSearch/"
|
||||
CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitterrc_fetchsearch")
|
||||
elif opts.twitter_name:
|
||||
client_type = 'ZulipTwitter/'
|
||||
client_type = "ZulipTwitter/"
|
||||
CONFIGFILE_INTERNAL = os.path.expanduser("~/.zulip_twitteruserrc_fetchuser")
|
||||
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:
|
||||
config = ConfigParser()
|
||||
|
@ -119,10 +119,10 @@ try:
|
|||
config_internal = ConfigParser()
|
||||
config_internal.read(CONFIGFILE_INTERNAL)
|
||||
|
||||
consumer_key = config.get('twitter', 'consumer_key')
|
||||
consumer_secret = config.get('twitter', 'consumer_secret')
|
||||
access_token_key = config.get('twitter', 'access_token_key')
|
||||
access_token_secret = config.get('twitter', 'access_token_secret')
|
||||
consumer_key = config.get("twitter", "consumer_key")
|
||||
consumer_secret = config.get("twitter", "consumer_secret")
|
||||
access_token_key = config.get("twitter", "access_token_key")
|
||||
access_token_secret = config.get("twitter", "access_token_secret")
|
||||
except (NoSectionError, NoOptionError):
|
||||
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")
|
||||
|
||||
try:
|
||||
since_id = config_internal.getint('twitter', 'since_id')
|
||||
since_id = config_internal.getint("twitter", "since_id")
|
||||
except (NoOptionError, NoSectionError):
|
||||
since_id = 0
|
||||
try:
|
||||
previous_twitter_name = config_internal.get('twitter', 'twitter_name')
|
||||
previous_twitter_name = config_internal.get("twitter", "twitter_name")
|
||||
except (NoOptionError, NoSectionError):
|
||||
previous_twitter_name = ''
|
||||
previous_twitter_name = ""
|
||||
try:
|
||||
previous_search_terms = config_internal.get('twitter', 'search_terms')
|
||||
previous_search_terms = config_internal.get("twitter", "search_terms")
|
||||
except (NoOptionError, NoSectionError):
|
||||
previous_search_terms = ''
|
||||
previous_search_terms = ""
|
||||
|
||||
try:
|
||||
import twitter
|
||||
|
@ -242,17 +242,17 @@ for status in statuses[::-1][: opts.limit_tweets]:
|
|||
|
||||
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
|
||||
print("Error sending message to zulip: %s" % ret['msg'])
|
||||
print("Error sending message to zulip: %s" % ret["msg"])
|
||||
break
|
||||
else:
|
||||
since_id = status.id
|
||||
|
||||
if 'twitter' not in config_internal.sections():
|
||||
config_internal.add_section('twitter')
|
||||
config_internal.set('twitter', 'since_id', str(since_id))
|
||||
config_internal.set('twitter', 'search_terms', str(opts.search_terms))
|
||||
config_internal.set('twitter', 'twitter_name', str(opts.twitter_name))
|
||||
if "twitter" not in config_internal.sections():
|
||||
config_internal.add_section("twitter")
|
||||
config_internal.set("twitter", "since_id", str(since_id))
|
||||
config_internal.set("twitter", "search_terms", str(opts.search_terms))
|
||||
config_internal.set("twitter", "twitter_name", str(opts.twitter_name))
|
||||
|
||||
write_config(config_internal, CONFIGFILE_INTERNAL)
|
||||
|
|
|
@ -13,12 +13,12 @@ import zephyr
|
|||
import zulip
|
||||
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('--verbose', dest='verbose', default=False, action='store_true')
|
||||
parser.add_option('--site', dest='site', default=None, action='store')
|
||||
parser.add_option('--sharded', 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("--sharded", default=False, action="store_true")
|
||||
(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)
|
||||
|
||||
|
@ -116,11 +116,11 @@ def send_zephyr(zwrite_args: List[str], content: str) -> bool:
|
|||
# Subscribe to Zulip
|
||||
try:
|
||||
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(res['msg'])
|
||||
logging.error(res["msg"])
|
||||
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:
|
||||
logger.exception("Unexpected error subscribing to Zulips")
|
||||
print_status_and_exit(1)
|
||||
|
@ -129,9 +129,9 @@ except Exception:
|
|||
zephyr_subs_to_add = []
|
||||
for (stream, test) in test_streams:
|
||||
if stream == "message":
|
||||
zephyr_subs_to_add.append((stream, 'personal', mit_user))
|
||||
zephyr_subs_to_add.append((stream, "personal", mit_user))
|
||||
else:
|
||||
zephyr_subs_to_add.append((stream, '*', '*'))
|
||||
zephyr_subs_to_add.append((stream, "*", "*"))
|
||||
|
||||
actually_subscribed = False
|
||||
for tries in range(10):
|
||||
|
@ -263,11 +263,11 @@ logger.info("Starting receiving messages!")
|
|||
|
||||
# receive zulips
|
||||
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(res['msg'])
|
||||
logging.error(res["msg"])
|
||||
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!")
|
||||
|
||||
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 z_foo variables are about the messages we _received_ in Zephyr
|
||||
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)
|
||||
(z_key_counts, z_missing_z, z_missing_h, z_duplicates, z_success) = process_keys(z_contents)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import os
|
|||
import sys
|
||||
import 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
|
||||
|
||||
|
||||
|
@ -18,20 +18,20 @@ def write_public_streams() -> None:
|
|||
# normalization and then lower-casing server-side
|
||||
canonical_cls = unicodedata.normalize("NFKC", stream_name).lower()
|
||||
if canonical_cls in [
|
||||
'security',
|
||||
'login',
|
||||
'network',
|
||||
'ops',
|
||||
'user_locate',
|
||||
'mit',
|
||||
'moof',
|
||||
'wsmonitor',
|
||||
'wg_ctl',
|
||||
'winlogger',
|
||||
'hm_ctl',
|
||||
'hm_stat',
|
||||
'zephyr_admin',
|
||||
'zephyr_ctl',
|
||||
"security",
|
||||
"login",
|
||||
"network",
|
||||
"ops",
|
||||
"user_locate",
|
||||
"mit",
|
||||
"moof",
|
||||
"wsmonitor",
|
||||
"wg_ctl",
|
||||
"winlogger",
|
||||
"hm_ctl",
|
||||
"hm_stat",
|
||||
"zephyr_admin",
|
||||
"zephyr_ctl",
|
||||
]:
|
||||
# These zephyr classes cannot be subscribed to by us, due
|
||||
# to MIT's Zephyr access control settings
|
||||
|
|
|
@ -39,8 +39,8 @@ def to_zulip_username(zephyr_username: str) -> str:
|
|||
(user, realm) = (zephyr_username, "ATHENA.MIT.EDU")
|
||||
if realm.upper() == "ATHENA.MIT.EDU":
|
||||
# Hack to make ctl's fake username setup work :)
|
||||
if user.lower() == 'golem':
|
||||
user = 'ctl'
|
||||
if user.lower() == "golem":
|
||||
user = "ctl"
|
||||
return user.lower() + "@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("@")
|
||||
if "|" not in user:
|
||||
# Hack to make ctl's fake username setup work :)
|
||||
if user.lower() == 'ctl':
|
||||
user = 'golem'
|
||||
if user.lower() == "ctl":
|
||||
user = "golem"
|
||||
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:
|
||||
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()
|
||||
|
@ -85,14 +85,14 @@ def unwrap_lines(body: str) -> str:
|
|||
previous_line = lines[0]
|
||||
for line in lines[1:]:
|
||||
line = line.rstrip()
|
||||
if re.match(r'^\W', line, flags=re.UNICODE) and re.match(
|
||||
r'^\W', previous_line, flags=re.UNICODE
|
||||
if re.match(r"^\W", line, flags=re.UNICODE) and re.match(
|
||||
r"^\W", previous_line, flags=re.UNICODE
|
||||
):
|
||||
result += previous_line + "\n"
|
||||
elif (
|
||||
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)
|
||||
):
|
||||
# Use 2 newlines to separate sections so that we
|
||||
|
@ -122,31 +122,31 @@ def send_zulip(zeph: ZephyrDict) -> Dict[str, Any]:
|
|||
message = {}
|
||||
if options.forward_class_messages:
|
||||
message["forged"] = "yes"
|
||||
message['type'] = zeph['type']
|
||||
message['time'] = zeph['time']
|
||||
message['sender'] = to_zulip_username(zeph['sender'])
|
||||
message["type"] = zeph["type"]
|
||||
message["time"] = zeph["time"]
|
||||
message["sender"] = to_zulip_username(zeph["sender"])
|
||||
if "subject" in zeph:
|
||||
# Truncate the subject to the current limit in Zulip. No
|
||||
# need to do this for stream names, since we're only
|
||||
# subscribed to valid stream names.
|
||||
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"
|
||||
if zeph["stream"] == "message":
|
||||
message['to'] = zeph['subject'].lower()
|
||||
message['subject'] = "instance %s" % (zeph['subject'],)
|
||||
message["to"] = zeph["subject"].lower()
|
||||
message["subject"] = "instance %s" % (zeph["subject"],)
|
||||
elif zeph["stream"] == "tabbott-test5":
|
||||
message['to'] = zeph['subject'].lower()
|
||||
message['subject'] = "test instance %s" % (zeph['subject'],)
|
||||
message["to"] = zeph["subject"].lower()
|
||||
message["subject"] = "test instance %s" % (zeph["subject"],)
|
||||
else:
|
||||
message["to"] = zeph["stream"]
|
||||
else:
|
||||
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:
|
||||
logger.debug("Message is: %s" % (str(message),))
|
||||
return {'result': "success"}
|
||||
return {"result": "success"}
|
||||
|
||||
return zulip_client.send_message(message)
|
||||
|
||||
|
@ -311,13 +311,13 @@ def parse_zephyr_body(zephyr_data: str, notice_format: str) -> Tuple[str, str]:
|
|||
try:
|
||||
(zsig, body) = zephyr_data.split("\x00", 1)
|
||||
if (
|
||||
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'
|
||||
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"
|
||||
):
|
||||
# Logic based off of owl_zephyr_get_message in barnowl
|
||||
fields = body.split('\x00')
|
||||
fields = body.split("\x00")
|
||||
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[1],
|
||||
fields[2],
|
||||
|
@ -327,7 +327,7 @@ def parse_zephyr_body(zephyr_data: str, notice_format: str) -> Tuple[str, str]:
|
|||
except ValueError:
|
||||
(zsig, body) = ("", zephyr_data)
|
||||
# Clean body of any null characters, since they're invalid in our protocol.
|
||||
body = body.replace('\x00', '')
|
||||
body = body.replace("\x00", "")
|
||||
return (zsig, body)
|
||||
|
||||
|
||||
|
@ -350,8 +350,8 @@ def parse_crypt_table(zephyr_class: str, instance: str) -> Optional[str]:
|
|||
continue
|
||||
groups = match.groupdict()
|
||||
if (
|
||||
groups['class'].lower() == zephyr_class
|
||||
and 'keypath' in groups
|
||||
groups["class"].lower() == zephyr_class
|
||||
and "keypath" in groups
|
||||
and groups.get("algorithm") == "AES"
|
||||
):
|
||||
return groups["keypath"]
|
||||
|
@ -453,23 +453,23 @@ def process_notice(notice: "zephyr.ZNotice", log: Optional[IO[str]]) -> None:
|
|||
|
||||
zeph: ZephyrDict
|
||||
zeph = {
|
||||
'time': str(notice.time),
|
||||
'sender': notice.sender,
|
||||
'zsig': zsig, # logged here but not used by app
|
||||
'content': body,
|
||||
"time": str(notice.time),
|
||||
"sender": notice.sender,
|
||||
"zsig": zsig, # logged here but not used by app
|
||||
"content": body,
|
||||
}
|
||||
if is_huddle:
|
||||
zeph['type'] = 'private'
|
||||
zeph['recipient'] = huddle_recipients
|
||||
zeph["type"] = "private"
|
||||
zeph["recipient"] = huddle_recipients
|
||||
elif is_personal:
|
||||
assert notice.recipient is not None
|
||||
zeph['type'] = 'private'
|
||||
zeph['recipient'] = to_zulip_username(notice.recipient)
|
||||
zeph["type"] = "private"
|
||||
zeph["recipient"] = to_zulip_username(notice.recipient)
|
||||
else:
|
||||
zeph['type'] = 'stream'
|
||||
zeph['stream'] = zephyr_class
|
||||
zeph["type"] = "stream"
|
||||
zeph["stream"] = zephyr_class
|
||||
if notice.instance.strip() != "":
|
||||
zeph['subject'] = notice.instance
|
||||
zeph["subject"] = notice.instance
|
||||
else:
|
||||
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)
|
||||
)
|
||||
if log is not None:
|
||||
log.write(json.dumps(zeph) + '\n')
|
||||
log.write(json.dumps(zeph) + "\n")
|
||||
log.flush()
|
||||
|
||||
if os.fork() == 0:
|
||||
|
@ -593,7 +593,7 @@ def zephyr_to_zulip(options: optparse.Values) -> None:
|
|||
zeph["subject"] = zeph["instance"]
|
||||
logger.info(
|
||||
"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)
|
||||
except Exception:
|
||||
|
@ -603,7 +603,7 @@ def zephyr_to_zulip(options: optparse.Values) -> None:
|
|||
logger.info("Successfully initialized; Starting receive loop.")
|
||||
|
||||
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)
|
||||
else:
|
||||
process_loop(None)
|
||||
|
@ -700,10 +700,10 @@ Feedback button or at support@zulip.com."""
|
|||
]
|
||||
|
||||
# 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"])
|
||||
|
||||
if message['type'] == "stream":
|
||||
if message["type"] == "stream":
|
||||
zephyr_class = message["display_recipient"]
|
||||
instance = message["subject"]
|
||||
|
||||
|
@ -725,11 +725,11 @@ Feedback button or at support@zulip.com."""
|
|||
zephyr_class = "message"
|
||||
zwrite_args.extend(["-c", zephyr_class, "-i", instance])
|
||||
logger.info("Forwarding message to class %s, instance %s" % (zephyr_class, instance))
|
||||
elif message['type'] == "private":
|
||||
if len(message['display_recipient']) == 1:
|
||||
elif message["type"] == "private":
|
||||
if len(message["display_recipient"]) == 1:
|
||||
recipient = to_zephyr_username(message["display_recipient"][0]["email"])
|
||||
recipients = [recipient]
|
||||
elif len(message['display_recipient']) == 2:
|
||||
elif len(message["display_recipient"]) == 2:
|
||||
recipient = ""
|
||||
for r in message["display_recipient"]:
|
||||
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]]:
|
||||
parser = optparse.OptionParser()
|
||||
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('--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('--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("--shard", help=optparse.SUPPRESS_HELP)
|
||||
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("--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(
|
||||
'--stream-file-path',
|
||||
dest='stream_file_path',
|
||||
"--stream-file-path",
|
||||
dest="stream_file_path",
|
||||
default="/home/zulip/public_streams",
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
)
|
||||
parser.add_option(
|
||||
'--no-forward-personals',
|
||||
dest='forward_personals',
|
||||
"--no-forward-personals",
|
||||
dest="forward_personals",
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
default=True,
|
||||
action='store_false',
|
||||
action="store_false",
|
||||
)
|
||||
parser.add_option(
|
||||
'--forward-mail-zephyrs',
|
||||
dest='forward_mail_zephyrs',
|
||||
"--forward-mail-zephyrs",
|
||||
dest="forward_mail_zephyrs",
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
default=False,
|
||||
action='store_true',
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_option(
|
||||
'--no-forward-from-zulip',
|
||||
"--no-forward-from-zulip",
|
||||
default=True,
|
||||
dest='forward_from_zulip',
|
||||
dest="forward_from_zulip",
|
||||
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('--sync-subscriptions', 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('--on-startup-command', default=None, help=optparse.SUPPRESS_HELP)
|
||||
parser.add_option('--user', default=os.environ["USER"], help=optparse.SUPPRESS_HELP)
|
||||
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("--ignore-expired-tickets", default=False, action="store_true")
|
||||
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("--user", default=os.environ["USER"], help=optparse.SUPPRESS_HELP)
|
||||
parser.add_option(
|
||||
'--stamp-path',
|
||||
"--stamp-path",
|
||||
default="/afs/athena.mit.edu/user/t/a/tabbott/for_friends",
|
||||
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-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-path", default=None, help=optparse.SUPPRESS_HELP)
|
||||
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(
|
||||
'--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(
|
||||
'--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()
|
||||
|
||||
|
@ -1235,7 +1235,7 @@ or specify the --api-key-file option."""
|
|||
# Personals mirror on behalf of another user.
|
||||
pgrep_query = "%s.*--user=%s" % (pgrep_query, options.user)
|
||||
proc = subprocess.Popen(
|
||||
['pgrep', '-U', os.environ["USER"], "-f", pgrep_query],
|
||||
["pgrep", "-U", os.environ["USER"], "-f", pgrep_query],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ def version() -> str:
|
|||
version_line = next(
|
||||
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
|
||||
|
||||
|
||||
|
@ -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.
|
||||
package_info = dict(
|
||||
name='zulip',
|
||||
name="zulip",
|
||||
version=version(),
|
||||
description='Bindings for the Zulip message API',
|
||||
description="Bindings for the Zulip message API",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
author='Zulip Open Source Project',
|
||||
author_email='zulip-devel@googlegroups.com',
|
||||
author="Zulip Open Source Project",
|
||||
author_email="zulip-devel@googlegroups.com",
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Topic :: Communications :: Chat',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
url='https://www.zulip.org/',
|
||||
python_requires=">=3.6",
|
||||
url="https://www.zulip.org/",
|
||||
project_urls={
|
||||
"Source": "https://github.com/zulip/python-zulip-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,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'zulip-send=zulip.send:main',
|
||||
'zulip-api-examples=zulip.api_examples:main',
|
||||
'zulip-matrix-bridge=integrations.bridge_with_matrix.matrix_bridge:main',
|
||||
'zulip-api=zulip.cli:cli',
|
||||
"console_scripts": [
|
||||
"zulip-send=zulip.send:main",
|
||||
"zulip-api-examples=zulip.api_examples:main",
|
||||
"zulip-matrix-bridge=integrations.bridge_with_matrix.matrix_bridge:main",
|
||||
"zulip-api=zulip.cli:cli",
|
||||
],
|
||||
},
|
||||
package_data={'zulip': ["py.typed"]},
|
||||
package_data={"zulip": ["py.typed"]},
|
||||
) # type: Dict[str, Any]
|
||||
|
||||
setuptools_info = dict(
|
||||
install_requires=[
|
||||
'requests[security]>=0.12.1',
|
||||
'matrix_client',
|
||||
'distro',
|
||||
'click',
|
||||
"requests[security]>=0.12.1",
|
||||
"matrix_client",
|
||||
"distro",
|
||||
"click",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -79,7 +79,7 @@ try:
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
package_info.update(setuptools_info)
|
||||
package_info['packages'] = find_packages(exclude=['tests'])
|
||||
package_info["packages"] = find_packages(exclude=["tests"])
|
||||
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
@ -89,12 +89,12 @@ except ImportError:
|
|||
try:
|
||||
import requests
|
||||
|
||||
assert LooseVersion(requests.__version__) >= LooseVersion('0.12.1')
|
||||
assert LooseVersion(requests.__version__) >= LooseVersion("0.12.1")
|
||||
except (ImportError, AssertionError):
|
||||
print("requests >=0.12.1 is not installed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
package_info['packages'] = ['zulip']
|
||||
package_info["packages"] = ["zulip"]
|
||||
|
||||
|
||||
setup(**package_info)
|
||||
|
|
|
@ -15,8 +15,8 @@ class TestDefaultArguments(TestCase):
|
|||
def test_invalid_arguments(self) -> None:
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum"))
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
with patch('sys.stderr', new=io.StringIO()) as mock_stderr:
|
||||
parser.parse_args(['invalid argument'])
|
||||
with patch("sys.stderr", new=io.StringIO()) as mock_stderr:
|
||||
parser.parse_args(["invalid argument"])
|
||||
self.assertEqual(cm.exception.code, 2)
|
||||
# Assert that invalid arguments exit with printing the full usage (non-standard behavior)
|
||||
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:
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage="lorem ipsum"))
|
||||
test_path = '~/zuliprc'
|
||||
args = parser.parse_args(['--config-file', test_path])
|
||||
test_path = "~/zuliprc"
|
||||
args = parser.parse_args(["--config-file", test_path])
|
||||
with self.assertRaises(ZulipError) as cm:
|
||||
zulip.init_from_options(args)
|
||||
expanded_test_path = os.path.abspath(os.path.expanduser(test_path))
|
||||
self.assertEqual(
|
||||
str(cm.exception),
|
||||
'api_key or email not specified and '
|
||||
'file {} does not exist'.format(expanded_test_path),
|
||||
"api_key or email not specified and "
|
||||
"file {} does not exist".format(expanded_test_path),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
@ -9,17 +9,17 @@ import zulip
|
|||
class TestHashUtilDecode(TestCase):
|
||||
def test_hash_util_decode(self) -> None:
|
||||
tests = [
|
||||
('topic', 'topic'),
|
||||
('.2Edot', '.dot'),
|
||||
('.23stream.20name', '#stream name'),
|
||||
('(no.20topic)', '(no topic)'),
|
||||
('.3Cstrong.3Ebold.3C.2Fstrong.3E', '<strong>bold</strong>'),
|
||||
('.3Asome_emoji.3A', ':some_emoji:'),
|
||||
("topic", "topic"),
|
||||
(".2Edot", ".dot"),
|
||||
(".23stream.20name", "#stream name"),
|
||||
("(no.20topic)", "(no topic)"),
|
||||
(".3Cstrong.3Ebold.3C.2Fstrong.3E", "<strong>bold</strong>"),
|
||||
(".3Asome_emoji.3A", ":some_emoji:"),
|
||||
]
|
||||
for encoded_string, decoded_string in tests:
|
||||
with self.subTest(encoded_string=encoded_string):
|
||||
self.assertEqual(zulip.hash_util_decode(encoded_string), decoded_string)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,11 +11,11 @@ def main() -> None:
|
|||
Prints the path to the Zulip API example scripts."""
|
||||
parser = argparse.ArgumentParser(usage=usage)
|
||||
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()
|
||||
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)):
|
||||
print(examples_path)
|
||||
else:
|
||||
|
@ -26,5 +26,5 @@ Prints the path to the Zulip API example scripts."""
|
|||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -14,17 +14,17 @@ Example: alert-words remove banana
|
|||
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('operation', choices=['get', 'add', 'remove'], type=str)
|
||||
parser.add_argument('words', type=str, nargs='*')
|
||||
parser.add_argument("operation", choices=["get", "add", "remove"], type=str)
|
||||
parser.add_argument("words", type=str, nargs="*")
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
if options.operation == 'get':
|
||||
if options.operation == "get":
|
||||
result = client.get_alert_words()
|
||||
elif options.operation == 'add':
|
||||
elif options.operation == "add":
|
||||
result = client.add_alert_words(options.words)
|
||||
elif options.operation == 'remove':
|
||||
elif options.operation == "remove":
|
||||
result = client.remove_alert_words(options.words)
|
||||
|
||||
print(result)
|
||||
|
|
|
@ -15,10 +15,10 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
|||
import zulip
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('--new-email', required=True)
|
||||
parser.add_argument('--new-password', required=True)
|
||||
parser.add_argument('--new-full-name', required=True)
|
||||
parser.add_argument('--new-short-name', required=True)
|
||||
parser.add_argument("--new-email", required=True)
|
||||
parser.add_argument("--new-password", required=True)
|
||||
parser.add_argument("--new-full-name", required=True)
|
||||
parser.add_argument("--new-short-name", required=True)
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
@ -26,10 +26,10 @@ client = zulip.init_from_options(options)
|
|||
print(
|
||||
client.create_user(
|
||||
{
|
||||
'email': options.new_email,
|
||||
'password': options.new_password,
|
||||
'full_name': options.new_full_name,
|
||||
'short_name': options.new_short_name,
|
||||
"email": options.new_email,
|
||||
"password": options.new_password,
|
||||
"full_name": options.new_full_name,
|
||||
"short_name": options.new_short_name,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@ Example: delete-message 42
|
|||
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('message_id', type=int)
|
||||
parser.add_argument("message_id", type=int)
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -11,7 +11,7 @@ Example: delete-stream 42
|
|||
|
||||
|
||||
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()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -15,9 +15,9 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
|||
import zulip
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('--message-id', type=int, required=True)
|
||||
parser.add_argument('--subject', default="")
|
||||
parser.add_argument('--content', default="")
|
||||
parser.add_argument("--message-id", type=int, required=True)
|
||||
parser.add_argument("--subject", default="")
|
||||
parser.add_argument("--content", default="")
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -19,12 +19,12 @@ def quote(string: str) -> str:
|
|||
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('--stream-id', type=int, required=True)
|
||||
parser.add_argument('--description')
|
||||
parser.add_argument('--new-name')
|
||||
parser.add_argument('--private', action='store_true')
|
||||
parser.add_argument('--announcement-only', action='store_true')
|
||||
parser.add_argument('--history-public-to-subscribers', action='store_true')
|
||||
parser.add_argument("--stream-id", type=int, required=True)
|
||||
parser.add_argument("--description")
|
||||
parser.add_argument("--new-name")
|
||||
parser.add_argument("--private", action="store_true")
|
||||
parser.add_argument("--announcement-only", action="store_true")
|
||||
parser.add_argument("--history-public-to-subscribers", action="store_true")
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
@ -32,12 +32,12 @@ client = zulip.init_from_options(options)
|
|||
print(
|
||||
client.update_stream(
|
||||
{
|
||||
'stream_id': options.stream_id,
|
||||
'description': quote(options.description),
|
||||
'new_name': quote(options.new_name),
|
||||
'is_private': options.private,
|
||||
'is_announcement_only': options.announcement_only,
|
||||
'history_public_to_subscribers': options.history_public_to_subscribers,
|
||||
"stream_id": options.stream_id,
|
||||
"description": quote(options.description),
|
||||
"new_name": quote(options.new_name),
|
||||
"is_private": options.private,
|
||||
"is_announcement_only": options.announcement_only,
|
||||
"history_public_to_subscribers": options.history_public_to_subscribers,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
@ -13,11 +13,11 @@ and store them in JSON format.
|
|||
Example: get-history --stream announce --topic important"""
|
||||
|
||||
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('--topic', help="The topic 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(
|
||||
'--filename',
|
||||
default='history.json',
|
||||
"--filename",
|
||||
default="history.json",
|
||||
help="The file name to store the fetched \
|
||||
history.\n Default 'history.json'",
|
||||
)
|
||||
|
@ -25,19 +25,19 @@ options = parser.parse_args()
|
|||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
narrow = [{'operator': 'stream', 'operand': options.stream}]
|
||||
narrow = [{"operator": "stream", "operand": options.stream}]
|
||||
if options.topic:
|
||||
narrow.append({'operator': 'topic', 'operand': options.topic})
|
||||
narrow.append({"operator": "topic", "operand": options.topic})
|
||||
|
||||
request = {
|
||||
# Initially we have the anchor as 0, so that it starts fetching
|
||||
# from the oldest message in the narrow
|
||||
'anchor': 0,
|
||||
'num_before': 0,
|
||||
'num_after': 1000,
|
||||
'narrow': narrow,
|
||||
'client_gravatar': False,
|
||||
'apply_markdown': False,
|
||||
"anchor": 0,
|
||||
"num_before": 0,
|
||||
"num_after": 1000,
|
||||
"narrow": narrow,
|
||||
"client_gravatar": False,
|
||||
"apply_markdown": False,
|
||||
}
|
||||
|
||||
all_messages = [] # type: List[Dict[str, Any]]
|
||||
|
@ -47,17 +47,17 @@ while not found_newest:
|
|||
result = client.get_messages(request)
|
||||
try:
|
||||
found_newest = result["found_newest"]
|
||||
if result['messages']:
|
||||
if result["messages"]:
|
||||
# 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"])
|
||||
except KeyError:
|
||||
# Might occur when the request is not returned with a success status
|
||||
print('Error occured: Payload was:')
|
||||
print("Error occured: Payload was:")
|
||||
print(result)
|
||||
quit()
|
||||
|
||||
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))
|
||||
|
|
|
@ -17,13 +17,13 @@ Example: get-messages --use-first-unread-anchor --num-before=5 \\
|
|||
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('--anchor', type=int)
|
||||
parser.add_argument('--use-first-unread-anchor', action='store_true')
|
||||
parser.add_argument('--num-before', type=int, required=True)
|
||||
parser.add_argument('--num-after', type=int, required=True)
|
||||
parser.add_argument('--client-gravatar', action='store_true')
|
||||
parser.add_argument('--apply-markdown', action='store_true')
|
||||
parser.add_argument('--narrow')
|
||||
parser.add_argument("--anchor", type=int)
|
||||
parser.add_argument("--use-first-unread-anchor", action="store_true")
|
||||
parser.add_argument("--num-before", type=int, required=True)
|
||||
parser.add_argument("--num-after", type=int, required=True)
|
||||
parser.add_argument("--client-gravatar", action="store_true")
|
||||
parser.add_argument("--apply-markdown", action="store_true")
|
||||
parser.add_argument("--narrow")
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
@ -31,13 +31,13 @@ client = zulip.init_from_options(options)
|
|||
print(
|
||||
client.get_messages(
|
||||
{
|
||||
'anchor': options.anchor,
|
||||
'use_first_unread_anchor': options.use_first_unread_anchor,
|
||||
'num_before': options.num_before,
|
||||
'num_after': options.num_after,
|
||||
'narrow': options.narrow,
|
||||
'client_gravatar': options.client_gravatar,
|
||||
'apply_markdown': options.apply_markdown,
|
||||
"anchor": options.anchor,
|
||||
"use_first_unread_anchor": options.use_first_unread_anchor,
|
||||
"num_before": options.num_before,
|
||||
"num_after": options.num_after,
|
||||
"narrow": options.narrow,
|
||||
"client_gravatar": options.client_gravatar,
|
||||
"apply_markdown": options.apply_markdown,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
@ -12,7 +12,7 @@ Example: get-raw-message 42
|
|||
|
||||
|
||||
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()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -10,7 +10,7 @@ Get all the topics for a specific stream.
|
|||
import zulip
|
||||
|
||||
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()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -10,7 +10,7 @@ Get presence data for another user.
|
|||
import zulip
|
||||
|
||||
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()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -10,7 +10,7 @@ Example: message-history 42
|
|||
"""
|
||||
|
||||
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()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -11,17 +11,17 @@ Example: mute-topic unmute Denmark party
|
|||
"""
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('op', choices=['mute', 'unmute'])
|
||||
parser.add_argument('stream')
|
||||
parser.add_argument('topic')
|
||||
parser.add_argument("op", choices=["mute", "unmute"])
|
||||
parser.add_argument("stream")
|
||||
parser.add_argument("topic")
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
OPERATIONS = {'mute': 'add', 'unmute': 'remove'}
|
||||
OPERATIONS = {"mute": "add", "unmute": "remove"}
|
||||
|
||||
print(
|
||||
client.mute_topic(
|
||||
{'op': OPERATIONS[options.op], 'stream': options.stream, 'topic': options.topic}
|
||||
{"op": OPERATIONS[options.op], "stream": options.stream, "topic": options.topic}
|
||||
)
|
||||
)
|
||||
|
|
|
@ -12,18 +12,18 @@ Example: send-message --type=stream commits --subject="my subject" --message="te
|
|||
Example: send-message user1@example.com user2@example.com
|
||||
"""
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('recipients', nargs='+')
|
||||
parser.add_argument('--subject', default='test')
|
||||
parser.add_argument('--message', default='test message')
|
||||
parser.add_argument('--type', default='private')
|
||||
parser.add_argument("recipients", nargs="+")
|
||||
parser.add_argument("--subject", default="test")
|
||||
parser.add_argument("--message", default="test message")
|
||||
parser.add_argument("--type", default="private")
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
message_data = {
|
||||
'type': options.type,
|
||||
'content': options.message,
|
||||
'subject': options.subject,
|
||||
'to': options.recipients,
|
||||
"type": options.type,
|
||||
"content": options.message,
|
||||
"subject": options.subject,
|
||||
"to": options.recipients,
|
||||
}
|
||||
print(client.send_message(message_data))
|
||||
|
|
|
@ -15,7 +15,7 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
|||
import zulip
|
||||
|
||||
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()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -15,7 +15,7 @@ Specify your Zulip API credentials and server in a ~/.zuliprc file or using the
|
|||
import zulip
|
||||
|
||||
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()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
|
|
@ -12,15 +12,15 @@ Example: update-message-flags remove starred 16 23 42
|
|||
"""
|
||||
|
||||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
parser.add_argument('op', choices=['add', 'remove'])
|
||||
parser.add_argument('flag')
|
||||
parser.add_argument('messages', type=int, nargs='+')
|
||||
parser.add_argument("op", choices=["add", "remove"])
|
||||
parser.add_argument("flag")
|
||||
parser.add_argument("messages", type=int, nargs="+")
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
print(
|
||||
client.update_message_flags(
|
||||
{'op': options.op, 'flag': options.flag, 'messages': options.messages}
|
||||
{"op": options.op, "flag": options.flag, "messages": options.messages}
|
||||
)
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ import zulip
|
|||
|
||||
|
||||
class StringIO(_StringIO):
|
||||
name = '' # https://github.com/python/typeshed/issues/598
|
||||
name = "" # https://github.com/python/typeshed/issues/598
|
||||
|
||||
|
||||
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.add_argument('--file-path', required=True)
|
||||
parser.add_argument("--file-path", required=True)
|
||||
options = parser.parse_args()
|
||||
|
||||
client = zulip.init_from_options(options)
|
||||
|
||||
if options.file_path:
|
||||
file = open(options.file_path, 'rb') # type: IO[Any]
|
||||
file = open(options.file_path, "rb") # type: IO[Any]
|
||||
else:
|
||||
file = StringIO('This is a test file.')
|
||||
file.name = 'test.txt'
|
||||
file = StringIO("This is a test file.")
|
||||
file.name = "test.txt"
|
||||
|
||||
response = client.upload_file(file)
|
||||
|
||||
try:
|
||||
print('File URI: {}'.format(response['uri']))
|
||||
print("File URI: {}".format(response["uri"]))
|
||||
except KeyError:
|
||||
print('Error! API response was: {}'.format(response))
|
||||
print("Error! API response was: {}".format(response))
|
||||
|
|
|
@ -4,7 +4,7 @@ from typing import Any, Dict, List
|
|||
|
||||
import zulip
|
||||
|
||||
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. \
|
||||
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 \
|
||||
|
@ -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 \
|
||||
* 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) \
|
||||
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 \
|
||||
questions in #git help . \
|
||||
* 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 \
|
||||
* 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 \
|
||||
* 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 \
|
||||
more users which you can see below the user status list, grep for "Invite more \
|
||||
users" in terminal.\n \
|
||||
* If you are stuck with something and can\'t figure out what to do you can ask \
|
||||
more users which you can see below the user status list, grep for \"Invite more \
|
||||
users\" in terminal.\n \
|
||||
* 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 \
|
||||
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 \
|
||||
Zulip in course of next few months and if you do a good job at that you \
|
||||
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
|
||||
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
|
||||
streams_to_cancel = ['development help']
|
||||
streams_to_cancel = ["development help"]
|
||||
|
||||
|
||||
def get_watchlist() -> List[Any]:
|
||||
storage = client.get_storage()
|
||||
return list(storage['storage'].values())
|
||||
return list(storage["storage"].values())
|
||||
|
||||
|
||||
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:
|
||||
try:
|
||||
if event['type'] == 'realm_user' and event['op'] == 'add':
|
||||
if event["type"] == "realm_user" and event["op"] == "add":
|
||||
watchlist = get_watchlist()
|
||||
watchlist.append(event['person']['email'])
|
||||
watchlist.append(event["person"]["email"])
|
||||
set_watchlist(watchlist)
|
||||
return
|
||||
if event['type'] == 'message':
|
||||
stream = event['message']['display_recipient']
|
||||
if event["type"] == "message":
|
||||
stream = event["message"]["display_recipient"]
|
||||
if stream not in streams_to_watch and stream not in streams_to_cancel:
|
||||
return
|
||||
watchlist = get_watchlist()
|
||||
if event['message']['sender_email'] in watchlist:
|
||||
watchlist.remove(event['message']['sender_email'])
|
||||
if event["message"]["sender_email"] in watchlist:
|
||||
watchlist.remove(event["message"]["sender_email"])
|
||||
if stream not in streams_to_cancel:
|
||||
client.send_message(
|
||||
{
|
||||
'type': 'private',
|
||||
'to': event['message']['sender_email'],
|
||||
'content': welcome_text.format(event['message']['sender_short_name']),
|
||||
"type": "private",
|
||||
"to": event["message"]["sender_email"],
|
||||
"content": welcome_text.format(event["message"]["sender_short_name"]),
|
||||
}
|
||||
)
|
||||
set_watchlist(watchlist)
|
||||
|
@ -92,7 +92,7 @@ def handle_event(event: Dict[str, Any]) -> None:
|
|||
|
||||
def start_event_handler() -> None:
|
||||
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()
|
||||
|
|
|
@ -10,25 +10,25 @@ import zulip
|
|||
|
||||
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:
|
||||
'''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(
|
||||
'Sending message to stream "%s", subject "%s"... '
|
||||
% (message_data['to'], message_data['subject'])
|
||||
% (message_data["to"], message_data["subject"])
|
||||
)
|
||||
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)
|
||||
if response['result'] == 'success':
|
||||
log.info('Message sent.')
|
||||
if response["result"] == "success":
|
||||
log.info("Message sent.")
|
||||
return True
|
||||
else:
|
||||
log.error(response['msg'])
|
||||
log.error(response["msg"])
|
||||
return False
|
||||
|
||||
|
||||
|
@ -46,27 +46,27 @@ def main() -> int:
|
|||
parser = zulip.add_default_arguments(argparse.ArgumentParser(usage=usage))
|
||||
|
||||
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(
|
||||
'-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(
|
||||
'-s',
|
||||
'--stream',
|
||||
dest='stream',
|
||||
action='store',
|
||||
help='Allows the user to specify a stream for the message.',
|
||||
"-s",
|
||||
"--stream",
|
||||
dest="stream",
|
||||
action="store",
|
||||
help="Allows the user to specify a stream for the message.",
|
||||
)
|
||||
group.add_argument(
|
||||
'-S',
|
||||
'--subject',
|
||||
dest='subject',
|
||||
action='store',
|
||||
help='Allows the user to specify a subject for the message.',
|
||||
"-S",
|
||||
"--subject",
|
||||
dest="subject",
|
||||
action="store",
|
||||
help="Allows the user to specify a subject for the message.",
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
@ -75,11 +75,11 @@ def main() -> int:
|
|||
logging.getLogger().setLevel(logging.INFO)
|
||||
# Sanity check user data
|
||||
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)):
|
||||
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):
|
||||
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)
|
||||
|
||||
|
@ -88,16 +88,16 @@ def main() -> int:
|
|||
|
||||
if options.stream:
|
||||
message_data = {
|
||||
'type': 'stream',
|
||||
'content': options.message,
|
||||
'subject': options.subject,
|
||||
'to': options.stream,
|
||||
"type": "stream",
|
||||
"content": options.message,
|
||||
"subject": options.subject,
|
||||
"to": options.stream,
|
||||
}
|
||||
else:
|
||||
message_data = {
|
||||
'type': 'private',
|
||||
'content': options.message,
|
||||
'to': options.recipients,
|
||||
"type": "private",
|
||||
"content": options.message,
|
||||
"to": options.recipients,
|
||||
}
|
||||
|
||||
if not do_send_message(client, message_data):
|
||||
|
@ -105,5 +105,5 @@ def main() -> int:
|
|||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
|
@ -9,50 +9,50 @@ IS_PYPA_PACKAGE = False
|
|||
|
||||
|
||||
package_data = {
|
||||
'': ['doc.md', '*.conf', 'assets/*'],
|
||||
'zulip_bots': ['py.typed'],
|
||||
"": ["doc.md", "*.conf", "assets/*"],
|
||||
"zulip_bots": ["py.typed"],
|
||||
}
|
||||
|
||||
# IS_PYPA_PACKAGE is set to True by tools/release-packages
|
||||
# before making a PyPA release.
|
||||
if not IS_PYPA_PACKAGE:
|
||||
package_data[''].append('fixtures/*.json')
|
||||
package_data[''].append('logo.*')
|
||||
package_data[""].append("fixtures/*.json")
|
||||
package_data[""].append("logo.*")
|
||||
|
||||
with open("README.md") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
# We should be installable with either setuptools or distutils.
|
||||
package_info = dict(
|
||||
name='zulip_bots',
|
||||
name="zulip_bots",
|
||||
version=ZULIP_BOTS_VERSION,
|
||||
description='Zulip\'s Bot framework',
|
||||
description="Zulip's Bot framework",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
author='Zulip Open Source Project',
|
||||
author_email='zulip-devel@googlegroups.com',
|
||||
author="Zulip Open Source Project",
|
||||
author_email="zulip-devel@googlegroups.com",
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Topic :: Communications :: Chat',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
url='https://www.zulip.org/',
|
||||
python_requires=">=3.6",
|
||||
url="https://www.zulip.org/",
|
||||
project_urls={
|
||||
"Source": "https://github.com/zulip/python-zulip-api/",
|
||||
"Documentation": "https://zulip.com/api",
|
||||
},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'zulip-run-bot=zulip_bots.run:main',
|
||||
'zulip-terminal=zulip_bots.terminal:main',
|
||||
"console_scripts": [
|
||||
"zulip-run-bot=zulip_bots.run:main",
|
||||
"zulip-terminal=zulip_bots.terminal:main",
|
||||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
|
@ -60,12 +60,12 @@ package_info = dict(
|
|||
|
||||
setuptools_info = dict(
|
||||
install_requires=[
|
||||
'pip',
|
||||
'zulip',
|
||||
'html2text',
|
||||
'lxml',
|
||||
'BeautifulSoup4',
|
||||
'typing_extensions',
|
||||
"pip",
|
||||
"zulip",
|
||||
"html2text",
|
||||
"lxml",
|
||||
"BeautifulSoup4",
|
||||
"typing_extensions",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -73,8 +73,8 @@ try:
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
package_info.update(setuptools_info)
|
||||
package_info['packages'] = find_packages()
|
||||
package_info['package_data'] = package_data
|
||||
package_info["packages"] = find_packages()
|
||||
package_info["package_data"] = package_data
|
||||
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
@ -97,17 +97,17 @@ except ImportError:
|
|||
print("{name} is not installed.".format(name=module_name), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
check_dependency_manually('zulip')
|
||||
check_dependency_manually('mock', '2.0.0')
|
||||
check_dependency_manually('html2text')
|
||||
check_dependency_manually('PyDictionary')
|
||||
check_dependency_manually("zulip")
|
||||
check_dependency_manually("mock", "2.0.0")
|
||||
check_dependency_manually("html2text")
|
||||
check_dependency_manually("PyDictionary")
|
||||
|
||||
# Include all submodules under bots/
|
||||
package_list = ['zulip_bots']
|
||||
dirs = os.listdir('zulip_bots/bots/')
|
||||
package_list = ["zulip_bots"]
|
||||
dirs = os.listdir("zulip_bots/bots/")
|
||||
for dir_name in dirs:
|
||||
if os.path.isdir(os.path.join('zulip_bots/bots/', dir_name)):
|
||||
package_list.append('zulip_bots.bots.' + dir_name)
|
||||
package_info['packages'] = package_list
|
||||
if os.path.isdir(os.path.join("zulip_bots/bots/", dir_name)):
|
||||
package_list.append("zulip_bots.bots." + dir_name)
|
||||
package_info["packages"] = package_list
|
||||
|
||||
setup(**package_info)
|
||||
|
|
|
@ -9,31 +9,31 @@ from zulip_bots.lib import BotHandler
|
|||
|
||||
class BaremetricsHandler:
|
||||
def initialize(self, bot_handler: BotHandler) -> None:
|
||||
self.config_info = bot_handler.get_config_info('baremetrics')
|
||||
self.api_key = self.config_info['api_key']
|
||||
self.config_info = bot_handler.get_config_info("baremetrics")
|
||||
self.api_key = self.config_info["api_key"]
|
||||
|
||||
self.auth_header = {'Authorization': 'Bearer ' + self.api_key}
|
||||
self.auth_header = {"Authorization": "Bearer " + self.api_key}
|
||||
|
||||
self.commands = [
|
||||
'help',
|
||||
'list-commands',
|
||||
'account-info',
|
||||
'list-sources',
|
||||
'list-plans <source_id>',
|
||||
'list-customers <source_id>',
|
||||
'list-subscriptions <source_id>',
|
||||
'create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>',
|
||||
"help",
|
||||
"list-commands",
|
||||
"account-info",
|
||||
"list-sources",
|
||||
"list-plans <source_id>",
|
||||
"list-customers <source_id>",
|
||||
"list-subscriptions <source_id>",
|
||||
"create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>",
|
||||
]
|
||||
|
||||
self.descriptions = [
|
||||
'Display bot info',
|
||||
'Display the list of available commands',
|
||||
'Display the account info',
|
||||
'List the sources',
|
||||
'List the plans for the source',
|
||||
'List the customers in the source',
|
||||
'List the subscriptions in the source',
|
||||
'Create a plan in the given source',
|
||||
"Display bot info",
|
||||
"Display the list of available commands",
|
||||
"Display the account info",
|
||||
"List the sources",
|
||||
"List the plans for the source",
|
||||
"List the customers in the source",
|
||||
"List the subscriptions in the source",
|
||||
"Create a plan in the given source",
|
||||
]
|
||||
|
||||
self.check_api_key(bot_handler)
|
||||
|
@ -44,36 +44,36 @@ class BaremetricsHandler:
|
|||
test_query_data = test_query_response.json()
|
||||
|
||||
try:
|
||||
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.')
|
||||
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.")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def usage(self) -> str:
|
||||
return '''
|
||||
return """
|
||||
This bot gives updates about customer behavior, financial performance, and analytics
|
||||
for an organization using the Baremetrics Api.\n
|
||||
Enter `list-commands` to show the list of available commands.
|
||||
Version 1.0
|
||||
'''
|
||||
"""
|
||||
|
||||
def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None:
|
||||
content = message['content'].strip().split()
|
||||
content = message["content"].strip().split()
|
||||
|
||||
if content == []:
|
||||
bot_handler.send_reply(message, 'No Command Specified')
|
||||
bot_handler.send_reply(message, "No Command Specified")
|
||||
return
|
||||
|
||||
content[0] = content[0].lower()
|
||||
|
||||
if content == ['help']:
|
||||
if content == ["help"]:
|
||||
bot_handler.send_reply(message, self.usage())
|
||||
return
|
||||
|
||||
if content == ['list-commands']:
|
||||
response = '**Available Commands:** \n'
|
||||
if content == ["list-commands"]:
|
||||
response = "**Available Commands:** \n"
|
||||
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)
|
||||
return
|
||||
|
@ -85,177 +85,177 @@ class BaremetricsHandler:
|
|||
try:
|
||||
instruction = commands[0]
|
||||
|
||||
if instruction == 'account-info':
|
||||
if instruction == "account-info":
|
||||
return self.get_account_info()
|
||||
|
||||
if instruction == 'list-sources':
|
||||
if instruction == "list-sources":
|
||||
return self.get_sources()
|
||||
|
||||
try:
|
||||
if instruction == 'list-plans':
|
||||
if instruction == "list-plans":
|
||||
return self.get_plans(commands[1])
|
||||
|
||||
if instruction == 'list-customers':
|
||||
if instruction == "list-customers":
|
||||
return self.get_customers(commands[1])
|
||||
|
||||
if instruction == 'list-subscriptions':
|
||||
if instruction == "list-subscriptions":
|
||||
return self.get_subscriptions(commands[1])
|
||||
|
||||
if instruction == 'create-plan':
|
||||
if instruction == "create-plan":
|
||||
if len(commands) == 8:
|
||||
return self.create_plan(commands[1:])
|
||||
else:
|
||||
return 'Invalid number of arguments.'
|
||||
return "Invalid number of arguments."
|
||||
|
||||
except IndexError:
|
||||
return 'Missing Params.'
|
||||
return "Missing Params."
|
||||
except KeyError:
|
||||
return 'Invalid Response From API.'
|
||||
return "Invalid Response From API."
|
||||
|
||||
return 'Invalid Command.'
|
||||
return "Invalid Command."
|
||||
|
||||
def get_account_info(self) -> str:
|
||||
url = "https://api.baremetrics.com/v1/account"
|
||||
account_response = requests.get(url, headers=self.auth_header)
|
||||
|
||||
account_data = account_response.json()
|
||||
account_data = account_data['account']
|
||||
account_data = account_data["account"]
|
||||
|
||||
template = [
|
||||
'**Your account information:**',
|
||||
'Id: {id}',
|
||||
'Company: {company}',
|
||||
'Default Currency: {currency}',
|
||||
"**Your account information:**",
|
||||
"Id: {id}",
|
||||
"Company: {company}",
|
||||
"Default Currency: {currency}",
|
||||
]
|
||||
|
||||
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:
|
||||
url = 'https://api.baremetrics.com/v1/sources'
|
||||
url = "https://api.baremetrics.com/v1/sources"
|
||||
sources_response = requests.get(url, headers=self.auth_header)
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
return response
|
||||
|
||||
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_data = plans_response.json()
|
||||
plans_data = plans_data['plans']
|
||||
plans_data = plans_data["plans"]
|
||||
|
||||
template = '\n'.join(
|
||||
template = "\n".join(
|
||||
[
|
||||
'{_count}.Name: {name}',
|
||||
'Active: {active}',
|
||||
'Interval: {interval}',
|
||||
'Interval Count: {interval_count}',
|
||||
'Amounts:',
|
||||
"{_count}.Name: {name}",
|
||||
"Active: {active}",
|
||||
"Interval: {interval}",
|
||||
"Interval Count: {interval_count}",
|
||||
"Amounts:",
|
||||
]
|
||||
)
|
||||
response = ['**Listing plans:**']
|
||||
response = ["**Listing plans:**"]
|
||||
for index, plan in enumerate(plans_data):
|
||||
response += (
|
||||
[template.format(_count=index + 1, **plan)]
|
||||
+ [' - {amount} {currency}'.format(**amount) for amount in plan['amounts']]
|
||||
+ ['']
|
||||
+ [" - {amount} {currency}".format(**amount) for amount in plan["amounts"]]
|
||||
+ [""]
|
||||
)
|
||||
|
||||
return '\n'.join(response)
|
||||
return "\n".join(response)
|
||||
|
||||
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_data = customers_response.json()
|
||||
customers_data = customers_data['customers']
|
||||
customers_data = customers_data["customers"]
|
||||
|
||||
# FIXME BUG here? mismatch of name and display name?
|
||||
template = '\n'.join(
|
||||
template = "\n".join(
|
||||
[
|
||||
'{_count}.Name: {display_name}',
|
||||
'Display Name: {name}',
|
||||
'OID: {oid}',
|
||||
'Active: {is_active}',
|
||||
'Email: {email}',
|
||||
'Notes: {notes}',
|
||||
'Current Plans:',
|
||||
"{_count}.Name: {display_name}",
|
||||
"Display Name: {name}",
|
||||
"OID: {oid}",
|
||||
"Active: {is_active}",
|
||||
"Email: {email}",
|
||||
"Notes: {notes}",
|
||||
"Current Plans:",
|
||||
]
|
||||
)
|
||||
response = ['**Listing customers:**']
|
||||
response = ["**Listing customers:**"]
|
||||
for index, customer in enumerate(customers_data):
|
||||
response += (
|
||||
[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:
|
||||
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_data = subscriptions_response.json()
|
||||
subscriptions_data = subscriptions_data['subscriptions']
|
||||
subscriptions_data = subscriptions_data["subscriptions"]
|
||||
|
||||
template = '\n'.join(
|
||||
template = "\n".join(
|
||||
[
|
||||
'{_count}.Customer Name: {name}',
|
||||
'Customer Display Name: {display_name}',
|
||||
'Customer OID: {oid}',
|
||||
'Customer Email: {email}',
|
||||
'Active: {_active}',
|
||||
'Plan Name: {_plan_name}',
|
||||
'Plan Amounts:',
|
||||
"{_count}.Customer Name: {name}",
|
||||
"Customer Display Name: {display_name}",
|
||||
"Customer OID: {oid}",
|
||||
"Customer Email: {email}",
|
||||
"Active: {_active}",
|
||||
"Plan Name: {_plan_name}",
|
||||
"Plan Amounts:",
|
||||
]
|
||||
)
|
||||
response = ['**Listing subscriptions:**']
|
||||
response = ["**Listing subscriptions:**"]
|
||||
for index, subscription in enumerate(subscriptions_data):
|
||||
response += (
|
||||
[
|
||||
template.format(
|
||||
_count=index + 1,
|
||||
_active=subscription['active'],
|
||||
_plan_name=subscription['plan']['name'],
|
||||
**subscription['customer'],
|
||||
_active=subscription["active"],
|
||||
_plan_name=subscription["plan"]["name"],
|
||||
**subscription["customer"],
|
||||
)
|
||||
]
|
||||
+ [
|
||||
' - {amount} {symbol}'.format(**amount)
|
||||
for amount in subscription['plan']['amounts']
|
||||
" - {amount} {symbol}".format(**amount)
|
||||
for amount in subscription["plan"]["amounts"]
|
||||
]
|
||||
+ ['']
|
||||
+ [""]
|
||||
)
|
||||
|
||||
return '\n'.join(response)
|
||||
return "\n".join(response)
|
||||
|
||||
def create_plan(self, parameters: List[str]) -> str:
|
||||
data_header = {
|
||||
'oid': parameters[1],
|
||||
'name': parameters[2],
|
||||
'currency': parameters[3],
|
||||
'amount': int(parameters[4]),
|
||||
'interval': parameters[5],
|
||||
'interval_count': int(parameters[6]),
|
||||
"oid": parameters[1],
|
||||
"name": parameters[2],
|
||||
"currency": parameters[3],
|
||||
"amount": int(parameters[4]),
|
||||
"interval": parameters[5],
|
||||
"interval_count": int(parameters[6]),
|
||||
} # 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)
|
||||
if 'error' not in create_plan_response.json():
|
||||
return 'Plan Created.'
|
||||
if "error" not in create_plan_response.json():
|
||||
return "Plan Created."
|
||||
else:
|
||||
return 'Invalid Arguments Error.'
|
||||
return "Invalid Arguments Error."
|
||||
|
||||
|
||||
handler_class = BaremetricsHandler
|
||||
|
|
|
@ -8,121 +8,121 @@ class TestBaremetricsBot(BotTestCase, DefaultTests):
|
|||
bot_name = "baremetrics"
|
||||
|
||||
def test_bot_responds_to_empty_message(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
self.verify_reply('', 'No Command Specified')
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
self.verify_reply("", "No Command Specified")
|
||||
|
||||
def test_help_query(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
self.verify_reply(
|
||||
'help',
|
||||
'''
|
||||
"help",
|
||||
"""
|
||||
This bot gives updates about customer behavior, financial performance, and analytics
|
||||
for an organization using the Baremetrics Api.\n
|
||||
Enter `list-commands` to show the list of available commands.
|
||||
Version 1.0
|
||||
''',
|
||||
""",
|
||||
)
|
||||
|
||||
def test_list_commands_command(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
self.verify_reply(
|
||||
'list-commands',
|
||||
'**Available Commands:** \n'
|
||||
' - help : Display bot info\n'
|
||||
' - list-commands : Display the list of available commands\n'
|
||||
' - account-info : Display the account info\n'
|
||||
' - list-sources : List the sources\n'
|
||||
' - list-plans <source_id> : List the plans for the source\n'
|
||||
' - list-customers <source_id> : List the customers in the source\n'
|
||||
' - list-subscriptions <source_id> : List the subscriptions in the '
|
||||
'source\n'
|
||||
' - create-plan <source_id> <oid> <name> <currency> <amount> <interval> '
|
||||
'<interval_count> : Create a plan in the given source\n',
|
||||
"list-commands",
|
||||
"**Available Commands:** \n"
|
||||
" - help : Display bot info\n"
|
||||
" - list-commands : Display the list of available commands\n"
|
||||
" - account-info : Display the account info\n"
|
||||
" - list-sources : List the sources\n"
|
||||
" - list-plans <source_id> : List the plans for the source\n"
|
||||
" - list-customers <source_id> : List the customers in the source\n"
|
||||
" - list-subscriptions <source_id> : List the subscriptions in the "
|
||||
"source\n"
|
||||
" - create-plan <source_id> <oid> <name> <currency> <amount> <interval> "
|
||||
"<interval_count> : Create a plan in the given source\n",
|
||||
)
|
||||
|
||||
def test_account_info_command(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}):
|
||||
with self.mock_http_conversation('account_info'):
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("account_info"):
|
||||
self.verify_reply(
|
||||
'account-info',
|
||||
'**Your account information:**\nId: 376418\nCompany: NA\nDefault '
|
||||
'Currency: United States Dollar',
|
||||
"account-info",
|
||||
"**Your account information:**\nId: 376418\nCompany: NA\nDefault "
|
||||
"Currency: United States Dollar",
|
||||
)
|
||||
|
||||
def test_list_sources_command(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}):
|
||||
with self.mock_http_conversation('list_sources'):
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("list_sources"):
|
||||
self.verify_reply(
|
||||
'list-sources',
|
||||
'**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: '
|
||||
'baremetrics\nProvider ID: None\n\n',
|
||||
"list-sources",
|
||||
"**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: "
|
||||
"baremetrics\nProvider ID: None\n\n",
|
||||
)
|
||||
|
||||
def test_list_plans_command(self) -> None:
|
||||
r = (
|
||||
'**Listing plans:**\n1.Name: Plan 1\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n'
|
||||
' - 450000 USD\n\n2.Name: Plan 2\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n'
|
||||
' - 450000 USD\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"
|
||||
)
|
||||
|
||||
with self.mock_config_info({'api_key': 'TEST'}):
|
||||
with self.mock_http_conversation('list_plans'):
|
||||
self.verify_reply('list-plans TEST', r)
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("list_plans"):
|
||||
self.verify_reply("list-plans TEST", r)
|
||||
|
||||
def test_list_customers_command(self) -> None:
|
||||
r = (
|
||||
'**Listing customers:**\n1.Name: Customer 1\nDisplay Name: Customer 1\nOID: customer_1\nActive: True\n'
|
||||
'Email: customer_1@baremetrics.com\nNotes: Here are some notes\nCurrent Plans:\n - Plan 1\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"
|
||||
)
|
||||
|
||||
with self.mock_config_info({'api_key': 'TEST'}):
|
||||
with self.mock_http_conversation('list_customers'):
|
||||
self.verify_reply('list-customers TEST', r)
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("list_customers"):
|
||||
self.verify_reply("list-customers TEST", r)
|
||||
|
||||
def test_list_subscriptions_command(self) -> None:
|
||||
r = (
|
||||
'**Listing subscriptions:**\n1.Customer Name: Customer 1\nCustomer Display Name: Customer 1\n'
|
||||
'Customer OID: customer_1\nCustomer Email: customer_1@baremetrics.com\nActive: True\n'
|
||||
'Plan Name: Plan 1\nPlan Amounts:\n - 1000 $\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"
|
||||
"Plan Name: Plan 1\nPlan Amounts:\n - 1000 $\n"
|
||||
)
|
||||
|
||||
with self.mock_config_info({'api_key': 'TEST'}):
|
||||
with self.mock_http_conversation('list_subscriptions'):
|
||||
self.verify_reply('list-subscriptions TEST', r)
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("list_subscriptions"):
|
||||
self.verify_reply("list-subscriptions TEST", r)
|
||||
|
||||
def test_exception_when_api_key_is_invalid(self) -> None:
|
||||
bot_test_instance = BaremetricsHandler()
|
||||
|
||||
with self.mock_config_info({'api_key': 'TEST'}):
|
||||
with self.mock_http_conversation('invalid_api_key'):
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("invalid_api_key"):
|
||||
with self.assertRaises(StubBotHandler.BotQuitException):
|
||||
bot_test_instance.initialize(StubBotHandler())
|
||||
|
||||
def test_invalid_command(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
self.verify_reply('abcd', 'Invalid Command.')
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
self.verify_reply("abcd", "Invalid Command.")
|
||||
|
||||
def test_missing_params(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
self.verify_reply('list-plans', 'Missing Params.')
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
self.verify_reply("list-plans", "Missing Params.")
|
||||
|
||||
def test_key_error(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
with self.mock_http_conversation('test_key_error'):
|
||||
self.verify_reply('list-plans TEST', 'Invalid Response From API.')
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
with self.mock_http_conversation("test_key_error"):
|
||||
self.verify_reply("list-plans TEST", "Invalid Response From API.")
|
||||
|
||||
def test_create_plan_command(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
with self.mock_http_conversation('create_plan'):
|
||||
self.verify_reply('create-plan TEST 1 TEST USD 123 TEST 123', 'Plan Created.')
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
with self.mock_http_conversation("create_plan"):
|
||||
self.verify_reply("create-plan TEST 1 TEST USD 123 TEST 123", "Plan Created.")
|
||||
|
||||
def test_create_plan_error_command(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
with self.mock_http_conversation('create_plan_error'):
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
with self.mock_http_conversation("create_plan_error"):
|
||||
self.verify_reply(
|
||||
'create-plan TEST 1 TEST USD 123 TEST 123', 'Invalid Arguments Error.'
|
||||
"create-plan TEST 1 TEST USD 123 TEST 123", "Invalid Arguments Error."
|
||||
)
|
||||
|
||||
def test_create_plan_argnum_error_command(self) -> None:
|
||||
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
|
||||
self.verify_reply('create-plan alpha beta', 'Invalid number of arguments.')
|
||||
with self.mock_config_info({"api_key": "TEST"}), patch("requests.get"):
|
||||
self.verify_reply("create-plan alpha beta", "Invalid number of arguments.")
|
||||
|
|
|
@ -6,7 +6,7 @@ from requests.exceptions import ConnectionError
|
|||
|
||||
from zulip_bots.lib import BotHandler
|
||||
|
||||
help_message = '''
|
||||
help_message = """
|
||||
You can add datapoints towards your beeminder goals \
|
||||
following the syntax shown below :smile:.\n \
|
||||
\n**@mention-botname daystamp, value, comment**\
|
||||
|
@ -14,22 +14,22 @@ following the syntax shown below :smile:.\n \
|
|||
[**NOTE:** Optional field, default is *current daystamp*],\
|
||||
\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
|
||||
\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
def get_beeminder_response(message_content: str, config_info: Dict[str, str]) -> str:
|
||||
username = config_info['username']
|
||||
goalname = config_info['goalname']
|
||||
auth_token = config_info['auth_token']
|
||||
username = config_info["username"]
|
||||
goalname = config_info["goalname"]
|
||||
auth_token = config_info["auth_token"]
|
||||
|
||||
message_content = message_content.strip()
|
||||
if message_content == '' or message_content == 'help':
|
||||
if message_content == "" or message_content == "help":
|
||||
return help_message
|
||||
|
||||
url = "https://www.beeminder.com/api/v1/users/{}/goals/{}/datapoints.json".format(
|
||||
username, goalname
|
||||
)
|
||||
message_pieces = message_content.split(',')
|
||||
message_pieces = message_content.split(",")
|
||||
for i in range(len(message_pieces)):
|
||||
message_pieces[i] = message_pieces[i].strip()
|
||||
|
||||
|
@ -81,21 +81,21 @@ right now.\nPlease try again later"
|
|||
|
||||
|
||||
class BeeminderHandler:
|
||||
'''
|
||||
"""
|
||||
This plugin allows users to easily add datapoints
|
||||
towards their beeminder goals via zulip
|
||||
'''
|
||||
"""
|
||||
|
||||
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
|
||||
auth_token = self.config_info['auth_token']
|
||||
auth_token = self.config_info["auth_token"]
|
||||
try:
|
||||
r = requests.get(
|
||||
"https://www.beeminder.com/api/v1/users/me.json", params={'auth_token': auth_token}
|
||||
"https://www.beeminder.com/api/v1/users/me.json", params={"auth_token": auth_token}
|
||||
)
|
||||
if r.status_code == 401:
|
||||
bot_handler.quit('Invalid key!')
|
||||
bot_handler.quit("Invalid key!")
|
||||
except ConnectionError as e:
|
||||
logging.exception(str(e))
|
||||
|
||||
|
@ -103,7 +103,7 @@ class BeeminderHandler:
|
|||
return "This plugin allows users to add datapoints towards their Beeminder goals"
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ class TestBeeminderBot(BotTestCase, DefaultTests):
|
|||
bot_name = "beeminder"
|
||||
normal_config = {"auth_token": "XXXXXX", "username": "aaron", "goalname": "goal"}
|
||||
|
||||
help_message = '''
|
||||
help_message = """
|
||||
You can add datapoints towards your beeminder goals \
|
||||
following the syntax shown below :smile:.\n \
|
||||
\n**@mention-botname daystamp, value, comment**\
|
||||
|
@ -17,44 +17,44 @@ following the syntax shown below :smile:.\n \
|
|||
[**NOTE:** Optional field, default is *current daystamp*],\
|
||||
\n* `value`**:** Enter a value [**NOTE:** Required field, can be any number],\
|
||||
\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
|
||||
'''
|
||||
"""
|
||||
|
||||
def test_bot_responds_to_empty_message(self) -> None:
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_valid_auth_token'
|
||||
"test_valid_auth_token"
|
||||
):
|
||||
self.verify_reply('', self.help_message)
|
||||
self.verify_reply("", self.help_message)
|
||||
|
||||
def test_help_message(self) -> None:
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_valid_auth_token'
|
||||
"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:
|
||||
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(
|
||||
'test_valid_auth_token'
|
||||
), self.mock_http_conversation('test_message_with_daystamp_and_value'):
|
||||
self.verify_reply('20180602, 2', bot_response)
|
||||
"test_valid_auth_token"
|
||||
), self.mock_http_conversation("test_message_with_daystamp_and_value"):
|
||||
self.verify_reply("20180602, 2", bot_response)
|
||||
|
||||
def test_message_with_value_and_comment(self) -> None:
|
||||
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
|
||||
bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created."
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_valid_auth_token'
|
||||
), self.mock_http_conversation('test_message_with_value_and_comment'):
|
||||
self.verify_reply('2, hi there!', bot_response)
|
||||
"test_valid_auth_token"
|
||||
), self.mock_http_conversation("test_message_with_value_and_comment"):
|
||||
self.verify_reply("2, hi there!", bot_response)
|
||||
|
||||
def test_message_with_daystamp_and_value_and_comment(self) -> None:
|
||||
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
|
||||
bot_response = "[Datapoint](https://www.beeminder.com/aaron/goal) created."
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_valid_auth_token'
|
||||
), self.mock_http_conversation('test_message_with_daystamp_and_value_and_comment'):
|
||||
self.verify_reply('20180602, 2, hi there!', bot_response)
|
||||
"test_valid_auth_token"
|
||||
), self.mock_http_conversation("test_message_with_daystamp_and_value_and_comment"):
|
||||
self.verify_reply("20180602, 2, hi there!", bot_response)
|
||||
|
||||
def test_syntax_error(self) -> None:
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_valid_auth_token'
|
||||
"test_valid_auth_token"
|
||||
):
|
||||
bot_response = "Make sure you follow the syntax.\n You can take a look \
|
||||
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:
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_valid_auth_token'
|
||||
), patch('requests.post', side_effect=ConnectionError()), patch('logging.exception'):
|
||||
"test_valid_auth_token"
|
||||
), patch("requests.post", side_effect=ConnectionError()), patch("logging.exception"):
|
||||
self.verify_reply(
|
||||
'?$!',
|
||||
'Uh-Oh, couldn\'t process the request \
|
||||
right now.\nPlease try again later',
|
||||
"?$!",
|
||||
"Uh-Oh, couldn't process the request \
|
||||
right now.\nPlease try again later",
|
||||
)
|
||||
|
||||
def test_invalid_when_handle_message(self) -> None:
|
||||
|
@ -75,20 +75,20 @@ right now.\nPlease try again later',
|
|||
StubBotHandler()
|
||||
|
||||
with self.mock_config_info(
|
||||
{'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'}
|
||||
), patch('requests.get', side_effect=ConnectionError()), self.mock_http_conversation(
|
||||
'test_invalid_when_handle_message'
|
||||
{"auth_token": "someInvalidKey", "username": "aaron", "goalname": "goal"}
|
||||
), patch("requests.get", side_effect=ConnectionError()), self.mock_http_conversation(
|
||||
"test_invalid_when_handle_message"
|
||||
), patch(
|
||||
'logging.exception'
|
||||
"logging.exception"
|
||||
):
|
||||
self.verify_reply('5', 'Error. Check your key!')
|
||||
self.verify_reply("5", "Error. Check your key!")
|
||||
|
||||
def test_error(self) -> None:
|
||||
bot_request = 'notNumber'
|
||||
bot_request = "notNumber"
|
||||
bot_response = "Error occured : 422"
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_valid_auth_token'
|
||||
), self.mock_http_conversation('test_error'):
|
||||
"test_valid_auth_token"
|
||||
), self.mock_http_conversation("test_error"):
|
||||
self.verify_reply(bot_request, bot_response)
|
||||
|
||||
def test_invalid_when_initialize(self) -> None:
|
||||
|
@ -96,8 +96,8 @@ right now.\nPlease try again later',
|
|||
bot_handler = StubBotHandler()
|
||||
|
||||
with self.mock_config_info(
|
||||
{'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'}
|
||||
), self.mock_http_conversation('test_invalid_when_initialize'), self.assertRaises(
|
||||
{"auth_token": "someInvalidKey", "username": "aaron", "goalname": "goal"}
|
||||
), self.mock_http_conversation("test_invalid_when_initialize"), self.assertRaises(
|
||||
bot_handler.BotQuitException
|
||||
):
|
||||
bot.initialize(bot_handler)
|
||||
|
@ -107,7 +107,7 @@ right now.\nPlease try again later',
|
|||
bot_handler = StubBotHandler()
|
||||
|
||||
with self.mock_config_info(self.normal_config), patch(
|
||||
'requests.get', side_effect=ConnectionError()
|
||||
), patch('logging.exception') as mock_logging:
|
||||
"requests.get", side_effect=ConnectionError()
|
||||
), patch("logging.exception") as mock_logging:
|
||||
bot.initialize(bot_handler)
|
||||
self.assertTrue(mock_logging.called)
|
||||
|
|
|
@ -7,38 +7,38 @@ import chess.uci
|
|||
|
||||
from zulip_bots.lib import BotHandler
|
||||
|
||||
START_REGEX = re.compile('start with other user$')
|
||||
START_COMPUTER_REGEX = re.compile('start as (?P<user_color>white|black) with computer')
|
||||
MOVE_REGEX = re.compile('do (?P<move_san>.+)$')
|
||||
RESIGN_REGEX = re.compile('resign$')
|
||||
START_REGEX = re.compile("start with other user$")
|
||||
START_COMPUTER_REGEX = re.compile("start as (?P<user_color>white|black) with computer")
|
||||
MOVE_REGEX = re.compile("do (?P<move_san>.+)$")
|
||||
RESIGN_REGEX = re.compile("resign$")
|
||||
|
||||
|
||||
class ChessHandler:
|
||||
def usage(self) -> str:
|
||||
return (
|
||||
'Chess Bot is a bot that allows you to play chess against either '
|
||||
'another user or the computer. Use `start with other user` or '
|
||||
'`start as <color> with computer` to start a game.\n\n'
|
||||
'In order to play against a computer, `chess.conf` must be set '
|
||||
'with the key `stockfish_location` set to the location of the '
|
||||
'Stockfish program on this computer.'
|
||||
"Chess Bot is a bot that allows you to play chess against either "
|
||||
"another user or the computer. Use `start with other user` or "
|
||||
"`start as <color> with computer` to start a game.\n\n"
|
||||
"In order to play against a computer, `chess.conf` must be set "
|
||||
"with the key `stockfish_location` set to the location of the "
|
||||
"Stockfish program on this computer."
|
||||
)
|
||||
|
||||
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:
|
||||
self.engine = chess.uci.popen_engine(self.config_info['stockfish_location'])
|
||||
self.engine = chess.uci.popen_engine(self.config_info["stockfish_location"])
|
||||
self.engine.uci()
|
||||
except FileNotFoundError:
|
||||
# It is helpful to allow for fake Stockfish locations if the bot
|
||||
# runner is testing or knows they won't be using an engine.
|
||||
print('That Stockfish doesn\'t exist. Continuing.')
|
||||
print("That Stockfish doesn't exist. Continuing.")
|
||||
|
||||
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())
|
||||
return
|
||||
|
||||
|
@ -50,29 +50,29 @@ class ChessHandler:
|
|||
is_with_computer = False
|
||||
last_fen = chess.Board().fen()
|
||||
|
||||
if bot_handler.storage.contains('is_with_computer'):
|
||||
if bot_handler.storage.contains("is_with_computer"):
|
||||
is_with_computer = (
|
||||
# `bot_handler`'s `storage` only accepts `str` values.
|
||||
bot_handler.storage.get('is_with_computer')
|
||||
bot_handler.storage.get("is_with_computer")
|
||||
== str(True)
|
||||
)
|
||||
|
||||
if bot_handler.storage.contains('last_fen'):
|
||||
last_fen = bot_handler.storage.get('last_fen')
|
||||
if bot_handler.storage.contains("last_fen"):
|
||||
last_fen = bot_handler.storage.get("last_fen")
|
||||
|
||||
if start_regex_match:
|
||||
self.start(message, bot_handler)
|
||||
elif start_computer_regex_match:
|
||||
self.start_computer(
|
||||
message, bot_handler, start_computer_regex_match.group('user_color') == 'white'
|
||||
message, bot_handler, start_computer_regex_match.group("user_color") == "white"
|
||||
)
|
||||
elif move_regex_match:
|
||||
if is_with_computer:
|
||||
self.move_computer(
|
||||
message, bot_handler, last_fen, move_regex_match.group('move_san')
|
||||
message, bot_handler, last_fen, move_regex_match.group("move_san")
|
||||
)
|
||||
else:
|
||||
self.move(message, bot_handler, last_fen, move_regex_match.group('move_san'))
|
||||
self.move(message, bot_handler, last_fen, move_regex_match.group("move_san"))
|
||||
elif resign_regex_match:
|
||||
self.resign(message, bot_handler, last_fen)
|
||||
|
||||
|
@ -88,9 +88,9 @@ class ChessHandler:
|
|||
bot_handler.send_reply(message, make_start_reponse(new_board))
|
||||
|
||||
# `bot_handler`'s `storage` only accepts `str` values.
|
||||
bot_handler.storage.put('is_with_computer', str(False))
|
||||
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(
|
||||
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`'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:
|
||||
self.move_computer_first(
|
||||
message,
|
||||
|
@ -204,18 +204,18 @@ class ChessHandler:
|
|||
# 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.
|
||||
if new_board.is_game_over(True):
|
||||
game_over_output = ''
|
||||
game_over_output = ""
|
||||
|
||||
if new_board.is_checkmate():
|
||||
game_over_output = make_loss_response(new_board, 'was checkmated')
|
||||
game_over_output = make_loss_response(new_board, "was checkmated")
|
||||
elif new_board.is_stalemate():
|
||||
game_over_output = make_draw_response('stalemate')
|
||||
game_over_output = make_draw_response("stalemate")
|
||||
elif new_board.is_insufficient_material():
|
||||
game_over_output = make_draw_response('insufficient material')
|
||||
game_over_output = make_draw_response("insufficient material")
|
||||
elif new_board.can_claim_fifty_moves():
|
||||
game_over_output = make_draw_response('50 moves without a capture or pawn move')
|
||||
game_over_output = make_draw_response("50 moves without a capture or pawn move")
|
||||
elif new_board.can_claim_threefold_repetition():
|
||||
game_over_output = make_draw_response('3-fold repetition')
|
||||
game_over_output = make_draw_response("3-fold repetition")
|
||||
|
||||
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.storage.put('last_fen', new_board.fen())
|
||||
bot_handler.storage.put("last_fen", new_board.fen())
|
||||
|
||||
def move_computer(
|
||||
self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str, move_san: str
|
||||
|
@ -299,7 +299,7 @@ class ChessHandler:
|
|||
message, make_move_reponse(new_board, new_board_after_computer_move, computer_move)
|
||||
)
|
||||
|
||||
bot_handler.storage.put('last_fen', new_board_after_computer_move.fen())
|
||||
bot_handler.storage.put("last_fen", new_board_after_computer_move.fen())
|
||||
|
||||
def move_computer_first(
|
||||
self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str
|
||||
|
@ -329,10 +329,10 @@ class ChessHandler:
|
|||
message, make_move_reponse(last_board, new_board_after_computer_move, computer_move)
|
||||
)
|
||||
|
||||
bot_handler.storage.put('last_fen', new_board_after_computer_move.fen())
|
||||
bot_handler.storage.put("last_fen", new_board_after_computer_move.fen())
|
||||
|
||||
# `bot_handler`'s `storage` only accepts `str` values.
|
||||
bot_handler.storage.put('is_with_computer', str(True))
|
||||
bot_handler.storage.put("is_with_computer", str(True))
|
||||
|
||||
def resign(self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str) -> None:
|
||||
"""Resigns the game for the current player.
|
||||
|
@ -347,7 +347,7 @@ class ChessHandler:
|
|||
if not last_board:
|
||||
return
|
||||
|
||||
bot_handler.send_reply(message, make_loss_response(last_board, 'resigned'))
|
||||
bot_handler.send_reply(message, make_loss_response(last_board, "resigned"))
|
||||
|
||||
|
||||
handler_class = ChessHandler
|
||||
|
@ -376,7 +376,7 @@ def make_draw_response(reason: str) -> str:
|
|||
|
||||
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:
|
||||
|
@ -389,10 +389,10 @@ def make_loss_response(board: chess.Board, reason: str) -> str:
|
|||
|
||||
Returns: The loss response string.
|
||||
"""
|
||||
return ('*{}* {}. **{}** wins!\n\n' '{}').format(
|
||||
'White' if board.turn else 'Black',
|
||||
return ("*{}* {}. **{}** wins!\n\n" "{}").format(
|
||||
"White" if board.turn else "Black",
|
||||
reason,
|
||||
'Black' if board.turn else 'White',
|
||||
"Black" if board.turn else "White",
|
||||
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.
|
||||
"""
|
||||
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()
|
||||
)
|
||||
|
||||
|
@ -417,8 +417,8 @@ def make_copied_wrong_response() -> str:
|
|||
Returns: The copied-wrong response string.
|
||||
"""
|
||||
return (
|
||||
'Sorry, it seems like you copied down the response wrong.\n\n'
|
||||
'Please try to copy the response again from the last message!'
|
||||
"Sorry, it seems like you copied down the response wrong.\n\n"
|
||||
"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.
|
||||
"""
|
||||
return (
|
||||
'New game! The board looks like this:\n\n'
|
||||
'{}'
|
||||
'\n\n\n'
|
||||
'Now it\'s **{}**\'s turn.'
|
||||
'\n\n\n'
|
||||
'{}'
|
||||
).format(make_str(board, True), 'white' if board.turn else 'black', make_footer())
|
||||
"New game! The board looks like this:\n\n"
|
||||
"{}"
|
||||
"\n\n\n"
|
||||
"Now it's **{}**'s turn."
|
||||
"\n\n\n"
|
||||
"{}"
|
||||
).format(make_str(board, True), "white" if board.turn else "black", make_footer())
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
return (
|
||||
'New game with computer! The board looks like this:\n\n'
|
||||
'{}'
|
||||
'\n\n\n'
|
||||
'Now it\'s **{}**\'s turn.'
|
||||
'\n\n\n'
|
||||
'{}'
|
||||
).format(make_str(board, True), 'white' if board.turn else 'black', make_footer())
|
||||
"New game with computer! The board looks like this:\n\n"
|
||||
"{}"
|
||||
"\n\n\n"
|
||||
"Now it's **{}**'s turn."
|
||||
"\n\n\n"
|
||||
"{}"
|
||||
).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:
|
||||
|
@ -474,21 +474,21 @@ def make_move_reponse(last_board: chess.Board, new_board: chess.Board, move: che
|
|||
Returns: The move response string.
|
||||
"""
|
||||
return (
|
||||
'The board was like this:\n\n'
|
||||
'{}'
|
||||
'\n\n\n'
|
||||
'Then *{}* moved *{}*:\n\n'
|
||||
'{}'
|
||||
'\n\n\n'
|
||||
'Now it\'s **{}**\'s turn.'
|
||||
'\n\n\n'
|
||||
'{}'
|
||||
"The board was like this:\n\n"
|
||||
"{}"
|
||||
"\n\n\n"
|
||||
"Then *{}* moved *{}*:\n\n"
|
||||
"{}"
|
||||
"\n\n\n"
|
||||
"Now it's **{}**'s turn."
|
||||
"\n\n\n"
|
||||
"{}"
|
||||
).format(
|
||||
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),
|
||||
make_str(new_board, new_board.turn),
|
||||
'white' if new_board.turn else 'black',
|
||||
"white" if new_board.turn else "black",
|
||||
make_footer(),
|
||||
)
|
||||
|
||||
|
@ -498,10 +498,10 @@ def make_footer() -> str:
|
|||
responses.
|
||||
"""
|
||||
return (
|
||||
'To make your next move, respond to Chess Bot with\n\n'
|
||||
'```do <your move>```\n\n'
|
||||
'*Remember to @-mention Chess Bot at the beginning of your '
|
||||
'response.*'
|
||||
"To make your next move, respond to Chess Bot with\n\n"
|
||||
"```do <your move>```\n\n"
|
||||
"*Remember to @-mention Chess Bot at the beginning of your "
|
||||
"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]
|
||||
)
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
# between pieces. Newlines are added later by the loop and spaces are added
|
||||
# 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
|
||||
# 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):
|
||||
# `(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,
|
||||
# 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.
|
||||
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
|
||||
# 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.
|
||||
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
|
||||
|
||||
|
@ -586,21 +586,21 @@ def replace_with_unicode(board_str: str) -> str:
|
|||
"""
|
||||
replaced_str = board_str
|
||||
|
||||
replaced_str = replaced_str.replace('P', '♙')
|
||||
replaced_str = replaced_str.replace('N', '♘')
|
||||
replaced_str = replaced_str.replace('B', '♗')
|
||||
replaced_str = replaced_str.replace('R', '♖')
|
||||
replaced_str = replaced_str.replace('Q', '♕')
|
||||
replaced_str = replaced_str.replace('K', '♔')
|
||||
replaced_str = replaced_str.replace("P", "♙")
|
||||
replaced_str = replaced_str.replace("N", "♘")
|
||||
replaced_str = replaced_str.replace("B", "♗")
|
||||
replaced_str = replaced_str.replace("R", "♖")
|
||||
replaced_str = replaced_str.replace("Q", "♕")
|
||||
replaced_str = replaced_str.replace("K", "♔")
|
||||
|
||||
replaced_str = replaced_str.replace('p', '♟')
|
||||
replaced_str = replaced_str.replace('n', '♞')
|
||||
replaced_str = replaced_str.replace('b', '♝')
|
||||
replaced_str = replaced_str.replace('r', '♜')
|
||||
replaced_str = replaced_str.replace('q', '♛')
|
||||
replaced_str = replaced_str.replace('k', '♚')
|
||||
replaced_str = replaced_str.replace("p", "♟")
|
||||
replaced_str = replaced_str.replace("n", "♞")
|
||||
replaced_str = replaced_str.replace("b", "♝")
|
||||
replaced_str = replaced_str.replace("r", "♜")
|
||||
replaced_str = replaced_str.replace("q", "♛")
|
||||
replaced_str = replaced_str.replace("k", "♚")
|
||||
|
||||
replaced_str = replaced_str.replace('.', '·')
|
||||
replaced_str = replaced_str.replace(".", "·")
|
||||
|
||||
return replaced_str
|
||||
|
||||
|
@ -613,4 +613,4 @@ def trim_whitespace_before_newline(str_to_trim: str) -> str:
|
|||
|
||||
Returns: The trimmed string.
|
||||
"""
|
||||
return re.sub(r'\s+$', '', str_to_trim, flags=re.M)
|
||||
return re.sub(r"\s+$", "", str_to_trim, flags=re.M)
|
||||
|
|
|
@ -4,7 +4,7 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
|||
class TestChessBot(BotTestCase, DefaultTests):
|
||||
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
|
||||
|
@ -27,9 +27,9 @@ To make your next move, respond to Chess Bot with
|
|||
|
||||
```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
|
||||
|
@ -68,9 +68,9 @@ To make your next move, respond to Chess Bot with
|
|||
|
||||
```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
|
||||
|
@ -90,9 +90,9 @@ To make your next move, respond to Chess Bot with
|
|||
|
||||
```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
|
||||
|
@ -105,20 +105,20 @@ To make your next move, respond to Chess Bot with
|
|||
7 ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ 7
|
||||
8 ♜ ♞ ♝ ♚ ♛ ♝ ♞ ♜ 8
|
||||
h g f e d c b a
|
||||
```'''
|
||||
```"""
|
||||
|
||||
def test_bot_responds_to_empty_message(self) -> None:
|
||||
with self.mock_config_info({'stockfish_location': '/foo/bar'}):
|
||||
response = self.get_response(dict(content=''))
|
||||
self.assertIn('play chess', response['content'])
|
||||
with self.mock_config_info({"stockfish_location": "/foo/bar"}):
|
||||
response = self.get_response(dict(content=""))
|
||||
self.assertIn("play chess", response["content"])
|
||||
|
||||
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(
|
||||
[
|
||||
('start with other user', self.START_RESPONSE),
|
||||
('do e4', self.DO_E4_RESPONSE),
|
||||
('do Ke4', self.DO_KE4_RESPONSE),
|
||||
('resign', self.RESIGN_RESPONSE),
|
||||
("start with other user", self.START_RESPONSE),
|
||||
("do e4", self.DO_E4_RESPONSE),
|
||||
("do Ke4", self.DO_KE4_RESPONSE),
|
||||
("resign", self.RESIGN_RESPONSE),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -5,21 +5,21 @@ from zulip_bots.game_handler import GameAdapter
|
|||
|
||||
|
||||
class ConnectFourMessageHandler:
|
||||
tokens = [':blue_circle:', ':red_circle:']
|
||||
tokens = [":blue_circle:", ":red_circle:"]
|
||||
|
||||
def parse_board(self, board: Any) -> str:
|
||||
# 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):
|
||||
board_str += '\n\n'
|
||||
board_str += "\n\n"
|
||||
for column in range(0, 7):
|
||||
if board[row][column] == 0:
|
||||
board_str += ':white_circle: '
|
||||
board_str += ":white_circle: "
|
||||
elif board[row][column] == 1:
|
||||
board_str += self.tokens[0] + ' '
|
||||
board_str += self.tokens[0] + " "
|
||||
elif board[row][column] == -1:
|
||||
board_str += self.tokens[1] + ' '
|
||||
board_str += self.tokens[1] + " "
|
||||
|
||||
return board_str
|
||||
|
||||
|
@ -27,33 +27,33 @@ class ConnectFourMessageHandler:
|
|||
return self.tokens[turn]
|
||||
|
||||
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
||||
column_number = move_info.replace('move ', '')
|
||||
return original_player + ' moved in column ' + column_number
|
||||
column_number = move_info.replace("move ", "")
|
||||
return original_player + " moved in column " + column_number
|
||||
|
||||
def game_start_message(self) -> str:
|
||||
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!'
|
||||
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!"
|
||||
|
||||
|
||||
class ConnectFourBotHandler(GameAdapter):
|
||||
'''
|
||||
"""
|
||||
Bot that uses the Game Adapter class
|
||||
to allow users to play other users
|
||||
or the comptuer in a game of Connect
|
||||
Four
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
game_name = 'Connect Four'
|
||||
bot_name = 'connect_four'
|
||||
game_name = "Connect Four"
|
||||
bot_name = "connect_four"
|
||||
move_help_message = (
|
||||
'* To make your move during a game, type\n'
|
||||
'```move <column-number>``` or ```<column-number>```'
|
||||
"* To make your move during a game, type\n"
|
||||
"```move <column-number>``` or ```<column-number>```"
|
||||
)
|
||||
move_regex = '(move ([1-7])$)|(([1-7])$)'
|
||||
move_regex = "(move ([1-7])$)|(([1-7])$)"
|
||||
model = ConnectFourModel
|
||||
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__(
|
||||
game_name,
|
||||
|
|
|
@ -5,10 +5,10 @@ from zulip_bots.game_handler import BadMoveException
|
|||
|
||||
|
||||
class ConnectFourModel:
|
||||
'''
|
||||
"""
|
||||
Object that manages running the Connect
|
||||
Four logic for the Connect Four Bot
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.blank_board = [
|
||||
|
@ -54,11 +54,11 @@ class ConnectFourModel:
|
|||
token_number = 1
|
||||
finding_move = True
|
||||
row = 5
|
||||
column = int(move.replace('move ', '')) - 1
|
||||
column = int(move.replace("move ", "")) - 1
|
||||
|
||||
while finding_move:
|
||||
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:
|
||||
self.current_board[row][column] = token_number
|
||||
finding_move = False
|
||||
|
@ -143,7 +143,7 @@ class ConnectFourModel:
|
|||
top_row_multiple = reduce(lambda x, y: x * y, self.current_board[0])
|
||||
|
||||
if top_row_multiple != 0:
|
||||
return 'draw'
|
||||
return "draw"
|
||||
|
||||
winner = (
|
||||
get_horizontal_wins(self.current_board)
|
||||
|
@ -156,4 +156,4 @@ class ConnectFourModel:
|
|||
elif winner == -1:
|
||||
return second_player
|
||||
|
||||
return ''
|
||||
return ""
|
||||
|
|
|
@ -6,10 +6,10 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
|||
|
||||
|
||||
class TestConnectFourBot(BotTestCase, DefaultTests):
|
||||
bot_name = 'connect_four'
|
||||
bot_name = "connect_four"
|
||||
|
||||
def make_request_message(
|
||||
self, content: str, user: str = 'foo@example.com', user_name: str = 'foo'
|
||||
self, content: str, user: str = "foo@example.com", user_name: str = "foo"
|
||||
) -> Dict[str, str]:
|
||||
message = dict(sender_email=user, content=content, sender_full_name=user_name)
|
||||
return message
|
||||
|
@ -20,14 +20,14 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
|||
request: str,
|
||||
expected_response: str,
|
||||
response_number: int,
|
||||
user: str = 'foo@example.com',
|
||||
user: str = "foo@example.com",
|
||||
) -> None:
|
||||
'''
|
||||
"""
|
||||
This function serves a similar purpose
|
||||
to BotTestCase.verify_dialog, but allows
|
||||
for multiple responses to be validated,
|
||||
and for mocking of the bot's internal data
|
||||
'''
|
||||
"""
|
||||
|
||||
bot, bot_handler = self._get_handlers()
|
||||
message = self.make_request_message(request, user)
|
||||
|
@ -38,10 +38,10 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
|||
responses = [message for (method, message) in bot_handler.transcript]
|
||||
|
||||
first_response = responses[response_number]
|
||||
self.assertEqual(expected_response, first_response['content'])
|
||||
self.assertEqual(expected_response, first_response["content"])
|
||||
|
||||
def help_message(self) -> str:
|
||||
return '''** Connect Four Bot Help:**
|
||||
return """** Connect Four Bot Help:**
|
||||
*Preface all commands with @**test-bot***
|
||||
* To start a game in a stream (*recommended*), type
|
||||
`start game`
|
||||
|
@ -62,15 +62,15 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
|||
* To see rules of this game, type
|
||||
`rules`
|
||||
* 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:
|
||||
self.verify_response('help', self.help_message(), 0)
|
||||
self.verify_response("help", self.help_message(), 0)
|
||||
|
||||
def test_game_message_handler_responses(self) -> None:
|
||||
board = (
|
||||
':one: :two: :three: :four: :five: :six: :seven:\n\n'
|
||||
+ '\
|
||||
":one: :two: :three: :four: :five: :six: :seven:\n\n"
|
||||
+ "\
|
||||
:white_circle: :white_circle: :white_circle: :white_circle: \
|
||||
:white_circle: :white_circle: :white_circle: \n\n\
|
||||
:white_circle: :white_circle: :white_circle: :white_circle: \
|
||||
|
@ -82,18 +82,18 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
|
|||
:blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \
|
||||
:white_circle: :white_circle: \n\n\
|
||||
:blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \
|
||||
:white_circle: :white_circle: '
|
||||
:white_circle: :white_circle: "
|
||||
)
|
||||
bot, bot_handler = self._get_handlers()
|
||||
self.assertEqual(bot.gameMessageHandler.parse_board(self.almost_win_board), board)
|
||||
self.assertEqual(bot.gameMessageHandler.get_player_color(1), ':red_circle:')
|
||||
self.assertEqual(bot.gameMessageHandler.get_player_color(1), ":red_circle:")
|
||||
self.assertEqual(
|
||||
bot.gameMessageHandler.alert_move_message('foo', 'move 6'), 'foo moved in column 6'
|
||||
bot.gameMessageHandler.alert_move_message("foo", "move 6"), "foo moved in column 6"
|
||||
)
|
||||
self.assertEqual(
|
||||
bot.gameMessageHandler.game_start_message(),
|
||||
'Type `move <column-number>` or `<column-number>` to place a token.\n\
|
||||
The first player to get 4 in a row wins!\n Good Luck!',
|
||||
"Type `move <column-number>` or `<column-number>` to place a token.\n\
|
||||
The first player to get 4 in a row wins!\n Good Luck!",
|
||||
)
|
||||
|
||||
blank_board = [
|
||||
|
@ -142,22 +142,22 @@ The first player to get 4 in a row wins!\n Good Luck!',
|
|||
final_board: List[List[int]],
|
||||
) -> None:
|
||||
connectFourModel.update_board(initial_board)
|
||||
test_board = connectFourModel.make_move('move ' + str(column_number), token_number)
|
||||
test_board = connectFourModel.make_move("move " + str(column_number), token_number)
|
||||
|
||||
self.assertEqual(test_board, final_board)
|
||||
|
||||
def confirmGameOver(board: List[List[int]], result: str) -> None:
|
||||
connectFourModel.update_board(board)
|
||||
game_over = connectFourModel.determine_game_over(['first_player', 'second_player'])
|
||||
game_over = connectFourModel.determine_game_over(["first_player", "second_player"])
|
||||
|
||||
self.assertEqual(game_over, result)
|
||||
|
||||
def confirmWinStates(array: List[List[List[List[int]]]]) -> None:
|
||||
for board in array[0]:
|
||||
confirmGameOver(board, 'first_player')
|
||||
confirmGameOver(board, "first_player")
|
||||
|
||||
for board in array[1]:
|
||||
confirmGameOver(board, 'second_player')
|
||||
confirmGameOver(board, "second_player")
|
||||
|
||||
connectFourModel = ConnectFourModel()
|
||||
|
||||
|
@ -553,8 +553,8 @@ The first player to get 4 in a row wins!\n Good Luck!',
|
|||
)
|
||||
|
||||
# Test Game Over Logic:
|
||||
confirmGameOver(blank_board, '')
|
||||
confirmGameOver(full_board, 'draw')
|
||||
confirmGameOver(blank_board, "")
|
||||
confirmGameOver(full_board, "draw")
|
||||
|
||||
# Test Win States:
|
||||
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:
|
||||
model = ConnectFourModel()
|
||||
move = 'move 4'
|
||||
move = "move 4"
|
||||
col = 3 # zero-indexed
|
||||
|
||||
self.assertEqual(model.get_column(col), [0, 0, 0, 0, 0, 0])
|
||||
|
|
|
@ -28,17 +28,17 @@ def round_to(x: float, digits: int) -> float:
|
|||
|
||||
|
||||
class ConverterHandler:
|
||||
'''
|
||||
"""
|
||||
This plugin allows users to make conversions between various units,
|
||||
e.g. Celsius to Fahrenheit, or kilobytes to gigabytes.
|
||||
It looks for messages of the format
|
||||
'@mention-bot <number> <unit_from> <unit_to>'
|
||||
The message '@mention-bot help' posts a short description of how to use
|
||||
the plugin, along with a list of all supported units.
|
||||
'''
|
||||
"""
|
||||
|
||||
def usage(self) -> str:
|
||||
return '''
|
||||
return """
|
||||
This plugin allows users to make conversions between
|
||||
various units, e.g. Celsius to Fahrenheit,
|
||||
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
|
||||
how to use the plugin, along with a list of
|
||||
all supported units.
|
||||
'''
|
||||
"""
|
||||
|
||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||
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:
|
||||
content = message['content']
|
||||
content = message["content"]
|
||||
|
||||
words = content.lower().split()
|
||||
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 = []
|
||||
|
||||
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)
|
||||
continue
|
||||
if (convert_index + 3) < len(words):
|
||||
|
@ -72,7 +72,7 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
|
|||
exponent = 0
|
||||
|
||||
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
|
||||
|
||||
# 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]
|
||||
|
||||
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:
|
||||
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:
|
||||
continue
|
||||
|
||||
base_unit = uf_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(
|
||||
'`'
|
||||
"`"
|
||||
+ unit_to.capitalize()
|
||||
+ '` and `'
|
||||
+ "` and `"
|
||||
+ unit_from
|
||||
+ '`'
|
||||
+ ' are not from the same category. '
|
||||
+ "`"
|
||||
+ " are not from the same category. "
|
||||
+ utils.QUICK_HELP
|
||||
)
|
||||
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[1]
|
||||
|
||||
if base_unit == 'bit':
|
||||
if base_unit == "bit":
|
||||
number_res *= 1024 ** (exponent // 3)
|
||||
else:
|
||||
number_res *= 10 ** exponent
|
||||
number_res = round_to(number_res, 7)
|
||||
|
||||
results.append(
|
||||
'{} {} = {} {}'.format(
|
||||
"{} {} = {} {}".format(
|
||||
number, words[convert_index + 2], number_res, words[convert_index + 3]
|
||||
)
|
||||
)
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@ class TestConverterBot(BotTestCase, DefaultTests):
|
|||
dialog = [
|
||||
(
|
||||
"",
|
||||
'Too few arguments given. Enter `@convert help` '
|
||||
'for help on using the converter.\n',
|
||||
"Too few arguments given. Enter `@convert help` "
|
||||
"for help on using the converter.\n",
|
||||
),
|
||||
(
|
||||
"foo bar",
|
||||
'Too few arguments given. Enter `@convert help` '
|
||||
'for help on using the converter.\n',
|
||||
"Too few arguments given. Enter `@convert help` "
|
||||
"for help on using the converter.\n",
|
||||
),
|
||||
("2 m cm", "2 m = 200.0 cm\n"),
|
||||
("12.0 celsius fahrenheit", "12.0 celsius = 53.600054 fahrenheit\n"),
|
||||
|
|
|
@ -3,152 +3,152 @@
|
|||
# factor that need to be added and multiplied to convert the unit into
|
||||
# the base unit in the last parameter.
|
||||
UNITS = {
|
||||
'bit': [0, 1, 'bit'],
|
||||
'byte': [0, 8, 'bit'],
|
||||
'cubic-centimeter': [0, 0.000001, 'cubic-meter'],
|
||||
'cubic-decimeter': [0, 0.001, 'cubic-meter'],
|
||||
'liter': [0, 0.001, 'cubic-meter'],
|
||||
'cubic-meter': [0, 1, 'cubic-meter'],
|
||||
'cubic-inch': [0, 0.000016387064, 'cubic-meter'],
|
||||
'fluid-ounce': [0, 0.000029574, 'cubic-meter'],
|
||||
'cubic-foot': [0, 0.028316846592, 'cubic-meter'],
|
||||
'cubic-yard': [0, 0.764554857984, 'cubic-meter'],
|
||||
'teaspoon': [0, 0.0000049289216, 'cubic-meter'],
|
||||
'tablespoon': [0, 0.000014787, 'cubic-meter'],
|
||||
'cup': [0, 0.00023658823648491, 'cubic-meter'],
|
||||
'gram': [0, 1, 'gram'],
|
||||
'kilogram': [0, 1000, 'gram'],
|
||||
'ton': [0, 1000000, 'gram'],
|
||||
'ounce': [0, 28.349523125, 'gram'],
|
||||
'pound': [0, 453.59237, 'gram'],
|
||||
'kelvin': [0, 1, 'kelvin'],
|
||||
'celsius': [273.15, 1, 'kelvin'],
|
||||
'fahrenheit': [255.372222, 0.555555, 'kelvin'],
|
||||
'centimeter': [0, 0.01, 'meter'],
|
||||
'decimeter': [0, 0.1, 'meter'],
|
||||
'meter': [0, 1, 'meter'],
|
||||
'kilometer': [0, 1000, 'meter'],
|
||||
'inch': [0, 0.0254, 'meter'],
|
||||
'foot': [0, 0.3048, 'meter'],
|
||||
'yard': [0, 0.9144, 'meter'],
|
||||
'mile': [0, 1609.344, 'meter'],
|
||||
'nautical-mile': [0, 1852, 'meter'],
|
||||
'square-centimeter': [0, 0.0001, 'square-meter'],
|
||||
'square-decimeter': [0, 0.01, 'square-meter'],
|
||||
'square-meter': [0, 1, 'square-meter'],
|
||||
'square-kilometer': [0, 1000000, 'square-meter'],
|
||||
'square-inch': [0, 0.00064516, 'square-meter'],
|
||||
'square-foot': [0, 0.09290304, 'square-meter'],
|
||||
'square-yard': [0, 0.83612736, 'square-meter'],
|
||||
'square-mile': [0, 2589988.110336, 'square-meter'],
|
||||
'are': [0, 100, 'square-meter'],
|
||||
'hectare': [0, 10000, 'square-meter'],
|
||||
'acre': [0, 4046.8564224, 'square-meter'],
|
||||
"bit": [0, 1, "bit"],
|
||||
"byte": [0, 8, "bit"],
|
||||
"cubic-centimeter": [0, 0.000001, "cubic-meter"],
|
||||
"cubic-decimeter": [0, 0.001, "cubic-meter"],
|
||||
"liter": [0, 0.001, "cubic-meter"],
|
||||
"cubic-meter": [0, 1, "cubic-meter"],
|
||||
"cubic-inch": [0, 0.000016387064, "cubic-meter"],
|
||||
"fluid-ounce": [0, 0.000029574, "cubic-meter"],
|
||||
"cubic-foot": [0, 0.028316846592, "cubic-meter"],
|
||||
"cubic-yard": [0, 0.764554857984, "cubic-meter"],
|
||||
"teaspoon": [0, 0.0000049289216, "cubic-meter"],
|
||||
"tablespoon": [0, 0.000014787, "cubic-meter"],
|
||||
"cup": [0, 0.00023658823648491, "cubic-meter"],
|
||||
"gram": [0, 1, "gram"],
|
||||
"kilogram": [0, 1000, "gram"],
|
||||
"ton": [0, 1000000, "gram"],
|
||||
"ounce": [0, 28.349523125, "gram"],
|
||||
"pound": [0, 453.59237, "gram"],
|
||||
"kelvin": [0, 1, "kelvin"],
|
||||
"celsius": [273.15, 1, "kelvin"],
|
||||
"fahrenheit": [255.372222, 0.555555, "kelvin"],
|
||||
"centimeter": [0, 0.01, "meter"],
|
||||
"decimeter": [0, 0.1, "meter"],
|
||||
"meter": [0, 1, "meter"],
|
||||
"kilometer": [0, 1000, "meter"],
|
||||
"inch": [0, 0.0254, "meter"],
|
||||
"foot": [0, 0.3048, "meter"],
|
||||
"yard": [0, 0.9144, "meter"],
|
||||
"mile": [0, 1609.344, "meter"],
|
||||
"nautical-mile": [0, 1852, "meter"],
|
||||
"square-centimeter": [0, 0.0001, "square-meter"],
|
||||
"square-decimeter": [0, 0.01, "square-meter"],
|
||||
"square-meter": [0, 1, "square-meter"],
|
||||
"square-kilometer": [0, 1000000, "square-meter"],
|
||||
"square-inch": [0, 0.00064516, "square-meter"],
|
||||
"square-foot": [0, 0.09290304, "square-meter"],
|
||||
"square-yard": [0, 0.83612736, "square-meter"],
|
||||
"square-mile": [0, 2589988.110336, "square-meter"],
|
||||
"are": [0, 100, "square-meter"],
|
||||
"hectare": [0, 10000, "square-meter"],
|
||||
"acre": [0, 4046.8564224, "square-meter"],
|
||||
}
|
||||
|
||||
PREFIXES = {
|
||||
'atto': -18,
|
||||
'femto': -15,
|
||||
'pico': -12,
|
||||
'nano': -9,
|
||||
'micro': -6,
|
||||
'milli': -3,
|
||||
'centi': -2,
|
||||
'deci': -1,
|
||||
'deca': 1,
|
||||
'hecto': 2,
|
||||
'kilo': 3,
|
||||
'mega': 6,
|
||||
'giga': 9,
|
||||
'tera': 12,
|
||||
'peta': 15,
|
||||
'exa': 18,
|
||||
"atto": -18,
|
||||
"femto": -15,
|
||||
"pico": -12,
|
||||
"nano": -9,
|
||||
"micro": -6,
|
||||
"milli": -3,
|
||||
"centi": -2,
|
||||
"deci": -1,
|
||||
"deca": 1,
|
||||
"hecto": 2,
|
||||
"kilo": 3,
|
||||
"mega": 6,
|
||||
"giga": 9,
|
||||
"tera": 12,
|
||||
"peta": 15,
|
||||
"exa": 18,
|
||||
}
|
||||
|
||||
ALIASES = {
|
||||
'a': 'are',
|
||||
'ac': 'acre',
|
||||
'c': 'celsius',
|
||||
'cm': 'centimeter',
|
||||
'cm2': 'square-centimeter',
|
||||
'cm3': 'cubic-centimeter',
|
||||
'cm^2': 'square-centimeter',
|
||||
'cm^3': 'cubic-centimeter',
|
||||
'dm': 'decimeter',
|
||||
'dm2': 'square-decimeter',
|
||||
'dm3': 'cubic-decimeter',
|
||||
'dm^2': 'square-decimeter',
|
||||
'dm^3': 'cubic-decimeter',
|
||||
'f': 'fahrenheit',
|
||||
'fl-oz': 'fluid-ounce',
|
||||
'ft': 'foot',
|
||||
'ft2': 'square-foot',
|
||||
'ft3': 'cubic-foot',
|
||||
'ft^2': 'square-foot',
|
||||
'ft^3': 'cubic-foot',
|
||||
'g': 'gram',
|
||||
'ha': 'hectare',
|
||||
'in': 'inch',
|
||||
'in2': 'square-inch',
|
||||
'in3': 'cubic-inch',
|
||||
'in^2': 'square-inch',
|
||||
'in^3': 'cubic-inch',
|
||||
'k': 'kelvin',
|
||||
'kg': 'kilogram',
|
||||
'km': 'kilometer',
|
||||
'km2': 'square-kilometer',
|
||||
'km^2': 'square-kilometer',
|
||||
'l': 'liter',
|
||||
'lb': 'pound',
|
||||
'm': 'meter',
|
||||
'm2': 'square-meter',
|
||||
'm3': 'cubic-meter',
|
||||
'm^2': 'square-meter',
|
||||
'm^3': 'cubic-meter',
|
||||
'mi': 'mile',
|
||||
'mi2': 'square-mile',
|
||||
'mi^2': 'square-mile',
|
||||
'nmi': 'nautical-mile',
|
||||
'oz': 'ounce',
|
||||
't': 'ton',
|
||||
'tbsp': 'tablespoon',
|
||||
'tsp': 'teaspoon',
|
||||
'y': 'yard',
|
||||
'y2': 'square-yard',
|
||||
'y3': 'cubic-yard',
|
||||
'y^2': 'square-yard',
|
||||
'y^3': 'cubic-yard',
|
||||
"a": "are",
|
||||
"ac": "acre",
|
||||
"c": "celsius",
|
||||
"cm": "centimeter",
|
||||
"cm2": "square-centimeter",
|
||||
"cm3": "cubic-centimeter",
|
||||
"cm^2": "square-centimeter",
|
||||
"cm^3": "cubic-centimeter",
|
||||
"dm": "decimeter",
|
||||
"dm2": "square-decimeter",
|
||||
"dm3": "cubic-decimeter",
|
||||
"dm^2": "square-decimeter",
|
||||
"dm^3": "cubic-decimeter",
|
||||
"f": "fahrenheit",
|
||||
"fl-oz": "fluid-ounce",
|
||||
"ft": "foot",
|
||||
"ft2": "square-foot",
|
||||
"ft3": "cubic-foot",
|
||||
"ft^2": "square-foot",
|
||||
"ft^3": "cubic-foot",
|
||||
"g": "gram",
|
||||
"ha": "hectare",
|
||||
"in": "inch",
|
||||
"in2": "square-inch",
|
||||
"in3": "cubic-inch",
|
||||
"in^2": "square-inch",
|
||||
"in^3": "cubic-inch",
|
||||
"k": "kelvin",
|
||||
"kg": "kilogram",
|
||||
"km": "kilometer",
|
||||
"km2": "square-kilometer",
|
||||
"km^2": "square-kilometer",
|
||||
"l": "liter",
|
||||
"lb": "pound",
|
||||
"m": "meter",
|
||||
"m2": "square-meter",
|
||||
"m3": "cubic-meter",
|
||||
"m^2": "square-meter",
|
||||
"m^3": "cubic-meter",
|
||||
"mi": "mile",
|
||||
"mi2": "square-mile",
|
||||
"mi^2": "square-mile",
|
||||
"nmi": "nautical-mile",
|
||||
"oz": "ounce",
|
||||
"t": "ton",
|
||||
"tbsp": "tablespoon",
|
||||
"tsp": "teaspoon",
|
||||
"y": "yard",
|
||||
"y2": "square-yard",
|
||||
"y3": "cubic-yard",
|
||||
"y^2": "square-yard",
|
||||
"y^3": "cubic-yard",
|
||||
}
|
||||
|
||||
HELP_MESSAGE = (
|
||||
'Converter usage:\n'
|
||||
'`@convert <number> <unit_from> <unit_to>`\n'
|
||||
'Converts `number` in the unit <unit_from> to '
|
||||
'the <unit_to> and prints the result\n'
|
||||
'`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n'
|
||||
'<unit_from> and <unit_to> are two of the following units:\n'
|
||||
'* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), '
|
||||
'square-meter (m^2, m2), square-kilometer (km^2, km2),'
|
||||
' square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), '
|
||||
' square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n'
|
||||
'* bit, byte\n'
|
||||
'* centimeter (cm), decimeter(dm), meter (m),'
|
||||
' kilometer (km), inch (in), foot (ft), yard (y),'
|
||||
' mile (mi), nautical-mile (nmi)\n'
|
||||
'* Kelvin (K), Celsius(C), Fahrenheit (F)\n'
|
||||
'* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), '
|
||||
'cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), '
|
||||
'cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n'
|
||||
'* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n'
|
||||
'* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n'
|
||||
'Allowed prefixes are:\n'
|
||||
'* atto, pico, femto, nano, micro, milli, centi, deci\n'
|
||||
'* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n'
|
||||
'Usage examples:\n'
|
||||
'* `@convert 12 celsius fahrenheit`\n'
|
||||
'* `@convert 0.002 kilomile millimeter`\n'
|
||||
'* `@convert 31.5 square-mile ha`\n'
|
||||
'* `@convert 56 g lb`\n'
|
||||
"Converter usage:\n"
|
||||
"`@convert <number> <unit_from> <unit_to>`\n"
|
||||
"Converts `number` in the unit <unit_from> to "
|
||||
"the <unit_to> and prints the result\n"
|
||||
"`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n"
|
||||
"<unit_from> and <unit_to> are two of the following units:\n"
|
||||
"* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), "
|
||||
"square-meter (m^2, m2), square-kilometer (km^2, km2),"
|
||||
" square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), "
|
||||
" square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n"
|
||||
"* bit, byte\n"
|
||||
"* centimeter (cm), decimeter(dm), meter (m),"
|
||||
" kilometer (km), inch (in), foot (ft), yard (y),"
|
||||
" mile (mi), nautical-mile (nmi)\n"
|
||||
"* Kelvin (K), Celsius(C), Fahrenheit (F)\n"
|
||||
"* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), "
|
||||
"cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), "
|
||||
"cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n"
|
||||
"* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n"
|
||||
"* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n"
|
||||
"Allowed prefixes are:\n"
|
||||
"* atto, pico, femto, nano, micro, milli, centi, deci\n"
|
||||
"* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n"
|
||||
"Usage examples:\n"
|
||||
"* `@convert 12 celsius fahrenheit`\n"
|
||||
"* `@convert 0.002 kilomile millimeter`\n"
|
||||
"* `@convert 31.5 square-mile ha`\n"
|
||||
"* `@convert 56 g lb`\n"
|
||||
)
|
||||
|
||||
QUICK_HELP = 'Enter `@convert help` for help on using the converter.'
|
||||
QUICK_HELP = "Enter `@convert help` for help on using the converter."
|
||||
|
|
|
@ -10,31 +10,31 @@ from zulip_bots.lib import BotHandler
|
|||
|
||||
|
||||
class DefineHandler:
|
||||
'''
|
||||
"""
|
||||
This plugin define a word that the user inputs. It
|
||||
looks for messages starting with '@mention-bot'.
|
||||
'''
|
||||
"""
|
||||
|
||||
DEFINITION_API_URL = 'https://owlbot.info/api/v2/dictionary/{}?format=json'
|
||||
REQUEST_ERROR_MESSAGE = 'Could not load definition.'
|
||||
EMPTY_WORD_REQUEST_ERROR_MESSAGE = 'Please enter a word to define.'
|
||||
PHRASE_ERROR_MESSAGE = 'Definitions for phrases are not available.'
|
||||
SYMBOLS_PRESENT_ERROR_MESSAGE = 'Definitions of words with symbols are not possible.'
|
||||
DEFINITION_API_URL = "https://owlbot.info/api/v2/dictionary/{}?format=json"
|
||||
REQUEST_ERROR_MESSAGE = "Could not load definition."
|
||||
EMPTY_WORD_REQUEST_ERROR_MESSAGE = "Please enter a word to define."
|
||||
PHRASE_ERROR_MESSAGE = "Definitions for phrases are not available."
|
||||
SYMBOLS_PRESENT_ERROR_MESSAGE = "Definitions of words with symbols are not possible."
|
||||
|
||||
def usage(self) -> str:
|
||||
return '''
|
||||
return """
|
||||
This plugin will allow users to define a word. Users should preface
|
||||
messages with @mention-bot.
|
||||
'''
|
||||
"""
|
||||
|
||||
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_handler.send_reply(message, bot_response)
|
||||
|
||||
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 len(split_content) > 1:
|
||||
return DefineHandler.PHRASE_ERROR_MESSAGE
|
||||
|
@ -51,7 +51,7 @@ class DefineHandler:
|
|||
if not to_define_lower:
|
||||
return self.EMPTY_WORD_REQUEST_ERROR_MESSAGE
|
||||
else:
|
||||
response = '**{}**:\n'.format(to_define)
|
||||
response = "**{}**:\n".format(to_define)
|
||||
|
||||
try:
|
||||
# Use OwlBot API to fetch definition.
|
||||
|
@ -65,9 +65,9 @@ class DefineHandler:
|
|||
else: # Definitions available.
|
||||
# Show definitions line by line.
|
||||
for d in definitions:
|
||||
example = d['example'] if d['example'] else '*No example available.*'
|
||||
response += '\n' + '* (**{}**) {}\n {}'.format(
|
||||
d['type'], d['definition'], html2text.html2text(example)
|
||||
example = d["example"] if d["example"] else "*No example available.*"
|
||||
response += "\n" + "* (**{}**) {}\n {}".format(
|
||||
d["type"], d["definition"], html2text.html2text(example)
|
||||
)
|
||||
|
||||
except Exception:
|
||||
|
|
|
@ -15,8 +15,8 @@ class TestDefineBot(BotTestCase, DefaultTests):
|
|||
"kept as a pet or for catching mice, and many breeds have been "
|
||||
"developed.\n their pet cat\n\n"
|
||||
)
|
||||
with self.mock_http_conversation('test_single_type_word'):
|
||||
self.verify_reply('cat', bot_response)
|
||||
with self.mock_http_conversation("test_single_type_word"):
|
||||
self.verify_reply("cat", bot_response)
|
||||
|
||||
# Multi-type word.
|
||||
bot_response = (
|
||||
|
@ -32,26 +32,26 @@ class TestDefineBot(BotTestCase, DefaultTests):
|
|||
"* (**exclamation**) used as an appeal for urgent assistance.\n"
|
||||
" Help! I'm drowning!\n\n"
|
||||
)
|
||||
with self.mock_http_conversation('test_multi_type_word'):
|
||||
self.verify_reply('help', bot_response)
|
||||
with self.mock_http_conversation("test_multi_type_word"):
|
||||
self.verify_reply("help", bot_response)
|
||||
|
||||
# Incorrect word.
|
||||
bot_response = "**foo**:\nCould not load definition."
|
||||
with self.mock_http_conversation('test_incorrect_word'):
|
||||
self.verify_reply('foo', bot_response)
|
||||
with self.mock_http_conversation("test_incorrect_word"):
|
||||
self.verify_reply("foo", bot_response)
|
||||
|
||||
# Phrases are not defined. No request is sent to the Internet.
|
||||
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
|
||||
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.
|
||||
bot_response = "Please enter a word to define."
|
||||
self.verify_reply('', bot_response)
|
||||
self.verify_reply("", bot_response)
|
||||
|
||||
def test_connection_error(self) -> None:
|
||||
with patch('requests.get', side_effect=Exception), patch('logging.exception'):
|
||||
self.verify_reply('aeroplane', '**aeroplane**:\nCould not load definition.')
|
||||
with patch("requests.get", side_effect=Exception), patch("logging.exception"):
|
||||
self.verify_reply("aeroplane", "**aeroplane**:\nCould not load definition.")
|
||||
|
|
|
@ -7,55 +7,55 @@ import apiai
|
|||
|
||||
from zulip_bots.lib import BotHandler
|
||||
|
||||
help_message = '''DialogFlow bot
|
||||
help_message = """DialogFlow bot
|
||||
This bot will interact with dialogflow bots.
|
||||
Simply send this bot a message, and it will respond depending on the configured bot's behaviour.
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
def get_bot_result(message_content: str, config: Dict[str, str], sender_id: str) -> str:
|
||||
if message_content.strip() == '' or message_content.strip() == 'help':
|
||||
return config['bot_info']
|
||||
ai = apiai.ApiAI(config['key'])
|
||||
if message_content.strip() == "" or message_content.strip() == "help":
|
||||
return config["bot_info"]
|
||||
ai = apiai.ApiAI(config["key"])
|
||||
try:
|
||||
request = ai.text_request()
|
||||
request.session_id = sender_id
|
||||
request.query = message_content
|
||||
response = request.getresponse()
|
||||
res_str = response.read().decode('utf8', 'ignore')
|
||||
res_str = response.read().decode("utf8", "ignore")
|
||||
res_json = json.loads(res_str)
|
||||
if res_json['status']['errorType'] != 'success' and 'result' not in res_json.keys():
|
||||
return 'Error {}: {}.'.format(
|
||||
res_json['status']['code'], res_json['status']['errorDetails']
|
||||
if res_json["status"]["errorType"] != "success" and "result" not in res_json.keys():
|
||||
return "Error {}: {}.".format(
|
||||
res_json["status"]["code"], res_json["status"]["errorDetails"]
|
||||
)
|
||||
if res_json['result']['fulfillment']['speech'] == '':
|
||||
if 'alternateResult' in res_json.keys():
|
||||
if res_json['alternateResult']['fulfillment']['speech'] != '':
|
||||
return res_json['alternateResult']['fulfillment']['speech']
|
||||
return 'Error. No result.'
|
||||
return res_json['result']['fulfillment']['speech']
|
||||
if res_json["result"]["fulfillment"]["speech"] == "":
|
||||
if "alternateResult" in res_json.keys():
|
||||
if res_json["alternateResult"]["fulfillment"]["speech"] != "":
|
||||
return res_json["alternateResult"]["fulfillment"]["speech"]
|
||||
return "Error. No result."
|
||||
return res_json["result"]["fulfillment"]["speech"]
|
||||
except Exception as e:
|
||||
logging.exception(str(e))
|
||||
return 'Error. {}.'.format(str(e))
|
||||
return "Error. {}.".format(str(e))
|
||||
|
||||
|
||||
class DialogFlowHandler:
|
||||
'''
|
||||
"""
|
||||
This plugin allows users to easily add their own
|
||||
DialogFlow bots to zulip
|
||||
'''
|
||||
"""
|
||||
|
||||
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:
|
||||
return '''
|
||||
return """
|
||||
This plugin will allow users to easily add their own
|
||||
DialogFlow bots to zulip
|
||||
'''
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -28,13 +28,13 @@ class MockTextRequest:
|
|||
def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]:
|
||||
response_data = read_bot_fixture_data(bot_name, test_name)
|
||||
try:
|
||||
response_data['request']
|
||||
df_response = response_data['response']
|
||||
response_data["request"]
|
||||
df_response = response_data["response"]
|
||||
except KeyError:
|
||||
print("ERROR: 'request' or 'response' field not found in fixture.")
|
||||
raise
|
||||
|
||||
with patch('apiai.ApiAI.text_request') as mock_text_request:
|
||||
with patch("apiai.ApiAI.text_request") as mock_text_request:
|
||||
request = MockTextRequest()
|
||||
request.response = df_response
|
||||
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):
|
||||
bot_name = 'dialogflow'
|
||||
bot_name = "dialogflow"
|
||||
|
||||
def _test(self, test_name: str, message: str, response: str) -> None:
|
||||
with self.mock_config_info(
|
||||
{'key': 'abcdefg', 'bot_info': 'bot info foo bar'}
|
||||
), mock_dialogflow(test_name, 'dialogflow'):
|
||||
{"key": "abcdefg", "bot_info": "bot info foo bar"}
|
||||
), mock_dialogflow(test_name, "dialogflow"):
|
||||
self.verify_reply(message, response)
|
||||
|
||||
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:
|
||||
self._test('test_403', 'hello', 'Error 403: Access Denied.')
|
||||
self._test("test_403", "hello", "Error 403: Access Denied.")
|
||||
|
||||
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:
|
||||
with patch('logging.exception'):
|
||||
self._test('test_exception', 'hello', 'Error. \'status\'.')
|
||||
with patch("logging.exception"):
|
||||
self._test("test_exception", "hello", "Error. 'status'.")
|
||||
|
||||
def test_help(self) -> None:
|
||||
self._test('test_normal', 'help', 'bot info foo bar')
|
||||
self._test('test_normal', '', 'bot info foo bar')
|
||||
self._test("test_normal", "help", "bot info foo bar")
|
||||
self._test("test_normal", "", "bot info foo bar")
|
||||
|
||||
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:
|
||||
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
|
||||
|
|
|
@ -9,21 +9,21 @@ URL = "[{name}](https://www.dropbox.com/home{path})"
|
|||
|
||||
|
||||
class DropboxHandler:
|
||||
'''
|
||||
"""
|
||||
This bot allows you to easily share, search and upload files
|
||||
between zulip and your dropbox account.
|
||||
'''
|
||||
"""
|
||||
|
||||
def initialize(self, bot_handler: BotHandler) -> None:
|
||||
self.config_info = bot_handler.get_config_info('dropbox_share')
|
||||
self.ACCESS_TOKEN = self.config_info.get('access_token')
|
||||
self.config_info = bot_handler.get_config_info("dropbox_share")
|
||||
self.ACCESS_TOKEN = self.config_info.get("access_token")
|
||||
self.client = Dropbox(self.ACCESS_TOKEN)
|
||||
|
||||
def usage(self) -> str:
|
||||
return get_help()
|
||||
|
||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||
command = message['content']
|
||||
command = message["content"]
|
||||
if command == "":
|
||||
command = "help"
|
||||
msg = dbx_command(self.client, command)
|
||||
|
@ -31,7 +31,7 @@ class DropboxHandler:
|
|||
|
||||
|
||||
def get_help() -> str:
|
||||
return '''
|
||||
return """
|
||||
Example commands:
|
||||
|
||||
```
|
||||
|
@ -44,11 +44,11 @@ def get_help() -> str:
|
|||
@mention-bot search: search a file/folder
|
||||
@mention-bot share: get a shareable link for the file/folder
|
||||
```
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
def get_usage_examples() -> str:
|
||||
return '''
|
||||
return """
|
||||
Usage:
|
||||
```
|
||||
@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 --fd foo - Search for boo in folder foo.
|
||||
```
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
REGEXES = dict(
|
||||
command='(ls|mkdir|read|rm|write|search|usage|help)',
|
||||
path=r'(\S+)',
|
||||
optional_path=r'(\S*)',
|
||||
some_text='(.+?)',
|
||||
folder=r'?(?:--fd (\S+))?',
|
||||
max_results=r'?(?:--mr (\d+))?',
|
||||
command="(ls|mkdir|read|rm|write|search|usage|help)",
|
||||
path=r"(\S+)",
|
||||
optional_path=r"(\S*)",
|
||||
some_text="(.+?)",
|
||||
folder=r"?(?:--fd (\S+))?",
|
||||
max_results=r"?(?:--mr (\d+))?",
|
||||
)
|
||||
|
||||
|
||||
def get_commands() -> Dict[str, Tuple[Any, List[str]]]:
|
||||
return {
|
||||
'help': (dbx_help, ['command']),
|
||||
'ls': (dbx_ls, ['optional_path']),
|
||||
'mkdir': (dbx_mkdir, ['path']),
|
||||
'rm': (dbx_rm, ['path']),
|
||||
'write': (dbx_write, ['path', 'some_text']),
|
||||
'read': (dbx_read, ['path']),
|
||||
'search': (dbx_search, ['some_text', 'folder', 'max_results']),
|
||||
'share': (dbx_share, ['path']),
|
||||
'usage': (dbx_usage, []),
|
||||
"help": (dbx_help, ["command"]),
|
||||
"ls": (dbx_ls, ["optional_path"]),
|
||||
"mkdir": (dbx_mkdir, ["path"]),
|
||||
"rm": (dbx_rm, ["path"]),
|
||||
"write": (dbx_write, ["path", "some_text"]),
|
||||
"read": (dbx_read, ["path"]),
|
||||
"search": (dbx_search, ["some_text", "folder", "max_results"]),
|
||||
"share": (dbx_share, ["path"]),
|
||||
"usage": (dbx_usage, []),
|
||||
}
|
||||
|
||||
|
||||
def dbx_command(client: Any, cmd: str) -> str:
|
||||
cmd = cmd.strip()
|
||||
if cmd == 'help':
|
||||
if cmd == "help":
|
||||
return get_help()
|
||||
cmd_name = cmd.split()[0]
|
||||
cmd_args = cmd[len(cmd_name) :].strip()
|
||||
commands = get_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]
|
||||
partial_regexes = [REGEXES[a] for a in arg_names]
|
||||
regex = ' '.join(partial_regexes)
|
||||
regex += '$'
|
||||
regex = " ".join(partial_regexes)
|
||||
regex += "$"
|
||||
m = re.match(regex, cmd_args)
|
||||
if m:
|
||||
return f(client, *m.groups())
|
||||
else:
|
||||
return 'ERROR: ' + syntax_help(cmd_name)
|
||||
return "ERROR: " + syntax_help(cmd_name)
|
||||
|
||||
|
||||
def syntax_help(cmd_name: str) -> str:
|
||||
commands = get_commands()
|
||||
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:
|
||||
cmd = cmd_name + ' ' + arg_syntax
|
||||
cmd = cmd_name + " " + arg_syntax
|
||||
else:
|
||||
cmd = cmd_name
|
||||
return 'syntax: {}'.format(cmd)
|
||||
return "syntax: {}".format(cmd)
|
||||
|
||||
|
||||
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:
|
||||
fn = '/' + fn # foo/boo -> /foo/boo
|
||||
fn = "/" + fn # foo/boo -> /foo/boo
|
||||
try:
|
||||
result = client.files_create_folder(fn)
|
||||
msg = "CREATED FOLDER: " + URL.format(name=result.name, path=result.path_lower)
|
||||
|
@ -143,8 +143,8 @@ def dbx_mkdir(client: Any, fn: str) -> str:
|
|||
|
||||
|
||||
def dbx_ls(client: Any, fn: str) -> str:
|
||||
if fn != '':
|
||||
fn = '/' + fn
|
||||
if fn != "":
|
||||
fn = "/" + fn
|
||||
|
||||
try:
|
||||
result = client.files_list_folder(fn)
|
||||
|
@ -152,9 +152,9 @@ def dbx_ls(client: Any, fn: str) -> str:
|
|||
for meta in result.entries:
|
||||
files_list += [" - " + URL.format(name=meta.name, path=meta.path_lower)]
|
||||
|
||||
msg = '\n'.join(files_list)
|
||||
if msg == '':
|
||||
msg = '`No files available`'
|
||||
msg = "\n".join(files_list)
|
||||
if msg == "":
|
||||
msg = "`No files available`"
|
||||
|
||||
except Exception:
|
||||
msg = (
|
||||
|
@ -167,7 +167,7 @@ def dbx_ls(client: Any, fn: str) -> str:
|
|||
|
||||
|
||||
def dbx_rm(client: Any, fn: str) -> str:
|
||||
fn = '/' + fn
|
||||
fn = "/" + fn
|
||||
|
||||
try:
|
||||
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:
|
||||
fn = '/' + fn
|
||||
fn = "/" + fn
|
||||
|
||||
try:
|
||||
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:
|
||||
fn = '/' + fn
|
||||
fn = "/" + fn
|
||||
|
||||
try:
|
||||
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:
|
||||
if folder is None:
|
||||
folder = ''
|
||||
folder = ""
|
||||
else:
|
||||
folder = '/' + folder
|
||||
folder = "/" + folder
|
||||
if max_results is None:
|
||||
max_results = '20'
|
||||
max_results = "20"
|
||||
try:
|
||||
result = client.files_search(folder, query, max_results=int(max_results))
|
||||
msg_list = []
|
||||
|
@ -221,7 +221,7 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
|
|||
file_info = entry.metadata
|
||||
count += 1
|
||||
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:
|
||||
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."
|
||||
)
|
||||
|
||||
if msg == '':
|
||||
if msg == "":
|
||||
msg = (
|
||||
"No files/folders found matching your query.\n"
|
||||
"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):
|
||||
fn = '/' + fn
|
||||
fn = "/" + fn
|
||||
try:
|
||||
result = client.sharing_create_shared_link(fn)
|
||||
msg = result.url
|
||||
|
|
|
@ -13,15 +13,15 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
|||
|
||||
def get_root_files_list(*args, **kwargs):
|
||||
return MockListFolderResult(
|
||||
entries=[MockFileMetadata('foo', '/foo'), MockFileMetadata('boo', '/boo')], has_more=False
|
||||
entries=[MockFileMetadata("foo", "/foo"), MockFileMetadata("boo", "/boo")], has_more=False
|
||||
)
|
||||
|
||||
|
||||
def get_folder_files_list(*args, **kwargs):
|
||||
return MockListFolderResult(
|
||||
entries=[
|
||||
MockFileMetadata('moo', '/foo/moo'),
|
||||
MockFileMetadata('noo', '/foo/noo'),
|
||||
MockFileMetadata("moo", "/foo/moo"),
|
||||
MockFileMetadata("noo", "/foo/noo"),
|
||||
],
|
||||
has_more=False,
|
||||
)
|
||||
|
@ -32,18 +32,18 @@ def get_empty_files_list(*args, **kwargs):
|
|||
|
||||
|
||||
def create_file(*args, **kwargs):
|
||||
return MockFileMetadata('foo', '/foo')
|
||||
return MockFileMetadata("foo", "/foo")
|
||||
|
||||
|
||||
def download_file(*args, **kwargs):
|
||||
return [MockFileMetadata('foo', '/foo'), MockHttpResponse('boo')]
|
||||
return [MockFileMetadata("foo", "/foo"), MockHttpResponse("boo")]
|
||||
|
||||
|
||||
def search_files(*args, **kwargs):
|
||||
return MockSearchResult(
|
||||
[
|
||||
MockSearchMatch(MockFileMetadata('foo', '/foo')),
|
||||
MockSearchMatch(MockFileMetadata('fooboo', '/fooboo')),
|
||||
MockSearchMatch(MockFileMetadata("foo", "/foo")),
|
||||
MockSearchMatch(MockFileMetadata("fooboo", "/fooboo")),
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -53,11 +53,11 @@ def get_empty_search_result(*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:
|
||||
return '''
|
||||
return """
|
||||
Example commands:
|
||||
|
||||
```
|
||||
|
@ -70,7 +70,7 @@ def get_help() -> str:
|
|||
@mention-bot search: search a file/folder
|
||||
@mention-bot share: get a shareable link for the file/folder
|
||||
```
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
class TestDropboxBot(BotTestCase, DefaultTests):
|
||||
|
@ -79,8 +79,8 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
|
||||
def test_bot_responds_to_empty_message(self):
|
||||
with self.mock_config_info(self.config_info):
|
||||
self.verify_reply('', get_help())
|
||||
self.verify_reply('help', get_help())
|
||||
self.verify_reply("", get_help())
|
||||
self.verify_reply("help", get_help())
|
||||
|
||||
def test_dbx_ls_root(self):
|
||||
bot_response = (
|
||||
|
@ -88,7 +88,7 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
" - [boo](https://www.dropbox.com/home/boo)"
|
||||
)
|
||||
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.verify_reply("ls", bot_response)
|
||||
|
||||
|
@ -98,14 +98,14 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
" - [noo](https://www.dropbox.com/home/foo/noo)"
|
||||
)
|
||||
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.verify_reply("ls foo", bot_response)
|
||||
|
||||
def test_dbx_ls_empty(self):
|
||||
bot_response = '`No files available`'
|
||||
bot_response = "`No files available`"
|
||||
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.verify_reply("ls", bot_response)
|
||||
|
||||
|
@ -116,16 +116,16 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
"or simply `ls` for listing folders in the root directory"
|
||||
)
|
||||
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.verify_reply("ls", bot_response)
|
||||
|
||||
def test_dbx_mkdir(self):
|
||||
bot_response = "CREATED FOLDER: [foo](https://www.dropbox.com/home/foo)"
|
||||
with patch(
|
||||
'dropbox.Dropbox.files_create_folder', side_effect=create_file
|
||||
"dropbox.Dropbox.files_create_folder", side_effect=create_file
|
||||
), 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):
|
||||
bot_response = (
|
||||
|
@ -133,49 +133,49 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
"Usage: `mkdir <foldername>` to create a folder."
|
||||
)
|
||||
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.verify_reply('mkdir foo/bar', bot_response)
|
||||
self.verify_reply("mkdir foo/bar", bot_response)
|
||||
|
||||
def test_dbx_rm(self):
|
||||
bot_response = "DELETED File/Folder : [foo](https://www.dropbox.com/home/foo)"
|
||||
with patch('dropbox.Dropbox.files_delete', side_effect=create_file), self.mock_config_info(
|
||||
with patch("dropbox.Dropbox.files_delete", side_effect=create_file), self.mock_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):
|
||||
bot_response = (
|
||||
"Please provide a correct folder path and name.\n"
|
||||
"Usage: `rm <foldername>` to delete a folder in root directory."
|
||||
)
|
||||
with patch('dropbox.Dropbox.files_delete', side_effect=Exception()), self.mock_config_info(
|
||||
with patch("dropbox.Dropbox.files_delete", side_effect=Exception()), self.mock_config_info(
|
||||
self.config_info
|
||||
):
|
||||
self.verify_reply('rm foo', bot_response)
|
||||
self.verify_reply("rm foo", bot_response)
|
||||
|
||||
def test_dbx_write(self):
|
||||
bot_response = "Written to file: [foo](https://www.dropbox.com/home/foo)"
|
||||
with patch('dropbox.Dropbox.files_upload', side_effect=create_file), self.mock_config_info(
|
||||
with patch("dropbox.Dropbox.files_upload", side_effect=create_file), self.mock_config_info(
|
||||
self.config_info
|
||||
):
|
||||
self.verify_reply('write foo boo', bot_response)
|
||||
self.verify_reply("write foo boo", bot_response)
|
||||
|
||||
def test_dbx_write_error(self):
|
||||
bot_response = (
|
||||
"Incorrect file path or file already exists.\nUsage: `write <filename> CONTENT`"
|
||||
)
|
||||
with patch('dropbox.Dropbox.files_upload', side_effect=Exception()), self.mock_config_info(
|
||||
with patch("dropbox.Dropbox.files_upload", side_effect=Exception()), self.mock_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):
|
||||
bot_response = "**foo** :\nboo"
|
||||
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.verify_reply('read foo', bot_response)
|
||||
self.verify_reply("read foo", bot_response)
|
||||
|
||||
def test_dbx_read_error(self):
|
||||
bot_response = (
|
||||
|
@ -183,16 +183,16 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
"Usage: `read <filename>` to read content of a file"
|
||||
)
|
||||
with patch(
|
||||
'dropbox.Dropbox.files_download', side_effect=Exception()
|
||||
"dropbox.Dropbox.files_download", side_effect=Exception()
|
||||
), 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):
|
||||
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.verify_reply('search foo', bot_response)
|
||||
self.verify_reply("search foo", bot_response)
|
||||
|
||||
def test_dbx_search_empty(self):
|
||||
bot_response = (
|
||||
|
@ -201,9 +201,9 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
" (i.e. “bat c” matches “bat cave” but not “batman car”)."
|
||||
)
|
||||
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.verify_reply('search boo --fd foo', bot_response)
|
||||
self.verify_reply("search boo --fd foo", bot_response)
|
||||
|
||||
def test_dbx_search_error(self):
|
||||
bot_response = (
|
||||
|
@ -211,32 +211,32 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
"Note:`--mr <int>` is optional and is used to specify maximun results.\n"
|
||||
" `--fd <folderName>` to search in specific folder."
|
||||
)
|
||||
with patch('dropbox.Dropbox.files_search', side_effect=Exception()), self.mock_config_info(
|
||||
with patch("dropbox.Dropbox.files_search", side_effect=Exception()), self.mock_config_info(
|
||||
self.config_info
|
||||
):
|
||||
self.verify_reply('search foo', bot_response)
|
||||
self.verify_reply("search foo", bot_response)
|
||||
|
||||
def test_dbx_share(self):
|
||||
bot_response = 'http://www.foo.com/boo'
|
||||
bot_response = "http://www.foo.com/boo"
|
||||
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.verify_reply('share boo', bot_response)
|
||||
self.verify_reply("share boo", bot_response)
|
||||
|
||||
def test_dbx_share_error(self):
|
||||
bot_response = "Please provide a correct file name.\nUsage: `share <filename>`"
|
||||
with patch(
|
||||
'dropbox.Dropbox.sharing_create_shared_link', side_effect=Exception()
|
||||
"dropbox.Dropbox.sharing_create_shared_link", side_effect=Exception()
|
||||
), 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):
|
||||
bot_response = 'syntax: ls <optional_path>'
|
||||
bot_response = "syntax: ls <optional_path>"
|
||||
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):
|
||||
bot_response = '''
|
||||
bot_response = """
|
||||
Usage:
|
||||
```
|
||||
@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 --fd foo - Search for boo in folder foo.
|
||||
```
|
||||
'''
|
||||
"""
|
||||
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):
|
||||
ls_error_response = "ERROR: syntax: ls <optional_path>"
|
||||
|
@ -277,7 +277,7 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
self.verify_reply("usage foo", usage_error_response)
|
||||
|
||||
def test_unkown_command(self):
|
||||
bot_response = '''ERROR: unrecognized command
|
||||
bot_response = """ERROR: unrecognized command
|
||||
|
||||
Example commands:
|
||||
|
||||
|
@ -291,6 +291,6 @@ class TestDropboxBot(BotTestCase, DefaultTests):
|
|||
@mention-bot search: search a file/folder
|
||||
@mention-bot share: get a shareable link for the file/folder
|
||||
```
|
||||
'''
|
||||
"""
|
||||
with self.mock_config_info(self.config_info):
|
||||
self.verify_reply('unknown command', bot_response)
|
||||
self.verify_reply("unknown command", bot_response)
|
||||
|
|
|
@ -7,9 +7,9 @@ def encrypt(text: str) -> str:
|
|||
# This is where the actual ROT13 is applied
|
||||
# WHY IS .JOIN NOT WORKING?!
|
||||
textlist = list(text)
|
||||
newtext = ''
|
||||
firsthalf = 'abcdefghijklmABCDEFGHIJKLM'
|
||||
lasthalf = 'nopqrstuvwxyzNOPQRSTUVWXYZ'
|
||||
newtext = ""
|
||||
firsthalf = "abcdefghijklmABCDEFGHIJKLM"
|
||||
lasthalf = "nopqrstuvwxyzNOPQRSTUVWXYZ"
|
||||
for char in textlist:
|
||||
if char in firsthalf:
|
||||
newtext += lasthalf[firsthalf.index(char)]
|
||||
|
@ -22,24 +22,24 @@ def encrypt(text: str) -> str:
|
|||
|
||||
|
||||
class EncryptHandler:
|
||||
'''
|
||||
"""
|
||||
This bot allows users to quickly encrypt messages using ROT13 encryption.
|
||||
It encrypts/decrypts messages starting with @mention-bot.
|
||||
'''
|
||||
"""
|
||||
|
||||
def usage(self) -> str:
|
||||
return '''
|
||||
return """
|
||||
This bot uses ROT13 encryption for its purposes.
|
||||
It responds to me starting with @mention-bot.
|
||||
Feeding encrypted messages into the bot decrypts them.
|
||||
'''
|
||||
"""
|
||||
|
||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||
bot_response = self.get_bot_encrypt_response(message)
|
||||
bot_handler.send_reply(message, bot_response)
|
||||
|
||||
def get_bot_encrypt_response(self, message: Dict[str, str]) -> str:
|
||||
original_content = message['content']
|
||||
original_content = message["content"]
|
||||
temp_content = encrypt(original_content)
|
||||
send_content = "Encrypted/Decrypted text: " + temp_content
|
||||
return send_content
|
||||
|
|
|
@ -7,7 +7,7 @@ class TestEncryptBot(BotTestCase, DefaultTests):
|
|||
def test_bot(self) -> None:
|
||||
dialog = [
|
||||
("", "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..!!"),
|
||||
("foo bar", "Encrypted/Decrypted text: sbb one"),
|
||||
("Please encrypt this", "Encrypted/Decrypted text: Cyrnfr rapelcg guvf"),
|
||||
|
|
|
@ -8,36 +8,36 @@ from zulip_bots.lib import BotHandler
|
|||
class FileUploaderHandler:
|
||||
def usage(self) -> str:
|
||||
return (
|
||||
'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 help : Display help message'
|
||||
"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 help : Display help message"
|
||||
)
|
||||
|
||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||
HELP_STR = (
|
||||
'Use this bot with any of the following commands:'
|
||||
'\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file'
|
||||
'\n* `@uploader help` : Display help message'
|
||||
"Use this bot with any of the following commands:"
|
||||
"\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file"
|
||||
"\n* `@uploader help` : Display help message"
|
||||
)
|
||||
|
||||
content = message['content'].strip()
|
||||
if content == 'help':
|
||||
content = message["content"].strip()
|
||||
if content == "help":
|
||||
bot_handler.send_reply(message, HELP_STR)
|
||||
return
|
||||
|
||||
path = Path(os.path.expanduser(content))
|
||||
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
|
||||
|
||||
path = path.resolve()
|
||||
upload = bot_handler.upload_file_from_path(str(path))
|
||||
if upload['result'] != 'success':
|
||||
msg = upload['msg']
|
||||
bot_handler.send_reply(message, 'Failed to upload `{}` file: {}'.format(path, msg))
|
||||
if upload["result"] != "success":
|
||||
msg = upload["msg"]
|
||||
bot_handler.send_reply(message, "Failed to upload `{}` file: {}".format(path, msg))
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -7,36 +7,36 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
|||
class TestFileUploaderBot(BotTestCase, DefaultTests):
|
||||
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:
|
||||
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.is_file', return_value=True)
|
||||
@patch("pathlib.Path.resolve", return_value=Path("/file.txt"))
|
||||
@patch("pathlib.Path.is_file", return_value=True)
|
||||
def test_file_upload_failed(self, is_file: Mock, resolve: Mock) -> None:
|
||||
server_reply = dict(result='', msg='error')
|
||||
server_reply = dict(result="", msg="error")
|
||||
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', '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.is_file', return_value=True)
|
||||
@patch("pathlib.Path.resolve", return_value=Path("/file.txt"))
|
||||
@patch("pathlib.Path.is_file", return_value=True)
|
||||
def test_file_upload_success(self, is_file: Mock, resolve: Mock) -> None:
|
||||
server_reply = dict(result='success', uri='https://file/uri')
|
||||
server_reply = dict(result="success", uri="https://file/uri")
|
||||
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):
|
||||
self.verify_reply(
|
||||
'help',
|
||||
"help",
|
||||
(
|
||||
'Use this bot with any of the following commands:'
|
||||
'\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file'
|
||||
'\n* `@uploader help` : Display help message'
|
||||
"Use this bot with any of the following commands:"
|
||||
"\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file"
|
||||
"\n* `@uploader help` : Display help message"
|
||||
),
|
||||
)
|
||||
|
|
|
@ -6,20 +6,20 @@ from requests.exceptions import ConnectionError
|
|||
|
||||
from zulip_bots.lib import BotHandler
|
||||
|
||||
USERS_LIST_URL = 'https://api.flock.co/v1/roster.listContacts'
|
||||
SEND_MESSAGE_URL = 'https://api.flock.co/v1/chat.sendMessage'
|
||||
USERS_LIST_URL = "https://api.flock.co/v1/roster.listContacts"
|
||||
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.
|
||||
*Syntax*: **@botname to: message** where `to` is **firstName** of recipient.
|
||||
'''
|
||||
"""
|
||||
|
||||
# Matches the recipient name provided by user with list of users in his contacts.
|
||||
# If matches, returns the matched User's ID
|
||||
def find_recipient_id(users: List[Any], recipient_name: str) -> str:
|
||||
for user in users:
|
||||
if recipient_name == user['firstName']:
|
||||
return user['id']
|
||||
if recipient_name == user["firstName"]:
|
||||
return user["id"]
|
||||
|
||||
|
||||
# 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(
|
||||
recipient_name: str, config: Dict[str, str]
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
token = config['token']
|
||||
payload = {'token': token}
|
||||
token = config["token"]
|
||||
payload = {"token": token}
|
||||
users, error = make_flock_request(USERS_LIST_URL, payload)
|
||||
if users is None:
|
||||
return (None, error)
|
||||
|
@ -58,8 +58,8 @@ def get_recipient_id(
|
|||
|
||||
# This handles the message sending work.
|
||||
def get_flock_response(content: str, config: Dict[str, str]) -> str:
|
||||
token = config['token']
|
||||
content_pieces = content.split(':')
|
||||
token = config["token"]
|
||||
content_pieces = content.split(":")
|
||||
recipient_name = content_pieces[0].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:
|
||||
return "Found user is invalid."
|
||||
|
||||
payload = {'to': recipient_id, 'text': message, 'token': token}
|
||||
payload = {"to": recipient_id, "text": message, "token": token}
|
||||
res, error = make_flock_request(SEND_MESSAGE_URL, payload)
|
||||
if res is None:
|
||||
return error
|
||||
|
@ -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:
|
||||
content = content.strip()
|
||||
if content == '' or content == 'help':
|
||||
if content == "" or content == "help":
|
||||
return help_message
|
||||
else:
|
||||
result = get_flock_response(content, config)
|
||||
|
@ -91,20 +91,20 @@ def get_flock_bot_response(content: str, config: Dict[str, str]) -> None:
|
|||
|
||||
|
||||
class FlockHandler:
|
||||
'''
|
||||
"""
|
||||
This is flock bot. Now you can send messages to any of your
|
||||
flock user without having to leave Zulip.
|
||||
'''
|
||||
"""
|
||||
|
||||
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:
|
||||
return '''Hello from Flock Bot. You can send messages to any Flock user
|
||||
right from Zulip.'''
|
||||
return """Hello from Flock Bot. You can send messages to any Flock user
|
||||
right from Zulip."""
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -11,74 +11,74 @@ class TestFlockBot(BotTestCase, DefaultTests):
|
|||
|
||||
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.
|
||||
*Syntax*: **@botname to: message** where `to` is **firstName** of recipient.
|
||||
'''
|
||||
"""
|
||||
|
||||
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:
|
||||
self.verify_reply('', self.help_message)
|
||||
self.verify_reply("", self.help_message)
|
||||
|
||||
def test_fetch_id_connection_error(self) -> None:
|
||||
with self.mock_config_info(self.normal_config), patch(
|
||||
'requests.get', side_effect=ConnectionError()
|
||||
), patch('logging.exception'):
|
||||
"requests.get", side_effect=ConnectionError()
|
||||
), patch("logging.exception"):
|
||||
self.verify_reply(
|
||||
'tyler: Hey tyler',
|
||||
"Uh-Oh, couldn\'t process the request \
|
||||
"tyler: Hey tyler",
|
||||
"Uh-Oh, couldn't process the request \
|
||||
right now.\nPlease try again later",
|
||||
)
|
||||
|
||||
def test_response_connection_error(self) -> None:
|
||||
with self.mock_config_info(self.message_config), patch(
|
||||
'requests.get', side_effect=ConnectionError()
|
||||
), patch('logging.exception'):
|
||||
"requests.get", side_effect=ConnectionError()
|
||||
), patch("logging.exception"):
|
||||
self.verify_reply(
|
||||
'Ricky: test message',
|
||||
"Uh-Oh, couldn\'t process the request \
|
||||
"Ricky: test message",
|
||||
"Uh-Oh, couldn't process the request \
|
||||
right now.\nPlease try again later",
|
||||
)
|
||||
|
||||
def test_no_recipient_found(self) -> None:
|
||||
bot_response = "No user found. Make sure you typed it correctly."
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_no_recipient_found'
|
||||
"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:
|
||||
bot_response = "Found user is invalid."
|
||||
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:
|
||||
bot_response = "Uh-Oh, couldn't process the request right now.\nPlease try again later"
|
||||
get_recipient_id.return_value = ["u:userid", None]
|
||||
with self.mock_config_info(self.normal_config), patch(
|
||||
'requests.get', side_effect=ConnectionError()
|
||||
), patch('logging.exception'):
|
||||
self.verify_reply('Rishabh: hi there', bot_response)
|
||||
"requests.get", side_effect=ConnectionError()
|
||||
), patch("logging.exception"):
|
||||
self.verify_reply("Rishabh: hi there", bot_response)
|
||||
|
||||
@patch('zulip_bots.bots.flock.flock.get_recipient_id')
|
||||
@patch("zulip_bots.bots.flock.flock.get_recipient_id")
|
||||
def test_message_send_success(self, get_recipient_id: str) -> None:
|
||||
bot_response = "Message sent."
|
||||
get_recipient_id.return_value = ["u:userid", None]
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_message_send_success'
|
||||
"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:
|
||||
bot_response = "Message sending failed :slightly_frowning_face:. Please try again."
|
||||
get_recipient_id.return_value = ["u:invalid", None]
|
||||
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
|
||||
'test_message_send_failed'
|
||||
"test_message_send_failed"
|
||||
):
|
||||
self.verify_reply('Rishabh: hi there', bot_response)
|
||||
self.verify_reply("Rishabh: hi there", bot_response)
|
||||
|
|
|
@ -5,7 +5,7 @@ from zulip_bots.lib import BotHandler
|
|||
|
||||
|
||||
class FollowupHandler:
|
||||
'''
|
||||
"""
|
||||
This plugin facilitates creating follow-up tasks when
|
||||
you are using Zulip to conduct a virtual meeting. It
|
||||
looks for messages starting with '@mention-bot'.
|
||||
|
@ -14,45 +14,45 @@ class FollowupHandler:
|
|||
Zulip stream called "followup," but this code could
|
||||
be adapted to write follow up items to some kind of
|
||||
external issue tracker as well.
|
||||
'''
|
||||
"""
|
||||
|
||||
def usage(self) -> str:
|
||||
return '''
|
||||
return """
|
||||
This plugin will allow users to flag messages
|
||||
as being follow-up items. Users should preface
|
||||
messages with "@mention-bot".
|
||||
|
||||
Before running this, make sure to create a stream
|
||||
called "followup" that your API user can send to.
|
||||
'''
|
||||
"""
|
||||
|
||||
def initialize(self, bot_handler: BotHandler) -> None:
|
||||
self.config_info = bot_handler.get_config_info('followup', optional=False)
|
||||
self.stream = self.config_info.get("stream", 'followup')
|
||||
self.config_info = bot_handler.get_config_info("followup", optional=False)
|
||||
self.stream = self.config_info.get("stream", "followup")
|
||||
|
||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||
if message['content'] == '':
|
||||
if message["content"] == "":
|
||||
bot_response = (
|
||||
"Please specify the message you want to send to followup stream after @mention-bot"
|
||||
)
|
||||
bot_handler.send_reply(message, bot_response)
|
||||
elif message['content'] == 'help':
|
||||
elif message["content"] == "help":
|
||||
bot_handler.send_reply(message, self.usage())
|
||||
else:
|
||||
bot_response = self.get_bot_followup_response(message)
|
||||
bot_handler.send_message(
|
||||
dict(
|
||||
type='stream',
|
||||
type="stream",
|
||||
to=self.stream,
|
||||
subject=message['sender_email'],
|
||||
subject=message["sender_email"],
|
||||
content=bot_response,
|
||||
)
|
||||
)
|
||||
|
||||
def get_bot_followup_response(self, message: Dict[str, str]) -> str:
|
||||
original_content = message['content']
|
||||
original_sender = message['sender_email']
|
||||
temp_content = 'from %s: ' % (original_sender,)
|
||||
original_content = message["content"]
|
||||
original_sender = message["sender_email"]
|
||||
temp_content = "from %s: " % (original_sender,)
|
||||
new_content = temp_content + original_content
|
||||
|
||||
return new_content
|
||||
|
|
|
@ -6,48 +6,48 @@ class TestFollowUpBot(BotTestCase, DefaultTests):
|
|||
|
||||
def test_followup_stream(self) -> None:
|
||||
message = dict(
|
||||
content='feed the cat',
|
||||
type='stream',
|
||||
sender_email='foo@example.com',
|
||||
content="feed the cat",
|
||||
type="stream",
|
||||
sender_email="foo@example.com",
|
||||
)
|
||||
|
||||
with self.mock_config_info({'stream': 'followup'}):
|
||||
with self.mock_config_info({"stream": "followup"}):
|
||||
response = self.get_response(message)
|
||||
|
||||
self.assertEqual(response['content'], 'from foo@example.com: feed the cat')
|
||||
self.assertEqual(response['to'], 'followup')
|
||||
self.assertEqual(response["content"], "from foo@example.com: feed the cat")
|
||||
self.assertEqual(response["to"], "followup")
|
||||
|
||||
def test_different_stream(self) -> None:
|
||||
message = dict(
|
||||
content='feed the cat',
|
||||
type='stream',
|
||||
sender_email='foo@example.com',
|
||||
content="feed the cat",
|
||||
type="stream",
|
||||
sender_email="foo@example.com",
|
||||
)
|
||||
|
||||
with self.mock_config_info({'stream': 'issue'}):
|
||||
with self.mock_config_info({"stream": "issue"}):
|
||||
response = self.get_response(message)
|
||||
|
||||
self.assertEqual(response['content'], 'from foo@example.com: feed the cat')
|
||||
self.assertEqual(response['to'], 'issue')
|
||||
self.assertEqual(response["content"], "from foo@example.com: feed the cat")
|
||||
self.assertEqual(response["to"], "issue")
|
||||
|
||||
def test_bot_responds_to_empty_message(self) -> None:
|
||||
bot_response = (
|
||||
'Please specify the message you want to send to followup stream after @mention-bot'
|
||||
"Please specify the message you want to send to followup stream after @mention-bot"
|
||||
)
|
||||
|
||||
with self.mock_config_info({'stream': 'followup'}):
|
||||
self.verify_reply('', bot_response)
|
||||
with self.mock_config_info({"stream": "followup"}):
|
||||
self.verify_reply("", bot_response)
|
||||
|
||||
def test_help_text(self) -> None:
|
||||
request = 'help'
|
||||
bot_response = '''
|
||||
request = "help"
|
||||
bot_response = """
|
||||
This plugin will allow users to flag messages
|
||||
as being follow-up items. Users should preface
|
||||
messages with "@mention-bot".
|
||||
|
||||
Before running this, make sure to create a stream
|
||||
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)
|
||||
|
|
|
@ -9,24 +9,24 @@ from zulip_bots.lib import BotHandler
|
|||
class FrontHandler:
|
||||
FRONT_API = "https://api2.frontapp.com/conversations/{}"
|
||||
COMMANDS = [
|
||||
('archive', "Archive a conversation."),
|
||||
('delete', "Delete a conversation."),
|
||||
('spam', "Mark a conversation as spam."),
|
||||
('open', "Restore a conversation."),
|
||||
('comment <text>', "Leave a comment."),
|
||||
("archive", "Archive a conversation."),
|
||||
("delete", "Delete a conversation."),
|
||||
("spam", "Mark a conversation as spam."),
|
||||
("open", "Restore a conversation."),
|
||||
("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 "
|
||||
|
||||
def usage(self) -> str:
|
||||
return '''
|
||||
return """
|
||||
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.
|
||||
'''
|
||||
"""
|
||||
|
||||
def initialize(self, bot_handler: BotHandler) -> None:
|
||||
config = bot_handler.get_config_info('front')
|
||||
api_key = config.get('api_key')
|
||||
config = bot_handler.get_config_info("front")
|
||||
api_key = config.get("api_key")
|
||||
if not api_key:
|
||||
raise KeyError("No API key specified.")
|
||||
|
||||
|
@ -100,9 +100,9 @@ class FrontHandler:
|
|||
return "Comment was sent."
|
||||
|
||||
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:
|
||||
bot_handler.send_reply(
|
||||
message,
|
||||
|
@ -114,25 +114,25 @@ class FrontHandler:
|
|||
|
||||
self.conversation_id = result.group()
|
||||
|
||||
if command == 'help':
|
||||
if command == "help":
|
||||
bot_handler.send_reply(message, self.help(bot_handler))
|
||||
|
||||
elif command == 'archive':
|
||||
elif command == "archive":
|
||||
bot_handler.send_reply(message, self.archive(bot_handler))
|
||||
|
||||
elif command == 'delete':
|
||||
elif command == "delete":
|
||||
bot_handler.send_reply(message, self.delete(bot_handler))
|
||||
|
||||
elif command == 'spam':
|
||||
elif command == "spam":
|
||||
bot_handler.send_reply(message, self.spam(bot_handler))
|
||||
|
||||
elif command == 'open':
|
||||
elif command == "open":
|
||||
bot_handler.send_reply(message, self.restore(bot_handler))
|
||||
|
||||
elif command.startswith(self.COMMENT_PREFIX):
|
||||
kwargs = {
|
||||
'author_id': "alt:email:" + message['sender_email'],
|
||||
'body': command[len(self.COMMENT_PREFIX) :],
|
||||
"author_id": "alt:email:" + message["sender_email"],
|
||||
"body": command[len(self.COMMENT_PREFIX) :],
|
||||
}
|
||||
bot_handler.send_reply(message, self.comment(bot_handler, **kwargs))
|
||||
else:
|
||||
|
|
|
@ -4,28 +4,28 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
|||
|
||||
|
||||
class TestFrontBot(BotTestCase, DefaultTests):
|
||||
bot_name = 'front'
|
||||
bot_name = "front"
|
||||
|
||||
def make_request_message(self, content: str) -> Dict[str, Any]:
|
||||
message = super().make_request_message(content)
|
||||
message['subject'] = "cnv_kqatm2"
|
||||
message['sender_email'] = "leela@planet-express.com"
|
||||
message["subject"] = "cnv_kqatm2"
|
||||
message["sender_email"] = "leela@planet-express.com"
|
||||
return message
|
||||
|
||||
def test_bot_invalid_api_key(self) -> None:
|
||||
invalid_api_key = ''
|
||||
with self.mock_config_info({'api_key': invalid_api_key}):
|
||||
invalid_api_key = ""
|
||||
with self.mock_config_info({"api_key": invalid_api_key}):
|
||||
with self.assertRaises(KeyError):
|
||||
bot, bot_handler = self._get_handlers()
|
||||
|
||||
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.")
|
||||
|
||||
def test_help(self) -> None:
|
||||
with self.mock_config_info({'api_key': "TEST"}):
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
self.verify_reply(
|
||||
'help',
|
||||
"help",
|
||||
"`archive` Archive a conversation.\n"
|
||||
"`delete` Delete a conversation.\n"
|
||||
"`spam` Mark a conversation as spam.\n"
|
||||
|
@ -34,71 +34,71 @@ class TestFrontBot(BotTestCase, DefaultTests):
|
|||
)
|
||||
|
||||
def test_archive(self) -> None:
|
||||
with self.mock_config_info({'api_key': "TEST"}):
|
||||
with self.mock_http_conversation('archive'):
|
||||
self.verify_reply('archive', "Conversation was archived.")
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("archive"):
|
||||
self.verify_reply("archive", "Conversation was archived.")
|
||||
|
||||
def test_archive_error(self) -> None:
|
||||
self._test_command_error('archive')
|
||||
self._test_command_error("archive")
|
||||
|
||||
def test_delete(self) -> None:
|
||||
with self.mock_config_info({'api_key': "TEST"}):
|
||||
with self.mock_http_conversation('delete'):
|
||||
self.verify_reply('delete', "Conversation was deleted.")
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("delete"):
|
||||
self.verify_reply("delete", "Conversation was deleted.")
|
||||
|
||||
def test_delete_error(self) -> None:
|
||||
self._test_command_error('delete')
|
||||
self._test_command_error("delete")
|
||||
|
||||
def test_spam(self) -> None:
|
||||
with self.mock_config_info({'api_key': "TEST"}):
|
||||
with self.mock_http_conversation('spam'):
|
||||
self.verify_reply('spam', "Conversation was marked as spam.")
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("spam"):
|
||||
self.verify_reply("spam", "Conversation was marked as spam.")
|
||||
|
||||
def test_spam_error(self) -> None:
|
||||
self._test_command_error('spam')
|
||||
self._test_command_error("spam")
|
||||
|
||||
def test_restore(self) -> None:
|
||||
with self.mock_config_info({'api_key': "TEST"}):
|
||||
with self.mock_http_conversation('open'):
|
||||
self.verify_reply('open', "Conversation was restored.")
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("open"):
|
||||
self.verify_reply("open", "Conversation was restored.")
|
||||
|
||||
def test_restore_error(self) -> None:
|
||||
self._test_command_error('open')
|
||||
self._test_command_error("open")
|
||||
|
||||
def test_comment(self) -> None:
|
||||
body = "@bender, I thought you were supposed to be cooking for this party."
|
||||
with self.mock_config_info({'api_key': "TEST"}):
|
||||
with self.mock_http_conversation('comment'):
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("comment"):
|
||||
self.verify_reply("comment " + body, "Comment was sent.")
|
||||
|
||||
def test_comment_error(self) -> None:
|
||||
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:
|
||||
bot_command = command_name
|
||||
if command_arg:
|
||||
bot_command += ' {}'.format(command_arg)
|
||||
with self.mock_config_info({'api_key': "TEST"}):
|
||||
with self.mock_http_conversation('{}_error'.format(command_name)):
|
||||
self.verify_reply(bot_command, 'Something went wrong.')
|
||||
bot_command += " {}".format(command_arg)
|
||||
with self.mock_config_info({"api_key": "TEST"}):
|
||||
with self.mock_http_conversation("{}_error".format(command_name)):
|
||||
self.verify_reply(bot_command, "Something went wrong.")
|
||||
|
||||
|
||||
class TestFrontBotWrongTopic(BotTestCase, DefaultTests):
|
||||
bot_name = 'front'
|
||||
bot_name = "front"
|
||||
|
||||
def make_request_message(self, content: str) -> Dict[str, Any]:
|
||||
message = super().make_request_message(content)
|
||||
message['subject'] = "kqatm2"
|
||||
message["subject"] = "kqatm2"
|
||||
return message
|
||||
|
||||
def test_bot_responds_to_empty_message(self) -> None:
|
||||
pass
|
||||
|
||||
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(
|
||||
'archive',
|
||||
"archive",
|
||||
"No coversation ID found. Please make "
|
||||
"sure that the name of the topic "
|
||||
"contains a valid coversation ID.",
|
||||
|
|
|
@ -4,54 +4,54 @@ from zulip_bots.game_handler import BadMoveException, GameAdapter
|
|||
|
||||
|
||||
class GameHandlerBotMessageHandler:
|
||||
tokens = [':blue_circle:', ':red_circle:']
|
||||
tokens = [":blue_circle:", ":red_circle:"]
|
||||
|
||||
def parse_board(self, board: Any) -> str:
|
||||
return 'foo'
|
||||
return "foo"
|
||||
|
||||
def get_player_color(self, turn: int) -> str:
|
||||
return self.tokens[turn]
|
||||
|
||||
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
||||
column_number = move_info.replace('move ', '')
|
||||
return original_player + ' moved in column ' + column_number
|
||||
column_number = move_info.replace("move ", "")
|
||||
return original_player + " moved in column " + column_number
|
||||
|
||||
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 \
|
||||
Good Luck!'
|
||||
Good Luck!"
|
||||
|
||||
|
||||
class MockModel:
|
||||
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:
|
||||
if not is_computer:
|
||||
if int(move.replace('move ', '')) < 9:
|
||||
return 'mock board'
|
||||
if int(move.replace("move ", "")) < 9:
|
||||
return "mock board"
|
||||
else:
|
||||
raise BadMoveException('Invalid Move.')
|
||||
return 'mock board'
|
||||
raise BadMoveException("Invalid Move.")
|
||||
return "mock board"
|
||||
|
||||
def determine_game_over(self, players: List[str]) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class GameHandlerBotHandler(GameAdapter):
|
||||
'''
|
||||
"""
|
||||
DO NOT USE THIS BOT
|
||||
This bot is used to test game_handler.py
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
game_name = 'foo test game'
|
||||
bot_name = 'game_handler_bot'
|
||||
move_help_message = '* To make your move during a game, type\n```move <column-number>```'
|
||||
move_regex = r'move (\d)$'
|
||||
game_name = "foo test game"
|
||||
bot_name = "game_handler_bot"
|
||||
move_help_message = "* To make your move during a game, type\n```move <column-number>```"
|
||||
move_regex = r"move (\d)$"
|
||||
model = MockModel
|
||||
gameMessageHandler = GameHandlerBotMessageHandler
|
||||
rules = ''
|
||||
rules = ""
|
||||
|
||||
super().__init__(
|
||||
game_name,
|
||||
|
|
|
@ -7,16 +7,16 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
|||
|
||||
|
||||
class TestGameHandlerBot(BotTestCase, DefaultTests):
|
||||
bot_name = 'game_handler_bot'
|
||||
bot_name = "game_handler_bot"
|
||||
|
||||
def make_request_message(
|
||||
self,
|
||||
content: str,
|
||||
user: str = 'foo@example.com',
|
||||
user_name: str = 'foo',
|
||||
type: str = 'private',
|
||||
stream: str = '',
|
||||
subject: str = '',
|
||||
user: str = "foo@example.com",
|
||||
user_name: str = "foo",
|
||||
type: str = "private",
|
||||
stream: str = "",
|
||||
subject: str = "",
|
||||
) -> Dict[str, str]:
|
||||
message = dict(
|
||||
sender_email=user,
|
||||
|
@ -35,58 +35,58 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
|||
expected_response: str,
|
||||
response_number: int,
|
||||
bot: Any = None,
|
||||
user_name: str = 'foo',
|
||||
stream: str = '',
|
||||
subject: str = '',
|
||||
user_name: str = "foo",
|
||||
stream: str = "",
|
||||
subject: str = "",
|
||||
max_messages: int = 20,
|
||||
) -> None:
|
||||
'''
|
||||
"""
|
||||
This function serves a similar purpose
|
||||
to BotTestCase.verify_dialog, but allows
|
||||
for multiple responses to be validated,
|
||||
and for mocking of the bot's internal data
|
||||
'''
|
||||
"""
|
||||
if bot is None:
|
||||
bot, bot_handler = self._get_handlers()
|
||||
else:
|
||||
_b, bot_handler = self._get_handlers()
|
||||
type = 'private' if stream == '' else 'stream'
|
||||
type = "private" if stream == "" else "stream"
|
||||
message = self.make_request_message(
|
||||
request, user_name + '@example.com', user_name, type, stream, subject
|
||||
request, user_name + "@example.com", user_name, type, stream, subject
|
||||
)
|
||||
bot_handler.reset_transcript()
|
||||
bot.handle_message(message, bot_handler)
|
||||
|
||||
responses = [message for (method, message) in bot_handler.transcript]
|
||||
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)
|
||||
|
||||
def add_user_to_cache(self, name: str, bot: Any = None) -> Any:
|
||||
if bot is None:
|
||||
bot, bot_handler = self._get_handlers()
|
||||
message = {
|
||||
'sender_email': '{}@example.com'.format(name),
|
||||
'sender_full_name': '{}'.format(name),
|
||||
"sender_email": "{}@example.com".format(name),
|
||||
"sender_full_name": "{}".format(name),
|
||||
}
|
||||
bot.add_user_to_cache(message)
|
||||
return bot
|
||||
|
||||
def setup_game(
|
||||
self,
|
||||
id: str = '',
|
||||
id: str = "",
|
||||
bot: Any = None,
|
||||
players: List[str] = ['foo', 'baz'],
|
||||
subject: str = 'test game',
|
||||
stream: str = 'test',
|
||||
players: List[str] = ["foo", "baz"],
|
||||
subject: str = "test game",
|
||||
stream: str = "test",
|
||||
) -> Any:
|
||||
if bot is None:
|
||||
bot, bot_handler = self._get_handlers()
|
||||
for p in players:
|
||||
self.add_user_to_cache(p, bot)
|
||||
players_emails = [p + '@example.com' for p in players]
|
||||
game_id = 'abc123'
|
||||
if id != '':
|
||||
players_emails = [p + "@example.com" for p in players]
|
||||
game_id = "abc123"
|
||||
if id != "":
|
||||
game_id = id
|
||||
instance = GameInstance(bot, False, subject, game_id, players_emails, stream)
|
||||
bot.instances.update({game_id: instance})
|
||||
|
@ -95,18 +95,18 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
|||
return bot
|
||||
|
||||
def setup_computer_game(self) -> Any:
|
||||
bot = self.add_user_to_cache('foo')
|
||||
bot.email = 'test-bot@example.com'
|
||||
self.add_user_to_cache('test-bot', bot)
|
||||
bot = self.add_user_to_cache("foo")
|
||||
bot.email = "test-bot@example.com"
|
||||
self.add_user_to_cache("test-bot", bot)
|
||||
instance = GameInstance(
|
||||
bot, False, 'test game', 'abc123', ['foo@example.com', 'test-bot@example.com'], 'test'
|
||||
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()
|
||||
return bot
|
||||
|
||||
def help_message(self) -> str:
|
||||
return '''** foo test game Bot Help:**
|
||||
return """** foo test game Bot Help:**
|
||||
*Preface all commands with @**test-bot***
|
||||
* To start a game in a stream (*recommended*), type
|
||||
`start game`
|
||||
|
@ -129,319 +129,319 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
|||
* To see rules of this game, type
|
||||
`rules`
|
||||
* To make your move during a game, type
|
||||
```move <column-number>```'''
|
||||
```move <column-number>```"""
|
||||
|
||||
def test_help_message(self) -> None:
|
||||
self.verify_response('help', self.help_message(), 0)
|
||||
self.verify_response('foo bar baz', self.help_message(), 0)
|
||||
self.verify_response("help", self.help_message(), 0)
|
||||
self.verify_response("foo bar baz", self.help_message(), 0)
|
||||
|
||||
def test_exception_handling(self) -> None:
|
||||
with patch('logging.exception'), patch(
|
||||
'zulip_bots.game_handler.GameAdapter.command_quit', side_effect=Exception
|
||||
with patch("logging.exception"), patch(
|
||||
"zulip_bots.game_handler.GameAdapter.command_quit", side_effect=Exception
|
||||
):
|
||||
self.verify_response('quit', 'Error .', 0)
|
||||
self.verify_response("quit", "Error .", 0)
|
||||
|
||||
def test_not_in_game_messages(self) -> None:
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'You are not in a game at the moment. Type `help` for help.',
|
||||
"move 3",
|
||||
"You are not in a game at the moment. Type `help` for help.",
|
||||
0,
|
||||
max_messages=1,
|
||||
)
|
||||
self.verify_response(
|
||||
'quit', 'You are not in a game. Type `help` for all commands.', 0, max_messages=1
|
||||
"quit", "You are not in a game. Type `help` for all commands.", 0, max_messages=1
|
||||
)
|
||||
|
||||
def test_start_game_with_name(self) -> None:
|
||||
bot = self.add_user_to_cache('baz')
|
||||
bot = self.add_user_to_cache("baz")
|
||||
self.verify_response(
|
||||
'start game with @**baz**',
|
||||
'You\'ve sent an invitation to play foo test game with @**baz**',
|
||||
"start game with @**baz**",
|
||||
"You've sent an invitation to play foo test game with @**baz**",
|
||||
1,
|
||||
bot=bot,
|
||||
)
|
||||
self.assertEqual(len(bot.invites), 1)
|
||||
|
||||
def test_start_game_with_email(self) -> None:
|
||||
bot = self.add_user_to_cache('baz')
|
||||
bot = self.add_user_to_cache("baz")
|
||||
self.verify_response(
|
||||
'start game with baz@example.com',
|
||||
'You\'ve sent an invitation to play foo test game with @**baz**',
|
||||
"start game with baz@example.com",
|
||||
"You've sent an invitation to play foo test game with @**baz**",
|
||||
1,
|
||||
bot=bot,
|
||||
)
|
||||
self.assertEqual(len(bot.invites), 1)
|
||||
|
||||
def test_join_game_and_start_in_stream(self) -> None:
|
||||
bot = self.add_user_to_cache('baz')
|
||||
self.add_user_to_cache('foo', bot)
|
||||
bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}}
|
||||
bot = self.add_user_to_cache("baz")
|
||||
self.add_user_to_cache("foo", bot)
|
||||
bot.invites = {"abc": {"stream": "test", "subject": "test game", "host": "foo@example.com"}}
|
||||
self.verify_response(
|
||||
'join',
|
||||
'@**baz** has joined the game',
|
||||
"join",
|
||||
"@**baz** has joined the game",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
user_name='baz',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
user_name="baz",
|
||||
)
|
||||
self.assertEqual(len(bot.instances.keys()), 1)
|
||||
|
||||
def test_start_game_in_stream(self) -> None:
|
||||
self.verify_response(
|
||||
'start game',
|
||||
'**foo** wants to play **foo test game**. Type @**test-bot** join to play them!',
|
||||
"start game",
|
||||
"**foo** wants to play **foo test game**. Type @**test-bot** join to play them!",
|
||||
0,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
|
||||
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(
|
||||
'start game with @**baz**',
|
||||
"start game with @**baz**",
|
||||
'If you were invited, and you\'re here, type "@**test-bot** accept" to accept the invite!',
|
||||
2,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='game test',
|
||||
stream="test",
|
||||
subject="game test",
|
||||
)
|
||||
|
||||
def test_join_no_game(self) -> None:
|
||||
self.verify_response(
|
||||
'join',
|
||||
'There is not a game in this subject. Type `help` for all commands.',
|
||||
"join",
|
||||
"There is not a game in this subject. Type `help` for all commands.",
|
||||
0,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
user_name='baz',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
user_name="baz",
|
||||
max_messages=1,
|
||||
)
|
||||
|
||||
def test_accept_invitation(self) -> None:
|
||||
bot = self.add_user_to_cache('baz')
|
||||
self.add_user_to_cache('foo', bot)
|
||||
bot = self.add_user_to_cache("baz")
|
||||
self.add_user_to_cache("foo", bot)
|
||||
bot.invites = {
|
||||
'abc': {
|
||||
'subject': '###private###',
|
||||
'stream': 'games',
|
||||
'host': 'foo@example.com',
|
||||
'baz@example.com': 'p',
|
||||
"abc": {
|
||||
"subject": "###private###",
|
||||
"stream": "games",
|
||||
"host": "foo@example.com",
|
||||
"baz@example.com": "p",
|
||||
}
|
||||
}
|
||||
self.verify_response(
|
||||
'accept', 'Accepted invitation to play **foo test game** from @**foo**.', 0, bot, 'baz'
|
||||
"accept", "Accepted invitation to play **foo test game** from @**foo**.", 0, bot, "baz"
|
||||
)
|
||||
|
||||
def test_decline_invitation(self) -> None:
|
||||
bot = self.add_user_to_cache('baz')
|
||||
self.add_user_to_cache('foo', bot)
|
||||
bot = self.add_user_to_cache("baz")
|
||||
self.add_user_to_cache("foo", bot)
|
||||
bot.invites = {
|
||||
'abc': {'subject': '###private###', 'host': 'foo@example.com', 'baz@example.com': 'p'}
|
||||
"abc": {"subject": "###private###", "host": "foo@example.com", "baz@example.com": "p"}
|
||||
}
|
||||
self.verify_response(
|
||||
'decline', 'Declined invitation to play **foo test game** from @**foo**.', 0, bot, 'baz'
|
||||
"decline", "Declined invitation to play **foo test game** from @**foo**.", 0, bot, "baz"
|
||||
)
|
||||
|
||||
def test_quit_invite(self) -> None:
|
||||
bot = self.add_user_to_cache('foo')
|
||||
bot.invites = {'abc': {'subject': '###private###', 'host': 'foo@example.com'}}
|
||||
self.verify_response('quit', 'Game cancelled.\n**foo** quit.', 0, bot, 'foo')
|
||||
bot = self.add_user_to_cache("foo")
|
||||
bot.invites = {"abc": {"subject": "###private###", "host": "foo@example.com"}}
|
||||
self.verify_response("quit", "Game cancelled.\n**foo** quit.", 0, bot, "foo")
|
||||
|
||||
def test_user_already_in_game_errors(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'start game with @**baz**',
|
||||
'You are already in a game. Type `quit` to leave.',
|
||||
"start game with @**baz**",
|
||||
"You are already in a game. Type `quit` to leave.",
|
||||
0,
|
||||
bot=bot,
|
||||
max_messages=1,
|
||||
)
|
||||
self.verify_response(
|
||||
'start game',
|
||||
'You are already in a game. Type `quit` to leave.',
|
||||
"start game",
|
||||
"You are already in a game. Type `quit` to leave.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
stream="test",
|
||||
max_messages=1,
|
||||
)
|
||||
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(
|
||||
'decline',
|
||||
'You are already in a game. Type `quit` to leave.',
|
||||
"decline",
|
||||
"You are already in a game. Type `quit` to leave.",
|
||||
0,
|
||||
bot=bot,
|
||||
max_messages=1,
|
||||
)
|
||||
self.verify_response(
|
||||
'join', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1
|
||||
"join", "You are already in a game. Type `quit` to leave.", 0, bot=bot, max_messages=1
|
||||
)
|
||||
|
||||
def test_register_command(self) -> None:
|
||||
bot = self.add_user_to_cache('foo')
|
||||
self.verify_response('register', 'Hello @**foo**. Thanks for registering!', 0, bot, 'foo')
|
||||
self.assertIn('foo@example.com', bot.user_cache.keys())
|
||||
bot = self.add_user_to_cache("foo")
|
||||
self.verify_response("register", "Hello @**foo**. Thanks for registering!", 0, bot, "foo")
|
||||
self.assertIn("foo@example.com", bot.user_cache.keys())
|
||||
|
||||
def test_no_active_invite_errors(self) -> None:
|
||||
self.verify_response('accept', 'No active invites. Type `help` for commands.', 0)
|
||||
self.verify_response('decline', 'No active invites. Type `help` for commands.', 0)
|
||||
self.verify_response("accept", "No active invites. Type `help` for commands.", 0)
|
||||
self.verify_response("decline", "No active invites. Type `help` for commands.", 0)
|
||||
|
||||
def test_wrong_number_of_players_message(self) -> None:
|
||||
bot = self.add_user_to_cache('baz')
|
||||
bot = self.add_user_to_cache("baz")
|
||||
bot.min_players = 5
|
||||
self.verify_response(
|
||||
'start game with @**baz**',
|
||||
'You must have at least 5 players to play.\nGame cancelled.',
|
||||
"start game with @**baz**",
|
||||
"You must have at least 5 players to play.\nGame cancelled.",
|
||||
0,
|
||||
bot=bot,
|
||||
)
|
||||
bot.min_players = 2
|
||||
bot.max_players = 1
|
||||
self.verify_response(
|
||||
'start game with @**baz**',
|
||||
'The maximum number of players for this game is 1.',
|
||||
"start game with @**baz**",
|
||||
"The maximum number of players for this game is 1.",
|
||||
0,
|
||||
bot=bot,
|
||||
)
|
||||
bot.max_players = 1
|
||||
bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}}
|
||||
bot.invites = {"abc": {"stream": "test", "subject": "test game", "host": "foo@example.com"}}
|
||||
self.verify_response(
|
||||
'join',
|
||||
'This game is full.',
|
||||
"join",
|
||||
"This game is full.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
user_name='baz',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
user_name="baz",
|
||||
)
|
||||
|
||||
def test_public_accept(self) -> None:
|
||||
bot = self.add_user_to_cache('baz')
|
||||
self.add_user_to_cache('foo', bot)
|
||||
bot = self.add_user_to_cache("baz")
|
||||
self.add_user_to_cache("foo", bot)
|
||||
bot.invites = {
|
||||
'abc': {
|
||||
'stream': 'test',
|
||||
'subject': 'test game',
|
||||
'host': 'baz@example.com',
|
||||
'foo@example.com': 'p',
|
||||
"abc": {
|
||||
"stream": "test",
|
||||
"subject": "test game",
|
||||
"host": "baz@example.com",
|
||||
"foo@example.com": "p",
|
||||
}
|
||||
}
|
||||
self.verify_response(
|
||||
'accept',
|
||||
'@**foo** has accepted the invitation.',
|
||||
"accept",
|
||||
"@**foo** has accepted the invitation.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
|
||||
def test_start_game_with_computer(self) -> None:
|
||||
self.verify_response(
|
||||
'start game with @**test-bot**',
|
||||
'Wait... That\'s me!',
|
||||
"start game with @**test-bot**",
|
||||
"Wait... That's me!",
|
||||
4,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
|
||||
def test_sent_by_bot(self) -> None:
|
||||
with self.assertRaises(IndexError):
|
||||
self.verify_response(
|
||||
'foo', '', 0, user_name='test-bot', stream='test', subject='test game'
|
||||
"foo", "", 0, user_name="test-bot", stream="test", subject="test game"
|
||||
)
|
||||
|
||||
def test_forfeit(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'forfeit', '**foo** forfeited!', 0, bot=bot, stream='test', subject='test game'
|
||||
"forfeit", "**foo** forfeited!", 0, bot=bot, stream="test", subject="test game"
|
||||
)
|
||||
|
||||
def test_draw(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'draw',
|
||||
'**foo** has voted for a draw!\nType `draw` to accept',
|
||||
"draw",
|
||||
"**foo** has voted for a draw!\nType `draw` to accept",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
self.verify_response(
|
||||
'draw',
|
||||
'It was a draw!',
|
||||
"draw",
|
||||
"It was a draw!",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
user_name='baz',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
user_name="baz",
|
||||
)
|
||||
|
||||
def test_normal_turns(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'**foo** moved in column 3\n\nfoo\n\nIt\'s **baz**\'s (:red_circle:) turn.',
|
||||
"move 3",
|
||||
"**foo** moved in column 3\n\nfoo\n\nIt's **baz**'s (:red_circle:) turn.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'**baz** moved in column 3\n\nfoo\n\nIt\'s **foo**\'s (:blue_circle:) turn.',
|
||||
"move 3",
|
||||
"**baz** moved in column 3\n\nfoo\n\nIt's **foo**'s (:blue_circle:) turn.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
user_name='baz',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
user_name="baz",
|
||||
)
|
||||
|
||||
def test_wrong_turn(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'move 5',
|
||||
'It\'s **foo**\'s (:blue_circle:) turn.',
|
||||
"move 5",
|
||||
"It's **foo**'s (:blue_circle:) turn.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
user_name='baz',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
user_name="baz",
|
||||
)
|
||||
|
||||
def test_private_message_error(self) -> None:
|
||||
self.verify_response(
|
||||
'start game',
|
||||
'If you are starting a game in private messages, you must invite players. Type `help` for commands.',
|
||||
"start game",
|
||||
"If you are starting a game in private messages, you must invite players. Type `help` for commands.",
|
||||
0,
|
||||
max_messages=1,
|
||||
)
|
||||
bot = self.add_user_to_cache('bar')
|
||||
bot = self.add_user_to_cache("bar")
|
||||
bot.invites = {
|
||||
'abcdefg': {'host': 'bar@example.com', 'stream': 'test', 'subject': 'test game'}
|
||||
"abcdefg": {"host": "bar@example.com", "stream": "test", "subject": "test game"}
|
||||
}
|
||||
self.verify_response(
|
||||
'join',
|
||||
'You cannot join games in private messages. Type `help` for all commands.',
|
||||
"join",
|
||||
"You cannot join games in private messages. Type `help` for all commands.",
|
||||
0,
|
||||
bot=bot,
|
||||
max_messages=1,
|
||||
)
|
||||
|
||||
def test_game_already_in_subject(self) -> None:
|
||||
bot = self.add_user_to_cache('foo')
|
||||
bot = self.add_user_to_cache("foo")
|
||||
bot.invites = {
|
||||
'abcdefg': {'host': 'foo@example.com', 'stream': 'test', 'subject': 'test game'}
|
||||
"abcdefg": {"host": "foo@example.com", "stream": "test", "subject": "test game"}
|
||||
}
|
||||
self.verify_response(
|
||||
'start game',
|
||||
'There is already a game in this stream.',
|
||||
"start game",
|
||||
"There is already a game in this stream.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
user_name='baz',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
user_name="baz",
|
||||
max_messages=1,
|
||||
)
|
||||
|
||||
|
@ -452,219 +452,219 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
|
|||
|
||||
def test_unknown_user(self) -> None:
|
||||
self.verify_response(
|
||||
'start game with @**bar**',
|
||||
'I don\'t know @**bar**. Tell them to say @**test-bot** register',
|
||||
"start game with @**bar**",
|
||||
"I don't know @**bar**. Tell them to say @**test-bot** register",
|
||||
0,
|
||||
)
|
||||
self.verify_response(
|
||||
'start game with bar@example.com',
|
||||
'I don\'t know bar@example.com. Tell them to use @**test-bot** register',
|
||||
"start game with bar@example.com",
|
||||
"I don't know bar@example.com. Tell them to use @**test-bot** register",
|
||||
0,
|
||||
)
|
||||
|
||||
def test_is_user_not_player(self) -> None:
|
||||
bot = self.add_user_to_cache('foo')
|
||||
self.add_user_to_cache('baz', bot)
|
||||
bot.invites = {'abcdefg': {'host': 'foo@example.com', 'baz@example.com': 'a'}}
|
||||
self.assertFalse(bot.is_user_not_player('foo@example.com'))
|
||||
self.assertFalse(bot.is_user_not_player('baz@example.com'))
|
||||
bot = self.add_user_to_cache("foo")
|
||||
self.add_user_to_cache("baz", bot)
|
||||
bot.invites = {"abcdefg": {"host": "foo@example.com", "baz@example.com": "a"}}
|
||||
self.assertFalse(bot.is_user_not_player("foo@example.com"))
|
||||
self.assertFalse(bot.is_user_not_player("baz@example.com"))
|
||||
|
||||
def test_move_help_message(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'move 123',
|
||||
'* To make your move during a game, type\n```move <column-number>```',
|
||||
"move 123",
|
||||
"* To make your move during a game, type\n```move <column-number>```",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
|
||||
def test_invalid_move_message(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'move 9',
|
||||
'Invalid Move.',
|
||||
"move 9",
|
||||
"Invalid Move.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
max_messages=2,
|
||||
)
|
||||
|
||||
def test_get_game_id_by_email(self) -> None:
|
||||
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:
|
||||
bot = self.setup_game()
|
||||
bot.put_user_cache()
|
||||
with patch(
|
||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
||||
return_value='foo@example.com',
|
||||
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||
return_value="foo@example.com",
|
||||
):
|
||||
self.verify_response(
|
||||
'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game'
|
||||
"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\
|
||||
--- | --- | --- | --- | --- \n\
|
||||
**foo** | 1 | 0 | 0 | 1\n\
|
||||
**baz** | 0 | 0 | 1 | 1\n\
|
||||
**test-bot** | 0 | 0 | 0 | 0'
|
||||
self.verify_response('leaderboard', leaderboard, 0, bot=bot)
|
||||
**test-bot** | 0 | 0 | 0 | 0"
|
||||
self.verify_response("leaderboard", leaderboard, 0, bot=bot)
|
||||
|
||||
def test_current_turn_winner(self) -> None:
|
||||
bot = self.setup_game()
|
||||
with patch(
|
||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
||||
return_value='current turn',
|
||||
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||
return_value="current turn",
|
||||
):
|
||||
self.verify_response(
|
||||
'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game'
|
||||
"move 3", "**foo** won! :tada:", 1, bot=bot, stream="test", subject="test game"
|
||||
)
|
||||
|
||||
def test_computer_turn(self) -> None:
|
||||
bot = self.setup_computer_game()
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'**foo** moved in column 3\n\nfoo\n\nIt\'s **test-bot**\'s (:red_circle:) turn.',
|
||||
"move 3",
|
||||
"**foo** moved in column 3\n\nfoo\n\nIt's **test-bot**'s (:red_circle:) turn.",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
with patch(
|
||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
||||
return_value='test-bot@example.com',
|
||||
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||
return_value="test-bot@example.com",
|
||||
):
|
||||
self.verify_response(
|
||||
'move 5', 'I won! Well Played!', 2, bot=bot, stream='test', subject='test game'
|
||||
"move 5", "I won! Well Played!", 2, bot=bot, stream="test", subject="test game"
|
||||
)
|
||||
|
||||
def test_computer_endgame_responses(self) -> None:
|
||||
bot = self.setup_computer_game()
|
||||
with patch(
|
||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
||||
return_value='foo@example.com',
|
||||
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||
return_value="foo@example.com",
|
||||
):
|
||||
self.verify_response(
|
||||
'move 5', 'You won! Nice!', 2, bot=bot, stream='test', subject='test game'
|
||||
"move 5", "You won! Nice!", 2, bot=bot, stream="test", subject="test game"
|
||||
)
|
||||
bot = self.setup_computer_game()
|
||||
with patch(
|
||||
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
|
||||
return_value='draw',
|
||||
"zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over",
|
||||
return_value="draw",
|
||||
):
|
||||
self.verify_response(
|
||||
'move 5',
|
||||
'It was a draw! Well Played!',
|
||||
"move 5",
|
||||
"It was a draw! Well Played!",
|
||||
2,
|
||||
bot=bot,
|
||||
stream='test',
|
||||
subject='test game',
|
||||
stream="test",
|
||||
subject="test game",
|
||||
)
|
||||
|
||||
def test_add_user_statistics(self) -> None:
|
||||
bot = self.add_user_to_cache('foo')
|
||||
bot.add_user_statistics('foo@example.com', {'foo': 3})
|
||||
self.assertEqual(bot.user_cache['foo@example.com']['stats']['foo'], 3)
|
||||
bot = self.add_user_to_cache("foo")
|
||||
bot.add_user_statistics("foo@example.com", {"foo": 3})
|
||||
self.assertEqual(bot.user_cache["foo@example.com"]["stats"]["foo"], 3)
|
||||
|
||||
def test_get_players(self) -> None:
|
||||
bot = self.setup_game()
|
||||
players = bot.get_players('abc123')
|
||||
self.assertEqual(players, ['foo@example.com', 'baz@example.com'])
|
||||
players = bot.get_players("abc123")
|
||||
self.assertEqual(players, ["foo@example.com", "baz@example.com"])
|
||||
|
||||
def test_none_function_responses(self) -> None:
|
||||
bot, bot_handler = self._get_handlers()
|
||||
self.assertEqual(bot.get_players('abc'), [])
|
||||
self.assertEqual(bot.get_user_by_name('no one'), {})
|
||||
self.assertEqual(bot.get_user_by_email('no one'), {})
|
||||
self.assertEqual(bot.get_players("abc"), [])
|
||||
self.assertEqual(bot.get_user_by_name("no one"), {})
|
||||
self.assertEqual(bot.get_user_by_email("no one"), {})
|
||||
|
||||
def test_get_game_info(self) -> None:
|
||||
bot = self.add_user_to_cache('foo')
|
||||
self.add_user_to_cache('baz', bot)
|
||||
bot = self.add_user_to_cache("foo")
|
||||
self.add_user_to_cache("baz", bot)
|
||||
bot.invites = {
|
||||
'abcdefg': {
|
||||
'host': 'foo@example.com',
|
||||
'baz@example.com': 'a',
|
||||
'stream': 'test',
|
||||
'subject': 'test game',
|
||||
"abcdefg": {
|
||||
"host": "foo@example.com",
|
||||
"baz@example.com": "a",
|
||||
"stream": "test",
|
||||
"subject": "test game",
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
bot.get_game_info('abcdefg'),
|
||||
bot.get_game_info("abcdefg"),
|
||||
{
|
||||
'game_id': 'abcdefg',
|
||||
'type': 'invite',
|
||||
'stream': 'test',
|
||||
'subject': 'test game',
|
||||
'players': ['foo@example.com', 'baz@example.com'],
|
||||
"game_id": "abcdefg",
|
||||
"type": "invite",
|
||||
"stream": "test",
|
||||
"subject": "test game",
|
||||
"players": ["foo@example.com", "baz@example.com"],
|
||||
},
|
||||
)
|
||||
|
||||
def test_parse_message(self) -> None:
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'Join your game using the link below!\n\n> **Game `abc123`**\n\
|
||||
"move 3",
|
||||
"Join your game using the link below!\n\n> **Game `abc123`**\n\
|
||||
> foo test game\n\
|
||||
> 2/2 players\n\
|
||||
> **[Join Game](/#narrow/stream/test/topic/test game)**',
|
||||
> **[Join Game](/#narrow/stream/test/topic/test game)**",
|
||||
0,
|
||||
bot=bot,
|
||||
)
|
||||
bot = self.setup_game()
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'''Your current game is not in this subject. \n\
|
||||
"move 3",
|
||||
"""Your current game is not in this subject. \n\
|
||||
To move subjects, send your message again, otherwise join the game using the link below.
|
||||
|
||||
> **Game `abc123`**
|
||||
> foo test game
|
||||
> 2/2 players
|
||||
> **[Join Game](/#narrow/stream/test/topic/test game)**''',
|
||||
> **[Join Game](/#narrow/stream/test/topic/test game)**""",
|
||||
0,
|
||||
bot=bot,
|
||||
stream='test 2',
|
||||
subject='game 2',
|
||||
stream="test 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:
|
||||
bot = self.setup_game('abc123')
|
||||
self.setup_game('abcdefg', bot, ['bar', 'abc'], 'test game 2', 'test2')
|
||||
bot = self.setup_game("abc123")
|
||||
self.setup_game("abcdefg", bot, ["bar", "abc"], "test game 2", "test2")
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'''Your current game is not in this subject. \n\
|
||||
"move 3",
|
||||
"""Your current game is not in this subject. \n\
|
||||
To move subjects, send your message again, otherwise join the game using the link below.
|
||||
|
||||
> **Game `abcdefg`**
|
||||
> foo test game
|
||||
> 2/2 players
|
||||
> **[Join Game](/#narrow/stream/test2/topic/test game 2)**''',
|
||||
> **[Join Game](/#narrow/stream/test2/topic/test game 2)**""",
|
||||
0,
|
||||
bot=bot,
|
||||
user_name='bar',
|
||||
stream='test game',
|
||||
subject='test2',
|
||||
user_name="bar",
|
||||
stream="test game",
|
||||
subject="test2",
|
||||
)
|
||||
self.verify_response(
|
||||
'move 3',
|
||||
'There is already a game in this subject.',
|
||||
"move 3",
|
||||
"There is already a game in this subject.",
|
||||
0,
|
||||
bot=bot,
|
||||
user_name='bar',
|
||||
stream='test game',
|
||||
subject='test',
|
||||
user_name="bar",
|
||||
stream="test game",
|
||||
subject="test",
|
||||
)
|
||||
bot.invites = {
|
||||
'foo bar baz': {
|
||||
'host': 'foo@example.com',
|
||||
'baz@example.com': 'a',
|
||||
'stream': 'test',
|
||||
'subject': 'test game',
|
||||
"foo bar baz": {
|
||||
"host": "foo@example.com",
|
||||
"baz@example.com": "a",
|
||||
"stream": "test",
|
||||
"subject": "test game",
|
||||
}
|
||||
}
|
||||
bot.change_game_subject('foo bar baz', 'test2', 'game2', self.make_request_message('foo'))
|
||||
self.assertEqual(bot.invites['foo bar baz']['stream'], 'test2')
|
||||
bot.change_game_subject("foo bar baz", "test2", "game2", self.make_request_message("foo"))
|
||||
self.assertEqual(bot.invites["foo bar baz"]["stream"], "test2")
|
||||
|
|
|
@ -31,8 +31,8 @@ class GameOfFifteenModel:
|
|||
|
||||
def determine_game_over(self, players: List[str]) -> str:
|
||||
if self.won(self.current_board):
|
||||
return 'current turn'
|
||||
return ''
|
||||
return "current turn"
|
||||
return ""
|
||||
|
||||
def won(self, board: Any) -> bool:
|
||||
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:
|
||||
board = self.current_board
|
||||
move = move.strip()
|
||||
move = move.split(' ')
|
||||
move = move.split(" ")
|
||||
|
||||
if '' in move:
|
||||
raise BadMoveException('You should enter space separated digits.')
|
||||
if "" in move:
|
||||
raise BadMoveException("You should enter space separated digits.")
|
||||
moves = len(move)
|
||||
for m in range(1, moves):
|
||||
tile = int(move[m])
|
||||
coordinates = self.get_coordinates(board)
|
||||
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]
|
||||
if (j - 1) > -1 and board[i][j - 1] == 0:
|
||||
board[i][j - 1] = tile
|
||||
|
@ -77,7 +77,7 @@ class GameOfFifteenModel:
|
|||
board[i][j] = 0
|
||||
else:
|
||||
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:
|
||||
return board
|
||||
|
@ -86,30 +86,30 @@ class GameOfFifteenModel:
|
|||
class GameOfFifteenMessageHandler:
|
||||
|
||||
tiles = {
|
||||
'0': ':grey_question:',
|
||||
'1': ':one:',
|
||||
'2': ':two:',
|
||||
'3': ':three:',
|
||||
'4': ':four:',
|
||||
'5': ':five:',
|
||||
'6': ':six:',
|
||||
'7': ':seven:',
|
||||
'8': ':eight:',
|
||||
"0": ":grey_question:",
|
||||
"1": ":one:",
|
||||
"2": ":two:",
|
||||
"3": ":three:",
|
||||
"4": ":four:",
|
||||
"5": ":five:",
|
||||
"6": ":six:",
|
||||
"7": ":seven:",
|
||||
"8": ":eight:",
|
||||
}
|
||||
|
||||
def parse_board(self, board: Any) -> str:
|
||||
# Header for the top of the board
|
||||
board_str = ''
|
||||
board_str = ""
|
||||
|
||||
for row in range(3):
|
||||
board_str += '\n\n'
|
||||
board_str += "\n\n"
|
||||
for column in range(3):
|
||||
board_str += self.tiles[str(board[row][column])]
|
||||
return board_str
|
||||
|
||||
def alert_move_message(self, original_player: str, move_info: str) -> str:
|
||||
tile = move_info.replace('move ', '')
|
||||
return original_player + ' moved ' + tile
|
||||
tile = move_info.replace("move ", "")
|
||||
return original_player + " moved " + tile
|
||||
|
||||
def game_start_message(self) -> str:
|
||||
return (
|
||||
|
@ -119,23 +119,23 @@ class GameOfFifteenMessageHandler:
|
|||
|
||||
|
||||
class GameOfFifteenBotHandler(GameAdapter):
|
||||
'''
|
||||
"""
|
||||
Bot that uses the Game Adapter class
|
||||
to allow users to play Game of Fifteen
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
game_name = 'Game of Fifteen'
|
||||
bot_name = 'Game of Fifteen'
|
||||
game_name = "Game of Fifteen"
|
||||
bot_name = "Game of Fifteen"
|
||||
move_help_message = (
|
||||
'* To make your move during a game, type\n```move <tile1> <tile2> ...```'
|
||||
"* 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
|
||||
gameMessageHandler = GameOfFifteenMessageHandler
|
||||
rules = '''Arrange the board’s tiles from smallest to largest, left to right,
|
||||
rules = """Arrange the board’s tiles from smallest to largest, left to right,
|
||||
top to bottom, and tiles adjacent to :grey_question: can only be moved.
|
||||
Final configuration will have :grey_question: in top left.'''
|
||||
Final configuration will have :grey_question: in top left."""
|
||||
|
||||
super().__init__(
|
||||
game_name,
|
||||
|
|
|
@ -6,10 +6,10 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
|
|||
|
||||
|
||||
class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
||||
bot_name = 'game_of_fifteen'
|
||||
bot_name = "game_of_fifteen"
|
||||
|
||||
def make_request_message(
|
||||
self, content: str, user: str = 'foo@example.com', user_name: str = 'foo'
|
||||
self, content: str, user: str = "foo@example.com", user_name: str = "foo"
|
||||
) -> Dict[str, str]:
|
||||
message = dict(sender_email=user, content=content, sender_full_name=user_name)
|
||||
return message
|
||||
|
@ -20,14 +20,14 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
|||
request: str,
|
||||
expected_response: str,
|
||||
response_number: int,
|
||||
user: str = 'foo@example.com',
|
||||
user: str = "foo@example.com",
|
||||
) -> None:
|
||||
'''
|
||||
"""
|
||||
This function serves a similar purpose
|
||||
to BotTestCase.verify_dialog, but allows
|
||||
for multiple responses to be validated,
|
||||
and for mocking of the bot's internal data
|
||||
'''
|
||||
"""
|
||||
|
||||
bot, bot_handler = self._get_handlers()
|
||||
message = self.make_request_message(request, user)
|
||||
|
@ -38,10 +38,10 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
|||
responses = [message for (method, message) in bot_handler.transcript]
|
||||
|
||||
first_response = responses[response_number]
|
||||
self.assertEqual(expected_response, first_response['content'])
|
||||
self.assertEqual(expected_response, first_response["content"])
|
||||
|
||||
def help_message(self) -> str:
|
||||
return '''** Game of Fifteen Bot Help:**
|
||||
return """** Game of Fifteen Bot Help:**
|
||||
*Preface all commands with @**test-bot***
|
||||
* To start a game in a stream, type
|
||||
`start game`
|
||||
|
@ -50,16 +50,16 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
|||
* To see rules of this game, type
|
||||
`rules`
|
||||
* To make your move during a game, type
|
||||
```move <tile1> <tile2> ...```'''
|
||||
```move <tile1> <tile2> ...```"""
|
||||
|
||||
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:
|
||||
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()
|
||||
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(
|
||||
bot.gameMessageHandler.game_start_message(),
|
||||
"Welcome to Game of Fifteen!"
|
||||
|
@ -86,13 +86,13 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
|||
final_board: List[List[int]],
|
||||
) -> None:
|
||||
gameOfFifteenModel.update_board(initial_board)
|
||||
test_board = gameOfFifteenModel.make_move('move ' + tile, token_number)
|
||||
test_board = gameOfFifteenModel.make_move("move " + tile, token_number)
|
||||
|
||||
self.assertEqual(test_board, final_board)
|
||||
|
||||
def confirmGameOver(board: List[List[int]], result: str) -> None:
|
||||
gameOfFifteenModel.update_board(board)
|
||||
game_over = gameOfFifteenModel.determine_game_over(['first_player'])
|
||||
game_over = gameOfFifteenModel.determine_game_over(["first_player"])
|
||||
|
||||
self.assertEqual(game_over, result)
|
||||
|
||||
|
@ -114,17 +114,17 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
|||
confirmAvailableMoves([1, 2, 3, 4, 5, 6, 7, 8], [0, 9, -1], initial_board)
|
||||
|
||||
# Test Move Logic
|
||||
confirmMove('1', 0, initial_board, [[8, 7, 6], [5, 4, 3], [2, 0, 1]])
|
||||
confirmMove("1", 0, initial_board, [[8, 7, 6], [5, 4, 3], [2, 0, 1]])
|
||||
|
||||
confirmMove('1 2', 0, initial_board, [[8, 7, 6], [5, 4, 3], [0, 2, 1]])
|
||||
confirmMove("1 2", 0, initial_board, [[8, 7, 6], [5, 4, 3], [0, 2, 1]])
|
||||
|
||||
confirmMove('1 2 5', 0, initial_board, [[8, 7, 6], [0, 4, 3], [5, 2, 1]])
|
||||
confirmMove("1 2 5", 0, initial_board, [[8, 7, 6], [0, 4, 3], [5, 2, 1]])
|
||||
|
||||
confirmMove('1 2 5 4', 0, initial_board, [[8, 7, 6], [4, 0, 3], [5, 2, 1]])
|
||||
confirmMove("1 2 5 4", 0, initial_board, [[8, 7, 6], [4, 0, 3], [5, 2, 1]])
|
||||
|
||||
confirmMove('3', 0, sample_board, [[7, 6, 8], [0, 3, 1], [2, 4, 5]])
|
||||
confirmMove("3", 0, sample_board, [[7, 6, 8], [0, 3, 1], [2, 4, 5]])
|
||||
|
||||
confirmMove('3 7', 0, sample_board, [[0, 6, 8], [7, 3, 1], [2, 4, 5]])
|
||||
confirmMove("3 7", 0, sample_board, [[0, 6, 8], [7, 3, 1], [2, 4, 5]])
|
||||
|
||||
# Test coordinates logic:
|
||||
confirm_coordinates(
|
||||
|
@ -143,16 +143,16 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
|
|||
)
|
||||
|
||||
# Test Game Over Logic:
|
||||
confirmGameOver(winning_board, 'current turn')
|
||||
confirmGameOver(sample_board, '')
|
||||
confirmGameOver(winning_board, "current turn")
|
||||
confirmGameOver(sample_board, "")
|
||||
|
||||
def test_invalid_moves(self) -> None:
|
||||
model = GameOfFifteenModel()
|
||||
move1 = 'move 2'
|
||||
move2 = 'move 5'
|
||||
move3 = 'move 23'
|
||||
move4 = 'move 0'
|
||||
move5 = 'move 1 2'
|
||||
move1 = "move 2"
|
||||
move2 = "move 5"
|
||||
move3 = "move 23"
|
||||
move4 = "move 0"
|
||||
move5 = "move 1 2"
|
||||
initial_board = [[8, 7, 6], [5, 4, 3], [2, 1, 0]]
|
||||
|
||||
model.update_board(initial_board)
|
||||
|
|
|
@ -7,8 +7,8 @@ from requests.exceptions import ConnectionError, HTTPError
|
|||
from zulip_bots.custom_exceptions import ConfigValidationError
|
||||
from zulip_bots.lib import BotHandler
|
||||
|
||||
GIPHY_TRANSLATE_API = 'http://api.giphy.com/v1/gifs/translate'
|
||||
GIPHY_RANDOM_API = 'http://api.giphy.com/v1/gifs/random'
|
||||
GIPHY_TRANSLATE_API = "http://api.giphy.com/v1/gifs/translate"
|
||||
GIPHY_RANDOM_API = "http://api.giphy.com/v1/gifs/random"
|
||||
|
||||
|
||||
class GiphyHandler:
|
||||
|
@ -21,15 +21,15 @@ class GiphyHandler:
|
|||
"""
|
||||
|
||||
def usage(self) -> str:
|
||||
return '''
|
||||
return """
|
||||
This plugin allows users to post GIFs provided by Giphy.
|
||||
Users should preface keywords with the Giphy-bot @mention.
|
||||
The bot responds also to private messages.
|
||||
'''
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def validate_config(config_info: Dict[str, str]) -> None:
|
||||
query = {'s': 'Hello', 'api_key': config_info['key']}
|
||||
query = {"s": "Hello", "api_key": config_info["key"]}
|
||||
try:
|
||||
data = requests.get(GIPHY_TRANSLATE_API, params=query)
|
||||
data.raise_for_status()
|
||||
|
@ -39,13 +39,13 @@ class GiphyHandler:
|
|||
error_message = str(e)
|
||||
if data.status_code == 403:
|
||||
error_message += (
|
||||
'This is likely due to an invalid key.\n'
|
||||
'Follow the instructions in doc.md for setting an API key.'
|
||||
"This is likely due to an invalid key.\n"
|
||||
"Follow the instructions in doc.md for setting an API key."
|
||||
)
|
||||
raise ConfigValidationError(error_message)
|
||||
|
||||
def initialize(self, bot_handler: BotHandler) -> None:
|
||||
self.config_info = bot_handler.get_config_info('giphy')
|
||||
self.config_info = bot_handler.get_config_info("giphy")
|
||||
|
||||
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
|
||||
bot_response = get_bot_giphy_response(message, bot_handler, self.config_info)
|
||||
|
@ -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.
|
||||
# In case of error, e.g. failure to fetch a GIF URL, it will
|
||||
# return a number.
|
||||
query = {'api_key': api_key}
|
||||
query = {"api_key": api_key}
|
||||
if len(keyword) > 0:
|
||||
query['s'] = keyword
|
||||
query["s"] = keyword
|
||||
url = GIPHY_TRANSLATE_API
|
||||
else:
|
||||
url = GIPHY_RANDOM_API
|
||||
|
@ -70,12 +70,12 @@ def get_url_gif_giphy(keyword: str, api_key: str) -> Union[int, str]:
|
|||
try:
|
||||
data = requests.get(url, params=query)
|
||||
except requests.exceptions.ConnectionError: # Usually triggered by bad connection.
|
||||
logging.exception('Bad connection')
|
||||
logging.exception("Bad connection")
|
||||
raise
|
||||
data.raise_for_status()
|
||||
|
||||
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.
|
||||
raise GiphyNoResultException()
|
||||
return gif_url
|
||||
|
@ -86,20 +86,20 @@ def get_bot_giphy_response(
|
|||
) -> str:
|
||||
# Each exception has a specific reply should "gif_url" return a number.
|
||||
# The bot will post the appropriate message for the error.
|
||||
keyword = message['content']
|
||||
keyword = message["content"]
|
||||
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:
|
||||
return (
|
||||
'Uh oh, sorry :slightly_frowning_face:, I '
|
||||
'cannot process your request right now. But, '
|
||||
'let\'s try again later! :grin:'
|
||||
"Uh oh, sorry :slightly_frowning_face:, I "
|
||||
"cannot process your request right now. But, "
|
||||
"let's try again later! :grin:"
|
||||
)
|
||||
except GiphyNoResultException:
|
||||
return 'Sorry, I don\'t have a GIF for "%s"! ' ':astonished:' % (keyword,)
|
||||
return 'Sorry, I don\'t have a GIF for "%s"! ' ":astonished:" % (keyword,)
|
||||
return (
|
||||
'[Click to enlarge](%s)'
|
||||
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)' % (gif_url,)
|
||||
"[Click to enlarge](%s)"
|
||||
"[](/static/images/interactive-bot/giphy/powered-by-giphy.png)" % (gif_url,)
|
||||
)
|
||||
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue