split github webhook logic in to a more general webhook module
This commit is contained in:
parent
2ef85960e9
commit
14f2fd6a03
4 changed files with 440 additions and 376 deletions
223
modules/github.py
Normal file
223
modules/github.py
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
#--depends-on channel_access
|
||||||
|
#--depends-on check_mode
|
||||||
|
#--depends-on commands
|
||||||
|
#--depends-on config
|
||||||
|
#--depends-on permissions
|
||||||
|
#--depends-on rest_api
|
||||||
|
|
||||||
|
import datetime, itertools, json, math, re, urllib.parse
|
||||||
|
from src import EventManager, ModuleManager, utils
|
||||||
|
|
||||||
|
COLOR_BRANCH = utils.consts.ORANGE
|
||||||
|
COLOR_REPO = utils.consts.GREY
|
||||||
|
COLOR_POSITIVE = utils.consts.GREEN
|
||||||
|
COLOR_NEUTRAL = utils.consts.LIGHTGREY
|
||||||
|
COLOR_NEGATIVE = utils.consts.RED
|
||||||
|
COLOR_ID = utils.consts.PINK
|
||||||
|
|
||||||
|
API_ISSUE_URL = "https://api.github.com/repos/%s/%s/issues/%s"
|
||||||
|
API_PULL_URL = "https://api.github.com/repos/%s/%s/pulls/%s"
|
||||||
|
|
||||||
|
@utils.export("channelset", {"setting": "github-default-repo",
|
||||||
|
"help": "Set the default github repo for the current channel",
|
||||||
|
"example": "jesopo/bitbot"})
|
||||||
|
@utils.export("channelset", {"setting": "auto-github",
|
||||||
|
"help": "Enable/disable automatically getting github issue/PR info",
|
||||||
|
"validate": utils.bool_or_none, "example": "on"})
|
||||||
|
@utils.export("channelset", {"setting": "auto-github-cooldown",
|
||||||
|
"help": "Set amount of seconds between auto-github duplicates",
|
||||||
|
"validate": utils.int_or_none, "example": "300"})
|
||||||
|
class Module(ModuleManager.BaseModule):
|
||||||
|
def _parse_ref(self, channel, ref):
|
||||||
|
repo, _, number = ref.rpartition("#")
|
||||||
|
org, _, repo = repo.partition("/")
|
||||||
|
|
||||||
|
default_repo = channel.get_setting("github-default-repo", "")
|
||||||
|
default_org, _, default_repo = default_repo.partition("/")
|
||||||
|
|
||||||
|
if org and not repo:
|
||||||
|
repo = org or default_repo
|
||||||
|
org = default_org
|
||||||
|
else:
|
||||||
|
org = org or default_org
|
||||||
|
repo = repo or default_repo
|
||||||
|
|
||||||
|
if not org or not repo or not number:
|
||||||
|
raise utils.EventError("Please provide username/repo#number")
|
||||||
|
if not number.isdigit():
|
||||||
|
raise utils.EventError("Issue number must be a number")
|
||||||
|
return org, repo, number
|
||||||
|
|
||||||
|
def _short_url(self, url):
|
||||||
|
try:
|
||||||
|
page = utils.http.request("https://git.io", method="POST",
|
||||||
|
post_data={"url": url})
|
||||||
|
return page.headers["Location"]
|
||||||
|
except utils.http.HTTPTimeoutException:
|
||||||
|
self.log.warn(
|
||||||
|
"HTTPTimeoutException while waiting for github short URL", [])
|
||||||
|
return url
|
||||||
|
|
||||||
|
def _change_count(self, n, symbol, color):
|
||||||
|
return utils.irc.color("%s%d" % (symbol, n), color)+utils.irc.bold("")
|
||||||
|
def _added(self, n):
|
||||||
|
return self._change_count(n, "+", COLOR_POSITIVE)
|
||||||
|
def _removed(self, n):
|
||||||
|
return self._change_count(n, "-", COLOR_NEGATIVE)
|
||||||
|
def _modified(self, n):
|
||||||
|
return self._change_count(n, "~", utils.consts.PURPLE)
|
||||||
|
|
||||||
|
def _parse_issue(self, page, username, repository, number):
|
||||||
|
repo = utils.irc.color("%s/%s" % (username, repository), COLOR_REPO)
|
||||||
|
number = utils.irc.color("#%s" % number, COLOR_ID)
|
||||||
|
labels = [label["name"] for label in page.data["labels"]]
|
||||||
|
labels_str = ""
|
||||||
|
if labels:
|
||||||
|
labels_str = "[%s] " % ", ".join(labels)
|
||||||
|
|
||||||
|
url = self._short_url(page.data["html_url"])
|
||||||
|
|
||||||
|
state = page.data["state"]
|
||||||
|
if state == "open":
|
||||||
|
state = utils.irc.color("open", COLOR_NEUTRAL)
|
||||||
|
elif state == "closed":
|
||||||
|
state = utils.irc.color("closed", COLOR_NEGATIVE)
|
||||||
|
|
||||||
|
return "(%s issue%s, %s) %s %s%s" % (
|
||||||
|
repo, number, state, page.data["title"], labels_str, url)
|
||||||
|
def _get_issue(self, username, repository, number):
|
||||||
|
return utils.http.request(
|
||||||
|
API_ISSUE_URL % (username, repository, number),
|
||||||
|
json=True)
|
||||||
|
|
||||||
|
@utils.hook("received.command.ghissue", min_args=1)
|
||||||
|
def github_issue(self, event):
|
||||||
|
if event["target"].get_setting("github-hide-prefix", False):
|
||||||
|
event["stdout"].hide_prefix()
|
||||||
|
event["stderr"].hide_prefix()
|
||||||
|
|
||||||
|
username, repository, number = self._parse_ref(
|
||||||
|
event["target"], event["args_split"][0])
|
||||||
|
|
||||||
|
page = self._get_issue(username, repository, number)
|
||||||
|
if page and page.code == 200:
|
||||||
|
self._parse_issue(page, username, repository, number)
|
||||||
|
else:
|
||||||
|
event["stderr"].write("Could not find issue")
|
||||||
|
|
||||||
|
def _parse_pull(self, page, username, repository, number):
|
||||||
|
repo = utils.irc.color("%s/%s" % (username, repository), COLOR_REPO)
|
||||||
|
number = utils.irc.color("#%s" % number, COLOR_ID)
|
||||||
|
branch_from = page.data["head"]["label"]
|
||||||
|
branch_to = page.data["base"]["label"]
|
||||||
|
added = self._added(page.data["additions"])
|
||||||
|
removed = self._removed(page.data["deletions"])
|
||||||
|
url = self._short_url(page.data["html_url"])
|
||||||
|
|
||||||
|
state = page.data["state"]
|
||||||
|
if page.data["merged"]:
|
||||||
|
state = utils.irc.color("merged", COLOR_POSITIVE)
|
||||||
|
elif state == "open":
|
||||||
|
state = utils.irc.color("open", COLOR_NEUTRAL)
|
||||||
|
elif state == "closed":
|
||||||
|
state = utils.irc.color("closed", COLOR_NEGATIVE)
|
||||||
|
|
||||||
|
return "(%s PR%s, %s) %s → %s [%s/%s] %s %s" % (
|
||||||
|
repo, number, state, branch_from, branch_to, added, removed,
|
||||||
|
page.data["title"], url)
|
||||||
|
def _get_pull(self, username, repository, number):
|
||||||
|
return utils.http.request(
|
||||||
|
API_PULL_URL % (username, repository, number),
|
||||||
|
json=True)
|
||||||
|
@utils.hook("received.command.ghpull", min_args=1)
|
||||||
|
def github_pull(self, event):
|
||||||
|
if event["target"].get_setting("github-hide-prefix", False):
|
||||||
|
event["stdout"].hide_prefix()
|
||||||
|
event["stderr"].hide_prefix()
|
||||||
|
|
||||||
|
username, repository, number = self._parse_ref(
|
||||||
|
event["target"], event["args_split"][0])
|
||||||
|
page = self._get_pull(username, repository, number)
|
||||||
|
|
||||||
|
if page and page.code == 200:
|
||||||
|
self._parse_pull(page, username, repository, number)
|
||||||
|
else:
|
||||||
|
event["stderr"].write("Could not find pull request")
|
||||||
|
|
||||||
|
def _get_info(self, target, ref):
|
||||||
|
username, repository, number = self._parse_ref(target, ref)
|
||||||
|
page = self._get_issue(username, repository, number)
|
||||||
|
if page and page.code == 200:
|
||||||
|
if "pull_request" in page.data:
|
||||||
|
pull = self._get_pull(username, repository, number)
|
||||||
|
return self._parse_pull(pull, username, repository, number)
|
||||||
|
else:
|
||||||
|
return self._parse_issue(page, username, repository, number)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@utils.hook("received.command.gh", alias_of="github")
|
||||||
|
@utils.hook("received.command.github", min_args=1)
|
||||||
|
def github(self, event):
|
||||||
|
if event["target"].get_setting("github-hide-prefix", False):
|
||||||
|
event["stdout"].hide_prefix()
|
||||||
|
event["stderr"].hide_prefix()
|
||||||
|
result = self._get_info(event["target"], event["args_split"][0])
|
||||||
|
if not result == None:
|
||||||
|
event["stdout"].write(result)
|
||||||
|
else:
|
||||||
|
event["stderr"].write("Issue/PR not found")
|
||||||
|
|
||||||
|
def _cache_ref(self, ref):
|
||||||
|
return "auto-github-%s" % ref.lower()
|
||||||
|
def _auto_github_cooldown(self, channel, ref):
|
||||||
|
cooldown = channel.get_setting("auto-github-cooldown", None)
|
||||||
|
if not cooldown == None:
|
||||||
|
cache = self._cache_ref(ref)
|
||||||
|
if not self.bot.cache.has_item(cache):
|
||||||
|
self.bot.cache.temporary_cache(cache, cooldown)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@utils.hook("command.regex", ignore_action=False)
|
||||||
|
def url_regex(self, event):
|
||||||
|
"""
|
||||||
|
:command: github
|
||||||
|
:pattern: https?://github.com/([^/]+)/([^/]+)/(pull|issues)/(\d+)
|
||||||
|
"""
|
||||||
|
if event["target"].get_setting("auto-github", False):
|
||||||
|
event.eat()
|
||||||
|
ref = "%s/%s#%s" % (event["match"].group(1),
|
||||||
|
event["match"].group(2), event["match"].group(4))
|
||||||
|
if self._auto_github_cooldown(event["target"], ref):
|
||||||
|
try:
|
||||||
|
result = self._get_info(event["target"], ref)
|
||||||
|
except utils.EventError:
|
||||||
|
return
|
||||||
|
if result:
|
||||||
|
if event["target"].get_setting("github-hide-prefix", False):
|
||||||
|
event["stdout"].hide_prefix()
|
||||||
|
event["stdout"].write(result)
|
||||||
|
|
||||||
|
@utils.hook("command.regex", ignore_action=False)
|
||||||
|
def ref_regex(self, event):
|
||||||
|
"""
|
||||||
|
:command: github
|
||||||
|
:pattern: (?:\S+(?:\/\S+)?)?#\d+
|
||||||
|
"""
|
||||||
|
if event["target"].get_setting("auto-github", False):
|
||||||
|
event.eat()
|
||||||
|
ref = event["match"].group(0)
|
||||||
|
if self._auto_github_cooldown(event["target"], ref):
|
||||||
|
try:
|
||||||
|
result = self._get_info(event["target"],
|
||||||
|
event["match"].group(0))
|
||||||
|
except utils.EventError:
|
||||||
|
return
|
||||||
|
if result:
|
||||||
|
if event["target"].get_setting("github-hide-prefix", False):
|
||||||
|
event["stdout"].hide_prefix()
|
||||||
|
event["stdout"].write(result)
|
|
@ -1,53 +0,0 @@
|
||||||
# !ghwebhook
|
|
||||||
|
|
||||||
## List registered web hooks
|
|
||||||
`!ghwebhook list`
|
|
||||||
|
|
||||||
## Adding a web hook
|
|
||||||
|
|
||||||
`!ghwebhook add [name]` where `[name]` is either a full repository name (e.g. `jesopo/bitbot`) to get a specific repository or a user/organisation name (e.g. `jesopo`) to get all repositories for that user/organisation
|
|
||||||
|
|
||||||
## Removing a web hook
|
|
||||||
Same as above but with `remove` instead of `add`
|
|
||||||
|
|
||||||
## Modifying shown events
|
|
||||||
`!ghwebhook events [hook] [events]` where `[hook]` is the `[name]` used in the `add` command and where `[events]` is a space-separated list of either raw events (see [here](https://developer.github.com/v3/activity/events/types/)) or the following (default: "ping code pr issue repo")
|
|
||||||
|
|
||||||
#### ping
|
|
||||||
Shows when a newly registered web hook first hits BitBot
|
|
||||||
|
|
||||||
#### code
|
|
||||||
Shows for commits and comments on commits
|
|
||||||
|
|
||||||
#### pr-minimal
|
|
||||||
Shows minimal pull request actions; opened, closed, reopened
|
|
||||||
|
|
||||||
#### pr
|
|
||||||
Shows the same actions as `pr-minimal` and also: edited, assigned, unassigned, review requests, comments on review requests
|
|
||||||
|
|
||||||
#### pr-all
|
|
||||||
Shows the same actions as `pr` and also: labeled, unlabeled, new commits
|
|
||||||
|
|
||||||
#### issue-minimal
|
|
||||||
Shows minimal issue actions; opened, closed, reopened, deleted
|
|
||||||
|
|
||||||
#### issue
|
|
||||||
Shows the same actions as `issue-minimal` and also: edited, assigned, unassigned, comments on issues
|
|
||||||
|
|
||||||
#### issue-all
|
|
||||||
Shows the same actions as `issue` and also: transferred, pinned, unpinned, labeled, unlabeled, milestoned, demilestoned
|
|
||||||
|
|
||||||
#### repo
|
|
||||||
Shows events related repositories themselves; repository/branch/tag created, repository/branch/tag deleted, release created, fork created
|
|
||||||
|
|
||||||
#### team
|
|
||||||
Shows when users are added or removed from teams
|
|
||||||
|
|
||||||
## List shown events
|
|
||||||
`!ghwebhook events [hook]`
|
|
||||||
|
|
||||||
## Modify shown branches
|
|
||||||
`!ghwebhook branches [hook] [branches]` where `[hook]` is the `[name]` used in the `add` command and where `[branches]` is a space-separated list of branch names
|
|
||||||
|
|
||||||
## List shown branches
|
|
||||||
`!ghwebhook branches [hook]`
|
|
190
modules/webhooks/__init__.py
Normal file
190
modules/webhooks/__init__.py
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
from src import ModuleManager, utils
|
||||||
|
from . import github
|
||||||
|
|
||||||
|
@utils.export("channelset", {"setting": "git-prevent-highlight",
|
||||||
|
"help": "Enable/disable preventing highlights",
|
||||||
|
"validate": utils.bool_or_none, "example": "on"})
|
||||||
|
@utils.export("channelset", {"setting": "git-hide-organisation",
|
||||||
|
"help": "Hide/show organisation in repository names",
|
||||||
|
"validate": utils.bool_or_none, "example": "on"})
|
||||||
|
@utils.export("channelset", {"setting": "git-hide-prefix",
|
||||||
|
"help": "Hide/show command-like prefix on git webhook outputs",
|
||||||
|
"validate": utils.bool_or_none, "example": "on"})
|
||||||
|
class Module(ModuleManager.BaseModule):
|
||||||
|
def on_load(self):
|
||||||
|
self._github = github.GitHub()
|
||||||
|
|
||||||
|
@utils.hook("api.post.github")
|
||||||
|
def github_webhook(self, event):
|
||||||
|
return self._webhook("github", "GitHub", self._github,
|
||||||
|
event["data"], event["headers"])
|
||||||
|
|
||||||
|
def _webhook(self, webhook_type, webhook_name, handler, payload_str,
|
||||||
|
headers):
|
||||||
|
payload = event["data"].decode("utf8")
|
||||||
|
if event["headers"]["Content-Type"] == FORM_ENCODED:
|
||||||
|
payload = urllib.parse.unquote(urllib.parse.parse_qs(payload)[
|
||||||
|
"payload"][0])
|
||||||
|
data = json.loads(payload)
|
||||||
|
|
||||||
|
full_name, repo_username, report_name, organisation = handler.names(
|
||||||
|
data, headers)
|
||||||
|
branch = handler.branch(data, headers)
|
||||||
|
event, event_action = handler.event(data, headers)
|
||||||
|
|
||||||
|
hooks = self.bot.database.channel_settings.find_by_setting(
|
||||||
|
"%s-hooks" % webhook_type)
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
repo_hooked = False
|
||||||
|
|
||||||
|
for server_id, channel_name, hooked_repos in hooks:
|
||||||
|
found_hook = None
|
||||||
|
if full_name and full_name in hooked_repos:
|
||||||
|
found_hook = hooked_repos[full_name]
|
||||||
|
elif repo_username and repo_username in hooked_repos:
|
||||||
|
found_hook = hooked_repos[repo_username]
|
||||||
|
elif organisation and organisation in hooked_repos:
|
||||||
|
found_hook = hooked_repos[organisation]
|
||||||
|
|
||||||
|
repo_hooked = True
|
||||||
|
server = self.bot.get_server_by_id(server_id)
|
||||||
|
if server and channel_name in server.channels:
|
||||||
|
if (branch and
|
||||||
|
found_hook["branches"] and
|
||||||
|
not branch in found_hook["branches"]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
events = []
|
||||||
|
for hooked_event in found_hook["events"]:
|
||||||
|
events.append(EVENT_CATEGORIES.get(hooked_event,
|
||||||
|
[hooked_event]))
|
||||||
|
events = list(itertools.chain(*events))
|
||||||
|
|
||||||
|
channel = server.channels.get(channel_name)
|
||||||
|
if (current_event in events or
|
||||||
|
(event_action and event_action in events)):
|
||||||
|
targets.append([server, channel])
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
if not repo_hooked:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return {"state": "success", "deliveries": 0}
|
||||||
|
|
||||||
|
outputs = handler.webhook(data, headers)
|
||||||
|
|
||||||
|
if outputs:
|
||||||
|
for server, channel in targets:
|
||||||
|
source = full_name or organisation
|
||||||
|
hide_org = channel.get_setting("git-hide-organisation", False)
|
||||||
|
if repo_name and hide_org:
|
||||||
|
source = repo_name
|
||||||
|
|
||||||
|
for output in outputs:
|
||||||
|
output = "(%s) %s" % (
|
||||||
|
utils.irc.color(source, COLOR_REPO), output)
|
||||||
|
|
||||||
|
if channel.get_setting("git-prevent-highlight", False):
|
||||||
|
output = self._prevent_highlight(server, channel,
|
||||||
|
output)
|
||||||
|
|
||||||
|
hide_prefix = channel.get_setting("git-hide-prefix", False)
|
||||||
|
self.events.on("send.stdout").call(target=channel,
|
||||||
|
module_name=webhook_name, server=server, message=output,
|
||||||
|
hide_prefix=hide_prefix)
|
||||||
|
|
||||||
|
def _prevent_highlight(self, server, channel, s):
|
||||||
|
for user in channel.users:
|
||||||
|
if len(user.nickname) == 1:
|
||||||
|
# if we don't ignore 1-letter nicknames, the below while loop
|
||||||
|
# will fire indefininitely.
|
||||||
|
continue
|
||||||
|
|
||||||
|
regex = re.compile(r"(.)\b(%s)(%s)" % (
|
||||||
|
re.escape(user.nickname[0]), re.escape(user.nickname[1:])),
|
||||||
|
re.I)
|
||||||
|
s = regex.sub("\\1\\2\u200c\\3", s)
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
@utils.hook("received.command.webhook", min_args=1, channel_only=True)
|
||||||
|
def github_webhook(self, event):
|
||||||
|
"""
|
||||||
|
:help: Add/remove/modify a git webhook
|
||||||
|
:require_mode: high
|
||||||
|
:require_access: git-webhook
|
||||||
|
:permission: gitoverride
|
||||||
|
:usage: list
|
||||||
|
:usage: add <hook>
|
||||||
|
:usage: remove <hook>
|
||||||
|
:usage: events <hook> [category [category ...]]
|
||||||
|
:usage: branches <hook> [branch [branch ...]]
|
||||||
|
"""
|
||||||
|
all_hooks = event["target"].get_setting("webhooks", {})
|
||||||
|
hook_name = None
|
||||||
|
existing_hook = None
|
||||||
|
if len(event["args_split"]) > 1:
|
||||||
|
hook_name = event["args_split"][1]
|
||||||
|
for existing_hook_name in all_hooks.keys():
|
||||||
|
if existing_hook_name.lower() == hook_name.lower():
|
||||||
|
existing_hook = existing_hook_name
|
||||||
|
break
|
||||||
|
|
||||||
|
success_message = None
|
||||||
|
|
||||||
|
subcommand = event["args_split"][0].lower()
|
||||||
|
if subcommand == "list":
|
||||||
|
event["stdout"].write("Registered web hooks: %s" %
|
||||||
|
", ".join(all_hooks.keys()))
|
||||||
|
elif subcommand == "add":
|
||||||
|
if existing_hook:
|
||||||
|
raise utils.EventError("There's already a hook for %s" %
|
||||||
|
hook_name)
|
||||||
|
|
||||||
|
all_hooks[hook_name] = {
|
||||||
|
"events": DEFAULT_EVENT_CATEGORIES.copy(),
|
||||||
|
"branches": [],
|
||||||
|
}
|
||||||
|
success_message = "Added hook for %s" % hook_name
|
||||||
|
|
||||||
|
elif subcommand == "remove":
|
||||||
|
if not existing_hook:
|
||||||
|
raise utils.EventError("No hook found for %s" % hook_name)
|
||||||
|
|
||||||
|
del all_hooks[existing_hook]
|
||||||
|
success_message = "Removed hook for %s" % hook_name
|
||||||
|
|
||||||
|
elif subcommand == "events":
|
||||||
|
if not existing_hook:
|
||||||
|
raise utils.EventError("No hook found for %s" % hook_name)
|
||||||
|
|
||||||
|
if len(event["args_split"]) < 3:
|
||||||
|
event["stdout"].write("Events for hook %s: %s" %
|
||||||
|
(hook_name, " ".join(all_hooks[existing_hook]["events"])))
|
||||||
|
else:
|
||||||
|
new_events = [e.lower() for e in event["args_split"][2:]]
|
||||||
|
all_hooks[existing_hook]["events"] = new_events
|
||||||
|
sucess_message = "Updated events for hook %s" % hook_name
|
||||||
|
elif subcommand == "branches":
|
||||||
|
if not existing_hook:
|
||||||
|
raise utils.EventError("No hook found for %s" % hook_name)
|
||||||
|
|
||||||
|
if len(event["args_split"]) < 3:
|
||||||
|
branches = ",".join(all_hooks[existing_hook]["branches"])
|
||||||
|
event["stdout"].write("Branches shown for hook %s: %s" %
|
||||||
|
(hook_name, branches))
|
||||||
|
else:
|
||||||
|
all_hooks[existing_hook]["branches"] = event["args_split"][2:]
|
||||||
|
success_message = "Updated branches for hook %s" % hook_name
|
||||||
|
else:
|
||||||
|
event["stderr"].write("Unknown command '%s'" %
|
||||||
|
event["args_split"][0])
|
||||||
|
|
||||||
|
if not success_message == None:
|
||||||
|
if all_hooks:
|
||||||
|
event["target"].set_setting("webhooks", all_hooks)
|
||||||
|
else:
|
||||||
|
event["target"].del_setting("webhooks")
|
||||||
|
|
||||||
|
event["stdout"].write(success_message)
|
|
@ -1,19 +1,5 @@
|
||||||
#--depends-on channel_access
|
#--depends-on-github
|
||||||
#--depends-on check_mode
|
from src import ModuleManager, utils
|
||||||
#--depends-on commands
|
|
||||||
#--depends-on config
|
|
||||||
#--depends-on permissions
|
|
||||||
#--depends-on rest_api
|
|
||||||
|
|
||||||
import datetime, itertools, json, math, re, urllib.parse
|
|
||||||
from src import EventManager, ModuleManager, utils
|
|
||||||
|
|
||||||
COLOR_BRANCH = utils.consts.ORANGE
|
|
||||||
COLOR_REPO = utils.consts.GREY
|
|
||||||
COLOR_POSITIVE = utils.consts.GREEN
|
|
||||||
COLOR_NEUTRAL = utils.consts.LIGHTGREY
|
|
||||||
COLOR_NEGATIVE = utils.consts.RED
|
|
||||||
COLOR_ID = utils.consts.PINK
|
|
||||||
|
|
||||||
FORM_ENCODED = "application/x-www-form-urlencoded"
|
FORM_ENCODED = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
|
@ -21,9 +7,6 @@ COMMIT_URL = "https://github.com/%s/commit/%s"
|
||||||
COMMIT_RANGE_URL = "https://github.com/%s/compare/%s...%s"
|
COMMIT_RANGE_URL = "https://github.com/%s/compare/%s...%s"
|
||||||
CREATE_URL = "https://github.com/%s/tree/%s"
|
CREATE_URL = "https://github.com/%s/tree/%s"
|
||||||
|
|
||||||
API_ISSUE_URL = "https://api.github.com/repos/%s/%s/issues/%s"
|
|
||||||
API_PULL_URL = "https://api.github.com/repos/%s/%s/pulls/%s"
|
|
||||||
|
|
||||||
DEFAULT_EVENT_CATEGORIES = [
|
DEFAULT_EVENT_CATEGORIES = [
|
||||||
"ping", "code", "pr", "issue", "repo"
|
"ping", "code", "pr", "issue", "repo"
|
||||||
]
|
]
|
||||||
|
@ -98,289 +81,34 @@ CHECK_RUN_CONCLUSION = {
|
||||||
}
|
}
|
||||||
CHECK_RUN_FAILURES = ["failure", "cancelled", "timed_out", "action_required"]
|
CHECK_RUN_FAILURES = ["failure", "cancelled", "timed_out", "action_required"]
|
||||||
|
|
||||||
@utils.export("channelset", {"setting": "github-hide-prefix",
|
class GitHub(object):
|
||||||
"help": "Hide/show command-like prefix on Github hook outputs",
|
def names(self, data, headers):
|
||||||
"validate": utils.bool_or_none, "example": "on"})
|
full_name = None
|
||||||
@utils.export("channelset", {"setting": "github-hide-organisation",
|
repo_username = None
|
||||||
"help": "Hide/show organisation in repository names",
|
repo_name = None
|
||||||
"validate": utils.bool_or_none, "example": "on"})
|
if "repository" in data:
|
||||||
@utils.export("channelset", {"setting": "github-default-repo",
|
full_name = data["repository"]["full_name"]
|
||||||
"help": "Set the default github repo for the current channel",
|
repo_username, repo_name = full_name.split("/", 1)
|
||||||
"example": "jesopo/bitbot"})
|
|
||||||
@utils.export("channelset", {"setting": "github-prevent-highlight",
|
|
||||||
"help": "Enable/disable preventing highlights",
|
|
||||||
"validate": utils.bool_or_none, "example": "on"})
|
|
||||||
@utils.export("channelset", {"setting": "auto-github",
|
|
||||||
"help": "Enable/disable automatically getting github issue/PR info",
|
|
||||||
"validate": utils.bool_or_none, "example": "on"})
|
|
||||||
@utils.export("channelset", {"setting": "auto-github-cooldown",
|
|
||||||
"help": "Set amount of seconds between auto-github duplicates",
|
|
||||||
"validate": utils.int_or_none, "example": "300"})
|
|
||||||
class Module(ModuleManager.BaseModule):
|
|
||||||
def _parse_ref(self, channel, ref):
|
|
||||||
repo, _, number = ref.rpartition("#")
|
|
||||||
org, _, repo = repo.partition("/")
|
|
||||||
|
|
||||||
default_repo = channel.get_setting("github-default-repo", "")
|
organisation = None
|
||||||
default_org, _, default_repo = default_repo.partition("/")
|
if "organization" in data:
|
||||||
|
organisation = data["organization"]["login"]
|
||||||
|
return full_name, repo_username, repo_name, oraganisation
|
||||||
|
|
||||||
if org and not repo:
|
def branch(self, data, headers):
|
||||||
repo = org or default_repo
|
if "ref" in data:
|
||||||
org = default_org
|
return data["ref"].rpartition("/")[2]
|
||||||
else:
|
return None
|
||||||
org = org or default_org
|
|
||||||
repo = repo or default_repo
|
|
||||||
|
|
||||||
if not org or not repo or not number:
|
def event(self, data, headers):
|
||||||
raise utils.EventError("Please provide username/repo#number")
|
event = headers["X-GitHub-Event"]
|
||||||
if not number.isdigit():
|
event_action = None
|
||||||
raise utils.EventError("Issue number must be a number")
|
if "action" in data:
|
||||||
return org, repo, number
|
event_action = "%s/%s" % (event, data["action"])
|
||||||
|
return event, event_action
|
||||||
|
|
||||||
def _parse_issue(self, page, username, repository, number):
|
def webhook(self, data, headers):
|
||||||
repo = utils.irc.color("%s/%s" % (username, repository), COLOR_REPO)
|
github_event = headers["X-GitHub-Event"]
|
||||||
number = utils.irc.color("#%s" % number, COLOR_ID)
|
|
||||||
labels = [label["name"] for label in page.data["labels"]]
|
|
||||||
labels_str = ""
|
|
||||||
if labels:
|
|
||||||
labels_str = "[%s] " % ", ".join(labels)
|
|
||||||
|
|
||||||
url = self._short_url(page.data["html_url"])
|
|
||||||
|
|
||||||
state = page.data["state"]
|
|
||||||
if state == "open":
|
|
||||||
state = utils.irc.color("open", COLOR_NEUTRAL)
|
|
||||||
elif state == "closed":
|
|
||||||
state = utils.irc.color("closed", COLOR_NEGATIVE)
|
|
||||||
|
|
||||||
return "(%s issue%s, %s) %s %s%s" % (
|
|
||||||
repo, number, state, page.data["title"], labels_str, url)
|
|
||||||
def _get_issue(self, username, repository, number):
|
|
||||||
return utils.http.request(
|
|
||||||
API_ISSUE_URL % (username, repository, number),
|
|
||||||
json=True)
|
|
||||||
|
|
||||||
@utils.hook("received.command.ghissue", min_args=1)
|
|
||||||
def github_issue(self, event):
|
|
||||||
if event["target"].get_setting("github-hide-prefix", False):
|
|
||||||
event["stdout"].hide_prefix()
|
|
||||||
event["stderr"].hide_prefix()
|
|
||||||
|
|
||||||
username, repository, number = self._parse_ref(
|
|
||||||
event["target"], event["args_split"][0])
|
|
||||||
|
|
||||||
page = self._get_issue(username, repository, number)
|
|
||||||
if page and page.code == 200:
|
|
||||||
self._parse_issue(page, username, repository, number)
|
|
||||||
else:
|
|
||||||
event["stderr"].write("Could not find issue")
|
|
||||||
|
|
||||||
def _parse_pull(self, page, username, repository, number):
|
|
||||||
repo = utils.irc.color("%s/%s" % (username, repository), COLOR_REPO)
|
|
||||||
number = utils.irc.color("#%s" % number, COLOR_ID)
|
|
||||||
branch_from = page.data["head"]["label"]
|
|
||||||
branch_to = page.data["base"]["label"]
|
|
||||||
added = self._added(page.data["additions"])
|
|
||||||
removed = self._removed(page.data["deletions"])
|
|
||||||
url = self._short_url(page.data["html_url"])
|
|
||||||
|
|
||||||
state = page.data["state"]
|
|
||||||
if page.data["merged"]:
|
|
||||||
state = utils.irc.color("merged", COLOR_POSITIVE)
|
|
||||||
elif state == "open":
|
|
||||||
state = utils.irc.color("open", COLOR_NEUTRAL)
|
|
||||||
elif state == "closed":
|
|
||||||
state = utils.irc.color("closed", COLOR_NEGATIVE)
|
|
||||||
|
|
||||||
return "(%s PR%s, %s) %s → %s [%s/%s] %s %s" % (
|
|
||||||
repo, number, state, branch_from, branch_to, added, removed,
|
|
||||||
page.data["title"], url)
|
|
||||||
def _get_pull(self, username, repository, number):
|
|
||||||
return utils.http.request(
|
|
||||||
API_PULL_URL % (username, repository, number),
|
|
||||||
json=True)
|
|
||||||
@utils.hook("received.command.ghpull", min_args=1)
|
|
||||||
def github_pull(self, event):
|
|
||||||
if event["target"].get_setting("github-hide-prefix", False):
|
|
||||||
event["stdout"].hide_prefix()
|
|
||||||
event["stderr"].hide_prefix()
|
|
||||||
|
|
||||||
username, repository, number = self._parse_ref(
|
|
||||||
event["target"], event["args_split"][0])
|
|
||||||
page = self._get_pull(username, repository, number)
|
|
||||||
|
|
||||||
if page and page.code == 200:
|
|
||||||
self._parse_pull(page, username, repository, number)
|
|
||||||
else:
|
|
||||||
event["stderr"].write("Could not find pull request")
|
|
||||||
|
|
||||||
def _get_info(self, target, ref):
|
|
||||||
username, repository, number = self._parse_ref(target, ref)
|
|
||||||
page = self._get_issue(username, repository, number)
|
|
||||||
if page and page.code == 200:
|
|
||||||
if "pull_request" in page.data:
|
|
||||||
pull = self._get_pull(username, repository, number)
|
|
||||||
return self._parse_pull(pull, username, repository, number)
|
|
||||||
else:
|
|
||||||
return self._parse_issue(page, username, repository, number)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@utils.hook("received.command.gh", alias_of="github")
|
|
||||||
@utils.hook("received.command.github", min_args=1)
|
|
||||||
def github(self, event):
|
|
||||||
if event["target"].get_setting("github-hide-prefix", False):
|
|
||||||
event["stdout"].hide_prefix()
|
|
||||||
event["stderr"].hide_prefix()
|
|
||||||
result = self._get_info(event["target"], event["args_split"][0])
|
|
||||||
if not result == None:
|
|
||||||
event["stdout"].write(result)
|
|
||||||
else:
|
|
||||||
event["stderr"].write("Issue/PR not found")
|
|
||||||
|
|
||||||
def _cache_ref(self, ref):
|
|
||||||
return "auto-github-%s" % ref.lower()
|
|
||||||
def _auto_github_cooldown(self, channel, ref):
|
|
||||||
cooldown = channel.get_setting("auto-github-cooldown", None)
|
|
||||||
if not cooldown == None:
|
|
||||||
cache = self._cache_ref(ref)
|
|
||||||
if not self.bot.cache.has_item(cache):
|
|
||||||
self.bot.cache.temporary_cache(cache, cooldown)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@utils.hook("command.regex", ignore_action=False)
|
|
||||||
def url_regex(self, event):
|
|
||||||
"""
|
|
||||||
:command: github
|
|
||||||
:pattern: https?://github.com/([^/]+)/([^/]+)/(pull|issues)/(\d+)
|
|
||||||
"""
|
|
||||||
if event["target"].get_setting("auto-github", False):
|
|
||||||
event.eat()
|
|
||||||
ref = "%s/%s#%s" % (event["match"].group(1),
|
|
||||||
event["match"].group(2), event["match"].group(4))
|
|
||||||
if self._auto_github_cooldown(event["target"], ref):
|
|
||||||
try:
|
|
||||||
result = self._get_info(event["target"], ref)
|
|
||||||
except utils.EventError:
|
|
||||||
return
|
|
||||||
if result:
|
|
||||||
if event["target"].get_setting("github-hide-prefix", False):
|
|
||||||
event["stdout"].hide_prefix()
|
|
||||||
event["stdout"].write(result)
|
|
||||||
|
|
||||||
@utils.hook("command.regex", ignore_action=False)
|
|
||||||
def ref_regex(self, event):
|
|
||||||
"""
|
|
||||||
:command: github
|
|
||||||
:pattern: (?:\S+(?:\/\S+)?)?#\d+
|
|
||||||
"""
|
|
||||||
if event["target"].get_setting("auto-github", False):
|
|
||||||
event.eat()
|
|
||||||
ref = event["match"].group(0)
|
|
||||||
if self._auto_github_cooldown(event["target"], ref):
|
|
||||||
try:
|
|
||||||
result = self._get_info(event["target"],
|
|
||||||
event["match"].group(0))
|
|
||||||
except utils.EventError:
|
|
||||||
return
|
|
||||||
if result:
|
|
||||||
if event["target"].get_setting("github-hide-prefix", False):
|
|
||||||
event["stdout"].hide_prefix()
|
|
||||||
event["stdout"].write(result)
|
|
||||||
|
|
||||||
@utils.hook("received.command.ghwebhook", min_args=1, channel_only=True)
|
|
||||||
def github_webhook(self, event):
|
|
||||||
"""
|
|
||||||
:help: Add/remove/modify a github webhook
|
|
||||||
:require_mode: high
|
|
||||||
:require_access: github-webhook
|
|
||||||
:permission: githuboverride
|
|
||||||
:usage: list
|
|
||||||
:usage: add <hook>
|
|
||||||
:usage: remove <hook>
|
|
||||||
:usage: events <hook> [category [category ...]]
|
|
||||||
:usage: branches <hook> [branch [branch ...]]
|
|
||||||
"""
|
|
||||||
all_hooks = event["target"].get_setting("github-hooks", {})
|
|
||||||
hook_name = None
|
|
||||||
existing_hook = None
|
|
||||||
if len(event["args_split"]) > 1:
|
|
||||||
hook_name = event["args_split"][1]
|
|
||||||
for existing_hook_name in all_hooks.keys():
|
|
||||||
if existing_hook_name.lower() == hook_name.lower():
|
|
||||||
existing_hook = existing_hook_name
|
|
||||||
break
|
|
||||||
|
|
||||||
subcommand = event["args_split"][0].lower()
|
|
||||||
if subcommand == "list":
|
|
||||||
event["stdout"].write("Registered web hooks: %s" %
|
|
||||||
", ".join(all_hooks.keys()))
|
|
||||||
elif subcommand == "add":
|
|
||||||
if existing_hook:
|
|
||||||
event["stderr"].write("There's already a hook for %s" %
|
|
||||||
hook_name)
|
|
||||||
return
|
|
||||||
|
|
||||||
all_hooks[hook_name] = {
|
|
||||||
"events": DEFAULT_EVENT_CATEGORIES.copy(),
|
|
||||||
"branches": []
|
|
||||||
}
|
|
||||||
event["target"].set_setting("github-hooks", all_hooks)
|
|
||||||
event["stdout"].write("Added hook for %s" % hook_name)
|
|
||||||
elif subcommand == "remove":
|
|
||||||
if not existing_hook:
|
|
||||||
event["stderr"].write("No hook found for %s" % hook_name)
|
|
||||||
return
|
|
||||||
del all_hooks[existing_hook]
|
|
||||||
if all_hooks:
|
|
||||||
event["target"].set_setting("github-hooks", all_hooks)
|
|
||||||
else:
|
|
||||||
event["target"].del_setting("github-hooks")
|
|
||||||
event["stdout"].write("Removed hook for %s" % hook_name)
|
|
||||||
elif subcommand == "events":
|
|
||||||
if not existing_hook:
|
|
||||||
event["stderr"].write("No hook found for %s" % hook_name)
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(event["args_split"]) < 3:
|
|
||||||
event["stdout"].write("Events for hook %s: %s" %
|
|
||||||
(hook_name, " ".join(all_hooks[existing_hook]["events"])))
|
|
||||||
else:
|
|
||||||
new_events = [e.lower() for e in event["args_split"][2:]]
|
|
||||||
all_hooks[existing_hook]["events"] = new_events
|
|
||||||
event["target"].set_setting("github-hooks", all_hooks)
|
|
||||||
event["stdout"].write("Updated events for hook %s" % hook_name)
|
|
||||||
elif subcommand == "branches":
|
|
||||||
if not existing_hook:
|
|
||||||
event["stderr"].write("No hook found for %s" % hook_name)
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(event["args_split"]) < 3:
|
|
||||||
branches = ",".join(all_hooks[existing_hook]["branches"])
|
|
||||||
event["stdout"].write("Branches shown for hook %s: %s" %
|
|
||||||
(hook_name, branches))
|
|
||||||
else:
|
|
||||||
all_hooks[existing_hook]["branches"] = event["args_split"][2:]
|
|
||||||
event["target"].set_setting("github-hooks", all_hooks)
|
|
||||||
event["stdout"].write("Updated shown branches for hook %s" %
|
|
||||||
hook_name)
|
|
||||||
else:
|
|
||||||
event["stderr"].write("Unknown command '%s'" %
|
|
||||||
event["args_split"][0])
|
|
||||||
|
|
||||||
@utils.hook("api.post.github")
|
|
||||||
def webhook(self, event):
|
|
||||||
payload = event["data"].decode("utf8")
|
|
||||||
if event["headers"]["Content-Type"] == FORM_ENCODED:
|
|
||||||
payload = urllib.parse.unquote(urllib.parse.parse_qs(payload)[
|
|
||||||
"payload"][0])
|
|
||||||
data = json.loads(payload)
|
|
||||||
|
|
||||||
github_event = event["headers"]["X-GitHub-Event"]
|
|
||||||
|
|
||||||
full_name = None
|
full_name = None
|
||||||
repo_username = None
|
repo_username = None
|
||||||
|
@ -472,30 +200,7 @@ class Module(ModuleManager.BaseModule):
|
||||||
outputs = self.membership(organisation, data)
|
outputs = self.membership(organisation, data)
|
||||||
elif github_event == "watch":
|
elif github_event == "watch":
|
||||||
outputs = self.watch(data)
|
outputs = self.watch(data)
|
||||||
|
return outputs
|
||||||
|
|
||||||
if outputs:
|
|
||||||
for server, channel in targets:
|
|
||||||
source = full_name or organisation
|
|
||||||
hide_org = channel.get_setting(
|
|
||||||
"github-hide-organisation", False)
|
|
||||||
if repo_name and hide_org:
|
|
||||||
source = repo_name
|
|
||||||
|
|
||||||
for output in outputs:
|
|
||||||
output = "(%s) %s" % (
|
|
||||||
utils.irc.color(source, COLOR_REPO), output)
|
|
||||||
|
|
||||||
if channel.get_setting("github-prevent-highlight", False):
|
|
||||||
output = self._prevent_highlight(server, channel,
|
|
||||||
output)
|
|
||||||
|
|
||||||
self.events.on("send.stdout").call(target=channel,
|
|
||||||
module_name="Github", server=server, message=output,
|
|
||||||
hide_prefix=channel.get_setting(
|
|
||||||
"github-hide-prefix", False))
|
|
||||||
|
|
||||||
return {"state": "success", "deliveries": len(targets)}
|
|
||||||
|
|
||||||
def _prevent_highlight(self, server, channel, s):
|
def _prevent_highlight(self, server, channel, s):
|
||||||
for user in channel.users:
|
for user in channel.users:
|
||||||
|
@ -576,7 +281,6 @@ class Module(ModuleManager.BaseModule):
|
||||||
|
|
||||||
return outputs
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
def commit_comment(self, full_name, data):
|
def commit_comment(self, full_name, data):
|
||||||
action = data["action"]
|
action = data["action"]
|
||||||
commit = self._short_hash(data["comment"]["commit_id"])
|
commit = self._short_hash(data["comment"]["commit_id"])
|
Loading…
Reference in a new issue