From b29f6eefdcfdc54d27c2ed080ad18b4f9b1c57d0 Mon Sep 17 00:00:00 2001 From: Sivagiri Visakan Date: Thu, 7 Mar 2019 20:53:55 +0530 Subject: [PATCH] zulip_bots/github_detail: Reply with contents of file when URL is given. --- .../bots/github_detail/github_detail.py | 75 +++++++++++++++++-- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/zulip_bots/zulip_bots/bots/github_detail/github_detail.py b/zulip_bots/zulip_bots/bots/github_detail/github_detail.py index 45f1bd31d..34a4d8d63 100644 --- a/zulip_bots/zulip_bots/bots/github_detail/github_detail.py +++ b/zulip_bots/zulip_bots/bots/github_detail/github_detail.py @@ -15,7 +15,12 @@ class GithubHandler(object): ''' GITHUB_ISSUE_URL_TEMPLATE = 'https://api.github.com/repos/{owner}/{repo}/issues/{id}' - HANDLE_MESSAGE_REGEX = re.compile("(?:([\w-]+)\/)?([\w-]+)?#(\d+)") + ISSUE_PR_NUMBER_REGEX = re.compile("(?:([\w-]+)\/)?([\w-]+)?#(\d+)") + + GITHUB_RAW_FILE_CONTENT_URL_TEMPLATE = 'https://raw.githubusercontent.com/{owner}/{repo}/{path}' + GITHUB_REPO_URL_REGEX = re.compile('^(http|https|git)://(www\.|)github.com/(?P([^/]+))/' + '(?P([^/]+))/(?P([^/]+))/' + '(?P([^#]+))(?P(|#.*))$') def initialize(self, bot_handler: Any) -> None: self.config_info = bot_handler.get_config_info('github_detail', optional=True) @@ -23,11 +28,16 @@ def initialize(self, bot_handler: Any) -> None: self.repo = self.config_info.get("repo", False) def usage(self) -> str: - return ("This plugin displays details on github issues and pull requests. " + return ("* **Get PR/issue details:**This plugin displays details on github issues and pull requests. " "To reference an issue or pull request usename mention the bot then " "anytime in the message type its id, for example:\n" "@**Github detail** #3212 zulip#3212 zulip/zulip#3212\n" - "The default owner is {} and the default repo is {}.".format(self.owner, self.repo)) + "The default owner is {} and the default repo is {}." + "\n* **Get a file's contents:**Give a full url to get the line contents" + "\nSend an valid URL to a GitHub file to get its contents." + "\nUse the line numbers to get only the specific lines from the file.\n" + "Example: `@**Github detail**` https://github.com/zulip/zulip/blob/" + "master/zerver/apps.py#L10-L12".format(self.owner, self.repo)) def format_message(self, details: Dict[str, Any]) -> str: number = details['number'] @@ -72,16 +82,31 @@ def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None: if message['content'] == 'help': bot_handler.send_reply(message, self.usage()) return + reply = '' + try: + # Try and match if a GitHub URL is present in the message + url_details = self.get_file_url_match(message['content']) + if url_details and url_details['type'] == 'blob': + reply = self.get_file_contents(**url_details) + else: + # Try and match the `owner/repo#123` type issue or PR number + reply = self.match_pr_and_get_reply(message['content']) + bot_handler.send_reply(message, reply or 'I couldn\'t process that message.' + 'Try replying `help` to know what I can do.') + except Exception as e: + logging.exception(str(e)) + bot_handler.send_reply(message, ':dizzy: Something unexpected happened!') + + def match_pr_and_get_reply(self, message_content): # Capture owner, repo, id issue_prs = list(re.finditer( - self.HANDLE_MESSAGE_REGEX, message['content'])) + self.ISSUE_PR_NUMBER_REGEX, message_content)) bot_messages = [] if len(issue_prs) > 5: # We limit to 5 requests to prevent denial-of-service bot_message = 'Please ask for <=5 links in any one request' - bot_handler.send_reply(message, bot_message) - return + return bot_message for issue_pr in issue_prs: owner, repo = self.get_owner_and_repo(issue_pr) @@ -97,8 +122,42 @@ def handle_message(self, message: Dict[str, str], bot_handler: Any) -> None: else: bot_messages.append("Failed to detect owner and repository name.") if len(bot_messages) == 0: - bot_messages.append("Failed to find any issue or PR.") + return None bot_message = '\n'.join(bot_messages) - bot_handler.send_reply(message, bot_message) + return bot_message + + def get_file_url_match(self, message_content): + details = re.match(self.GITHUB_REPO_URL_REGEX, message_content) + return details.groupdict() if details is not None else None + + def get_file_contents(self, owner, repository, path, lines, **kwargs): + url = self.GITHUB_RAW_FILE_CONTENT_URL_TEMPLATE.format(owner=owner, + repo=repository, + path=path) + try: + raw_file = requests.get(url) + raw_file.raise_for_status() + except requests.exceptions.RequestException as e: + return 'An error occurred while trying to fetch the contents of the file :oh_no: \n' \ + 'Are you sure the URL is correct ?' + + if lines: + line_number_regex = re.compile('^#L(?P(\d+))(|-L(?P(\d+)))$') + line_number_match = re.match(line_number_regex, lines) + if line_number_match: + # Decrementing to select the index in a list + start = int(line_number_match.group('start'))-1 + + # Sometimes, the ending line number might be missing(i.e only one line has been selected) + # In that case we set it to start+1 (so the list can be sliced correctly) + end = line_number_match.group('end') or start+1 + end = int(end) + lines_as_list = raw_file.text.split('\n') + reply = '\n'.join(lines_as_list[start:end]) + return '**[{}](https://www.github.com/blob/{}):** *{} - {}*\n' \ + '```\n\n{}\n```'.format(path, path, start+1, end, reply) + else: + return '**[{}](https://www.github.com/blob/{}):**\n' \ + '```\n{}\n```'.format(path, path, raw_file.text) handler_class = GithubHandler