From 1b58c13d91b1756e157c573d1ae53c96ca123a6d Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 13 Dec 2016 11:43:16 +0000 Subject: [PATCH] Interactive bots: Create Github Issues bot. --- contrib_bots/lib/github.py | 25 ++++++ contrib_bots/lib/github_issues.py | 125 ++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 contrib_bots/lib/github.py create mode 100644 contrib_bots/lib/github_issues.py diff --git a/contrib_bots/lib/github.py b/contrib_bots/lib/github.py new file mode 100644 index 0000000..548156b --- /dev/null +++ b/contrib_bots/lib/github.py @@ -0,0 +1,25 @@ +# The purpose of this file is to handle all requests +# to the Github API, which will make sure that all +# requests are to the same account, and that all requests +# authenticated correctly and securely +# The sole purpose of this is to authenticate, and it will +# return the requests session that is properly authenticated +import os +import requests +import six.moves.configparser + +CONFIG_FILE = '~/.github-issue-bot/github-issue-bot.conf' + +def auth(): + # Sets up the configParser to read the .conf file + config = six.moves.configparser.ConfigParser() + # Reads the config file + config.read([os.path.expanduser(CONFIG_FILE)]) + # Gets the properties from the config file + oauth_token = config.get('github', 'github_token') + username = config.get('github', 'github_username') + # Creates and authorises a requests session + session = requests.session() + session.auth = (username, oauth_token) + # Returns the session + return session diff --git a/contrib_bots/lib/github_issues.py b/contrib_bots/lib/github_issues.py new file mode 100644 index 0000000..c8864fd --- /dev/null +++ b/contrib_bots/lib/github_issues.py @@ -0,0 +1,125 @@ +from __future__ import absolute_import +from future import standard_library +standard_library.install_aliases() +from . import github +import json +import os +import requests +import six.moves.configparser +import urllib.request, urllib.error, urllib.parse + +class IssueHandler(object): + ''' + This plugin facilitates sending issues to github, when + an item is prefixed with '@issue' or '@bug' + + It will also write items to the issues stream, as well + as reporting it to github + ''' + + URL = 'https://api.github.com/repos/{}/{}/issues' + CHARACTER_LIMIT = 70 + CONFIG_FILE = '~/.github-issue-bot/github-issue-bot.conf' + REPO_NAME = '' + REPO_OWNER = '' + + def __init__(self): + # gets token from config file + # Token at CONFIG_FILE address + config = six.moves.configparser.ConfigParser() + config.read([os.path.expanduser(self.CONFIG_FILE)]) + + self.REPO_NAME = config.get('github', 'github_repo') + self.REPO_OWNER = config.get('github', 'github_repo_owner') + + def usage(self): + return ''' + This plugin will allow users to flag messages + as being issues with Zulip by using te prefix '@issue' + + Before running this, make sure to create a stream + called "issues" that your API user can send to. + + Also, make sure that the credentials of the github bot have + been typed in correctly, that there is a personal access token + with access to public repositories ONLY, + and that the repository name is entered correctly. + + Check ~/.github-issue-bot/github-issue-bot.conf, and make sure there are + github_repo (The name of the repo to post to) + github_repo_owner (The owner of the repo to post to) + github_username (The username of the github bot) + github_token (The personal access token for the github bot) + ''' + + def triage_message(self, message): + # return True if we want to (possibly) respond to this message + original_content = message['content'] + # This next line of code is defensive, as we + # never want to get into an infinite loop of posting follow + # ups for own follow ups! + if message['display_recipient'] == 'issue': + return False + is_issue = original_content.startswith('@issue') + return is_issue + + def handle_message(self, message, client, state_handler): + + original_content = message['content'] + original_sender = message['sender_email'] + + new_content = original_content.replace('@issue', 'by {}:'.format(original_sender,)) + # gets the repo url + url_new = self.URL.format(self.REPO_OWNER, self.REPO_NAME) + + # signs into github using the provided username and password + session = github.auth() + + # Gets rid of the @issue in the issue title + issue_title = message['content'].replace('@issue', '').strip() + issue_content = '' + new_issue_title = '' + for part_of_title in issue_title.split(): + if len(new_issue_title) < self.CHARACTER_LIMIT: + new_issue_title += '{} '.format(part_of_title) + else: + issue_content += '{} '.format(part_of_title) + + new_issue_title = new_issue_title.strip() + issue_content = issue_content.strip() + new_issue_title += '...' + + # Creates the issue json, that is transmitted to the github api servers + issue = { + 'title': new_issue_title, + 'body': '{} **Sent by [{}](https://chat.zulip.org/#) from zulip**'.format(issue_content, original_sender), + 'assignee': '', + 'milestone': 'none', + 'labels': [''], + } + # Sends the HTTP post request + r = session.post(url_new, json.dumps(issue)) + + if r.ok: + # sends the message onto the 'issues' stream so it can be seen by zulip users + client.send_message(dict( + type='stream', + to='issues', + subject=message['sender_email'], + # Adds a check mark so that the user can verify if it has been sent + content='{} :heavy_check_mark:'.format(new_content), + )) + return + # This means that the issue has not been sent + # sends the message onto the 'issues' stream so it can be seen by zulip users + client.send_message(dict( + type='stream', + to='issues', + subject=message['sender_email'], + # Adds a cross so that the user can see that it has failed, and provides a link to a + # google search that can (hopefully) direct them to the error + content='{} :x: Code: [{}](https://www.google.com/search?q=Github HTTP {} Error {})' + .format(new_content, r.status_code, r.status_code, r.content), + )) + +handler_class = IssueHandler