diff --git a/.gitignore b/.gitignore index 910ff9029..f88e6cf14 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ botserverrc # Pycharm \.DS_Store \.idea/ + +# VS Code +.vscode/settings.json + diff --git a/zulip_bots/zulip_bots/bots/bugzilla/__init__.py b/zulip_bots/zulip_bots/bots/bugzilla/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zulip_bots/zulip_bots/bots/bugzilla/bugzilla.conf b/zulip_bots/zulip_bots/bots/bugzilla/bugzilla.conf new file mode 100644 index 000000000..0dff8efcb --- /dev/null +++ b/zulip_bots/zulip_bots/bots/bugzilla/bugzilla.conf @@ -0,0 +1,3 @@ +[bugzilla] +site = https://bugs.site.net +api_key = xxxx diff --git a/zulip_bots/zulip_bots/bots/bugzilla/bugzilla.py b/zulip_bots/zulip_bots/bots/bugzilla/bugzilla.py new file mode 100644 index 000000000..b6856a452 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/bugzilla/bugzilla.py @@ -0,0 +1,92 @@ +import re +import requests +from typing import Any, Dict + +TOPIC_REGEX = re.compile('^Bug (?P.+)$') + +HELP_REGEX = re.compile('help$') + +HELP_RESPONSE = ''' +**help** + +`help` returns this short help + +**comment** + +With no argument, by default, a new comment is added to the bug that is associated to the topic. +For example, on topic Bug 123, + +you: + + > @**Bugzilla** A new comment + +Then `A new comment` is added to bug 123 +''' + + +class BugzillaHandler(object): + ''' + A docstring documenting this bot. + ''' + + def usage(self): + return ''' + Bugzilla Bot uses the Bugzilla REST API v1 to interact with Bugzilla. In order to use + Bugzilla Bot, `bugzilla.conf` must be set up. See `doc.md` for more details. + ''' + + def initialize(self, bot_handler: Any) -> None: + config = bot_handler.get_config_info('bugzilla') + + site = config.get('site') + api_key = config.get('api_key') + if not site: + raise KeyError('No `site` was specified') + if not api_key: + raise KeyError('No `api_key` was specified') + + self.site = site + self.api_key = api_key + + def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None: + content = message.get('content') + topic = message.get('subject') + + if HELP_REGEX.match(content): + self.handle_help(message, bot_handler) + return None + + try: + bug_number = self.extract_bug_number(topic) + except ValueError: + bot_handler.send_reply(message, 'Unsupported topic: ' + topic) + return None + + comment = content + self.handle_comment(bug_number, comment, message, bot_handler) + + def handle_help(self, message: Dict[str, str], bot_handler: Any) -> None: + bot_handler.send_reply(message, HELP_RESPONSE) + + def handle_comment(self, bug_number: str, comment: str, message: Dict[str, str], bot_handler: Any) -> None: + url = '{}/rest/bug/{}/comment'.format(self.site, bug_number) + requests.post(url, + json=self.make_comment_json(comment)) + + def make_comment_json(self, comment: str) -> Any: + json = { + 'api_key': self.api_key, + 'comment': comment + } + return json + + def extract_bug_number(self, topic: str) -> Any: + topic_match = TOPIC_REGEX.match(topic) + + if not topic_match: + raise ValueError + else: + return topic_match.group('bug_number') + + +handler_class = BugzillaHandler diff --git a/zulip_bots/zulip_bots/bots/bugzilla/doc.md b/zulip_bots/zulip_bots/bots/bugzilla/doc.md new file mode 100644 index 000000000..7a625c165 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/bugzilla/doc.md @@ -0,0 +1,37 @@ +# Bugzilla bot + +This bot allows to update directly Bugzilla from Zulip + +## Setup + +To use Bugzilla Bot, first set up `bugzilla.conf`. `bugzilla.conf` takes 2 options: + - site (the site like `https://bugs.xxx.net` that includes both the protocol and the domain) + - api_key (a Bugzilla API key) + + Example: + ``` + [bugzilla] +site = https://bugs.site.net +api_key = xxxx + ``` + + +## Usage + +Run this bot as described +[here](https://zulipchat.com/api/running-bots#running-a-bot). + +Use this bot with the following command + +`@mentioned-bot ` in a topic that is named `Bug 123` where 123 is the bug number + +### comment + +With no argument, by default, a new comment is added to the bug that is associated to the topic. +For example, on topic Bug 123, + +you: + + > @**Bugzilla** A new comment + +Then `A new comment` is added to bug 123 diff --git a/zulip_bots/zulip_bots/bots/bugzilla/fixtures/test_comment.json b/zulip_bots/zulip_bots/bots/bugzilla/fixtures/test_comment.json new file mode 100644 index 000000000..fe29e01ef --- /dev/null +++ b/zulip_bots/zulip_bots/bots/bugzilla/fixtures/test_comment.json @@ -0,0 +1,17 @@ +{ + "request": { + "api_url": "https://bugs.net/rest/bug/123/comment", + "method": "POST", + "json": { + "api_key": "kkk", + "comment": "a comment" + } + }, + "response": { + "id": 124 + }, + "response-headers": { + "status": 200, + "content-type": "application/json; charset=utf-8" + } +} diff --git a/zulip_bots/zulip_bots/bots/bugzilla/test_bugzilla.py b/zulip_bots/zulip_bots/bots/bugzilla/test_bugzilla.py new file mode 100644 index 000000000..6cfb1d028 --- /dev/null +++ b/zulip_bots/zulip_bots/bots/bugzilla/test_bugzilla.py @@ -0,0 +1,94 @@ +from typing import Any, Dict +from zulip_bots.test_lib import BotTestCase, DefaultTests + + +class TestBugzillaBot(BotTestCase, DefaultTests): + bot_name = 'bugzilla' + + MOCK_CONFIG_INFO = { + 'site': 'https://bugs.net', + 'api_key': 'kkk' + } + + MOCK_HELP_RESPONSE = ''' +**help** + +`help` returns this short help + +**comment** + +With no argument, by default, a new comment is added to the bug that is associated to the topic. +For example, on topic Bug 123, + +you: + + > @**Bugzilla** A new comment + +Then `A new comment` is added to bug 123 +''' + + def make_request_message(self, content: str) -> Dict[str, Any]: + message = super().make_request_message(content) + message['subject'] = "Bug 123" + return message + + def handle_message_only(self, request: str) -> Dict[str, Any]: + bot, bot_handler = self._get_handlers() + message = self.make_request_message(request) + bot_handler.reset_transcript() + bot.handle_message(message, bot_handler) + + def test_bot_responds_to_empty_message(self) -> None: + pass + + def _test_invalid_config(self, invalid_config, error_message) -> None: + with self.mock_config_info(invalid_config), \ + self.assertRaisesRegexp(KeyError, error_message): + bot, bot_handler = self._get_handlers() + + def test_config_without_site(self) -> None: + config_without_site = { + 'api_key': 'kkk', + } + self._test_invalid_config(config_without_site, + 'No `site` was specified') + + def test_config_without_api_key(self) -> None: + config_without_api_key = { + 'site': 'https://bugs.xx', + } + self._test_invalid_config(config_without_api_key, + 'No `api_key` was specified') + + def test_comment(self) -> None: + with self.mock_config_info(self.MOCK_CONFIG_INFO), \ + self.mock_http_conversation('test_comment'): + self.handle_message_only('a comment') + + def test_help(self) -> None: + with self.mock_config_info(self.MOCK_CONFIG_INFO): + self.verify_reply('help', self.MOCK_HELP_RESPONSE) + + +class TestBugzillaBotWrongTopic(BotTestCase, DefaultTests): + bot_name = 'bugzilla' + + MOCK_CONFIG_INFO = { + 'site': 'https://bugs.net', + 'api_key': 'kkk' + } + + MOCK_COMMENT_INVALID_TOPIC_RESPONSE = 'Unsupported topic: kqatm2' + + def make_request_message(self, content: str) -> Dict[str, Any]: + message = super().make_request_message(content) + message['subject'] = "kqatm2" + return message + + def test_bot_responds_to_empty_message(self) -> None: + pass + + def test_no_bug_number(self) -> None: + with self.mock_config_info(self.MOCK_CONFIG_INFO): + self.verify_reply( + 'a comment', self.MOCK_COMMENT_INVALID_TOPIC_RESPONSE)