integrations: restructure code and added better coverage.
This commit is contained in:
parent
f935770cbf
commit
d59ca21bfc
14 changed files with 646 additions and 372 deletions
|
|
@ -19,7 +19,7 @@ import sys
|
|||
import logging
|
||||
|
||||
from rhodecode.integrations.registry import IntegrationTypeRegistry
|
||||
from rhodecode.integrations.types import webhook, slack, hipchat, email, base
|
||||
from rhodecode.integrations.types import webhook, slack, email, base
|
||||
from rhodecode.lib.exc_tracking import store_exception
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -34,8 +34,6 @@ integration_type_registry.register_integration_type(
|
|||
webhook.WebhookIntegrationType)
|
||||
integration_type_registry.register_integration_type(
|
||||
slack.SlackIntegrationType)
|
||||
integration_type_registry.register_integration_type(
|
||||
hipchat.HipchatIntegrationType)
|
||||
integration_type_registry.register_integration_type(
|
||||
email.EmailIntegrationType)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,17 +20,18 @@ import colander
|
|||
import string
|
||||
import collections
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
from requests.packages.urllib3.util import Retry
|
||||
|
||||
from mako import exceptions
|
||||
|
||||
from rhodecode.lib.utils2 import safe_str
|
||||
from rhodecode.translation import _
|
||||
|
||||
from rhodecode.lib.str_utils import safe_str
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -238,157 +239,6 @@ class CommitParsingDataHandler(object):
|
|||
return branches_commits
|
||||
|
||||
|
||||
class WebhookDataHandler(CommitParsingDataHandler):
|
||||
name = 'webhook'
|
||||
|
||||
def __init__(self, template_url, headers):
|
||||
self.template_url = template_url
|
||||
self.headers = headers
|
||||
|
||||
def get_base_parsed_template(self, data):
|
||||
"""
|
||||
initially parses the passed in template with some common variables
|
||||
available on ALL calls
|
||||
"""
|
||||
# note: make sure to update the `WEBHOOK_URL_VARS` if this changes
|
||||
common_vars = {
|
||||
'repo_name': data['repo']['repo_name'],
|
||||
'repo_type': data['repo']['repo_type'],
|
||||
'repo_id': data['repo']['repo_id'],
|
||||
'repo_url': data['repo']['url'],
|
||||
'username': data['actor']['username'],
|
||||
'user_id': data['actor']['user_id'],
|
||||
'event_name': data['name']
|
||||
}
|
||||
|
||||
extra_vars = {}
|
||||
for extra_key, extra_val in data['repo']['extra_fields'].items():
|
||||
extra_vars[f'extra__{extra_key}'] = extra_val
|
||||
common_vars.update(extra_vars)
|
||||
|
||||
template_url = self.template_url.replace('${extra:', '${extra__')
|
||||
for k, v in common_vars.items():
|
||||
template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
|
||||
return template_url
|
||||
|
||||
def repo_push_event_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
url_calls = []
|
||||
|
||||
branches_commits = self.aggregate_branch_data(
|
||||
data['push']['branches'], data['push']['commits'])
|
||||
if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
|
||||
# call it multiple times, for each branch if used in variables
|
||||
for branch, commit_ids in branches_commits.items():
|
||||
branch_url = UrlTmpl(url).safe_substitute(branch=branch)
|
||||
|
||||
if '${branch_head}' in branch_url:
|
||||
# last commit in the aggregate is the head of the branch
|
||||
branch_head = commit_ids['branch_head']
|
||||
branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
|
||||
|
||||
# call further down for each commit if used
|
||||
if '${commit_id}' in branch_url:
|
||||
for commit_data in commit_ids['commits']:
|
||||
commit_id = commit_data['raw_id']
|
||||
commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
|
||||
# register per-commit call
|
||||
log.debug(
|
||||
'register %s call(%s) to url %s',
|
||||
self.name, event, commit_url)
|
||||
url_calls.append(
|
||||
(commit_url, self.headers, data))
|
||||
|
||||
else:
|
||||
# register per-branch call
|
||||
log.debug('register %s call(%s) to url %s',
|
||||
self.name, event, branch_url)
|
||||
url_calls.append((branch_url, self.headers, data))
|
||||
|
||||
else:
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
url_calls.append((url, self.headers, data))
|
||||
|
||||
return url_calls
|
||||
|
||||
def repo_commit_comment_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
comment_vars = [
|
||||
('commit_comment_id', data['comment']['comment_id']),
|
||||
('commit_comment_text', data['comment']['comment_text']),
|
||||
('commit_comment_type', data['comment']['comment_type']),
|
||||
|
||||
('commit_comment_f_path', data['comment']['comment_f_path']),
|
||||
('commit_comment_line_no', data['comment']['comment_line_no']),
|
||||
|
||||
('commit_comment_commit_id', data['commit']['commit_id']),
|
||||
('commit_comment_commit_branch', data['commit']['commit_branch']),
|
||||
('commit_comment_commit_message', data['commit']['commit_message']),
|
||||
]
|
||||
for k, v in comment_vars:
|
||||
url = UrlTmpl(url).safe_substitute(**{k: v})
|
||||
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def repo_commit_comment_edit_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
comment_vars = [
|
||||
('commit_comment_id', data['comment']['comment_id']),
|
||||
('commit_comment_text', data['comment']['comment_text']),
|
||||
('commit_comment_type', data['comment']['comment_type']),
|
||||
|
||||
('commit_comment_f_path', data['comment']['comment_f_path']),
|
||||
('commit_comment_line_no', data['comment']['comment_line_no']),
|
||||
|
||||
('commit_comment_commit_id', data['commit']['commit_id']),
|
||||
('commit_comment_commit_branch', data['commit']['commit_branch']),
|
||||
('commit_comment_commit_message', data['commit']['commit_message']),
|
||||
]
|
||||
for k, v in comment_vars:
|
||||
url = UrlTmpl(url).safe_substitute(**{k: v})
|
||||
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def repo_create_event_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def pull_request_event_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
pr_vars = [
|
||||
('pull_request_id', data['pullrequest']['pull_request_id']),
|
||||
('pull_request_title', data['pullrequest']['title']),
|
||||
('pull_request_url', data['pullrequest']['url']),
|
||||
('pull_request_shadow_url', data['pullrequest']['shadow_url']),
|
||||
('pull_request_commits_uid', data['pullrequest']['commits_uid']),
|
||||
]
|
||||
for k, v in pr_vars:
|
||||
url = UrlTmpl(url).safe_substitute(**{k: v})
|
||||
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def __call__(self, event, data):
|
||||
from rhodecode import events
|
||||
|
||||
if isinstance(event, events.RepoPushEvent):
|
||||
return self.repo_push_event_handler(event, data)
|
||||
elif isinstance(event, events.RepoCreateEvent):
|
||||
return self.repo_create_event_handler(event, data)
|
||||
elif isinstance(event, events.RepoCommitCommentEvent):
|
||||
return self.repo_commit_comment_handler(event, data)
|
||||
elif isinstance(event, events.RepoCommitCommentEditEvent):
|
||||
return self.repo_commit_comment_edit_handler(event, data)
|
||||
elif isinstance(event, events.PullRequestEvent):
|
||||
return self.pull_request_event_handler(event, data)
|
||||
else:
|
||||
raise ValueError(
|
||||
f'event type `{event.__class__}` has no handler defined')
|
||||
|
||||
|
||||
def get_auth(settings):
|
||||
from requests.auth import HTTPBasicAuth
|
||||
username = settings.get('username')
|
||||
|
|
|
|||
17
rhodecode/integrations/types/handlers/__init__.py
Normal file
17
rhodecode/integrations/types/handlers/__init__.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (C) 2012-2023 RhodeCode GmbH
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License, version 3
|
||||
# (only), as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
242
rhodecode/integrations/types/handlers/slack.py
Normal file
242
rhodecode/integrations/types/handlers/slack.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
# Copyright (C) 2012-2023 RhodeCode GmbH
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License, version 3
|
||||
# (only), as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
import re
|
||||
import textwrap
|
||||
import dataclasses
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from mako.template import Template
|
||||
|
||||
from rhodecode import events
|
||||
from rhodecode.integrations.types.base import CommitParsingDataHandler, render_with_traceback
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SlackData:
|
||||
title: str
|
||||
text: str
|
||||
fields: list[dict] | None = None
|
||||
overrides: dict | None = None
|
||||
|
||||
|
||||
def html_to_slack_links(message):
|
||||
return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(r'<\1|\2>', message)
|
||||
|
||||
|
||||
REPO_PUSH_TEMPLATE = Template('''
|
||||
<%
|
||||
def branch_text(branch):
|
||||
if branch:
|
||||
return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
|
||||
else:
|
||||
## case for SVN no branch push...
|
||||
return 'to trunk'
|
||||
%> \
|
||||
|
||||
% for branch, branch_commits in branches_commits.items():
|
||||
${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
|
||||
% for commit in branch_commits['commits']:
|
||||
`<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
|
||||
% endfor
|
||||
% endfor
|
||||
''')
|
||||
|
||||
|
||||
class SlackDataHandler(CommitParsingDataHandler):
|
||||
name = 'slack'
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __call__(self, event: events.RhodecodeEvent, data):
|
||||
if not isinstance(event, events.RhodecodeEvent):
|
||||
raise TypeError(f"event {event} is not subtype of events.RhodecodeEvent")
|
||||
|
||||
actor = data["actor"]["username"]
|
||||
default_title = f'*{actor}* caused a *{event.name}* event'
|
||||
default_text = f'*{actor}* caused a *{event.name}* event'
|
||||
|
||||
default_slack_data = SlackData(title=default_title, text=default_text)
|
||||
|
||||
if isinstance(event, events.PullRequestCommentEvent):
|
||||
return self.format_pull_request_comment_event(
|
||||
event, data, default_slack_data
|
||||
)
|
||||
elif isinstance(event, events.PullRequestCommentEditEvent):
|
||||
return self.format_pull_request_comment_event(
|
||||
event, data, default_slack_data
|
||||
)
|
||||
elif isinstance(event, events.PullRequestReviewEvent):
|
||||
return self.format_pull_request_review_event(event, data, default_slack_data)
|
||||
elif isinstance(event, events.PullRequestEvent):
|
||||
return self.format_pull_request_event(event, data, default_slack_data)
|
||||
elif isinstance(event, events.RepoPushEvent):
|
||||
return self.format_repo_push_event(event, data, default_slack_data)
|
||||
elif isinstance(event, events.RepoCreateEvent):
|
||||
return self.format_repo_create_event(event, data, default_slack_data)
|
||||
else:
|
||||
raise ValueError(
|
||||
f'event type `{event.__class__}` has no handler defined')
|
||||
|
||||
def format_pull_request_comment_event(self, event, data, slack_data):
|
||||
comment_text = data['comment']['text']
|
||||
if len(comment_text) > 200:
|
||||
comment_text = '<{comment_url}|{comment_text}...>'.format(
|
||||
comment_text=comment_text[:200],
|
||||
comment_url=data['comment']['url'],
|
||||
)
|
||||
|
||||
fields = None
|
||||
overrides = None
|
||||
status_text = None
|
||||
|
||||
if data['comment']['status']:
|
||||
status_color = {
|
||||
'approved': '#0ac878',
|
||||
'rejected': '#e85e4d'}.get(data['comment']['status'])
|
||||
|
||||
if status_color:
|
||||
overrides = {"color": status_color}
|
||||
|
||||
status_text = data['comment']['status']
|
||||
|
||||
if data['comment']['file']:
|
||||
fields = [
|
||||
{
|
||||
"title": "file",
|
||||
"value": data['comment']['file']
|
||||
},
|
||||
{
|
||||
"title": "line",
|
||||
"value": data['comment']['line']
|
||||
}
|
||||
]
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
|
||||
'''))
|
||||
title = render_with_traceback(
|
||||
template, data=data, comment=event.comment)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*pull request title*: ${pr_title}
|
||||
% if status_text:
|
||||
*submitted status*: `${status_text}`
|
||||
% endif
|
||||
>>> ${comment_text}
|
||||
'''))
|
||||
text = render_with_traceback(
|
||||
template,
|
||||
comment_text=comment_text,
|
||||
pr_title=data['pullrequest']['title'],
|
||||
status_text=status_text)
|
||||
|
||||
slack_data.title = title
|
||||
slack_data.text = text
|
||||
slack_data.fields = fields
|
||||
slack_data.overrides = overrides
|
||||
|
||||
return slack_data
|
||||
|
||||
def format_pull_request_review_event(self, event, data, slack_data) -> SlackData:
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
|
||||
'''))
|
||||
title = render_with_traceback(template, data=data)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*pull request title*: ${pr_title}
|
||||
'''))
|
||||
text = render_with_traceback(
|
||||
template,
|
||||
pr_title=data['pullrequest']['title'])
|
||||
|
||||
slack_data.title = title
|
||||
slack_data.text = text
|
||||
|
||||
return slack_data
|
||||
|
||||
def format_pull_request_event(self, event, data, slack_data) -> SlackData:
|
||||
action = {
|
||||
events.PullRequestCloseEvent: 'closed',
|
||||
events.PullRequestMergeEvent: 'merged',
|
||||
events.PullRequestUpdateEvent: 'updated',
|
||||
events.PullRequestCreateEvent: 'created',
|
||||
}.get(event.__class__, str(event.__class__))
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
|
||||
'''))
|
||||
title = render_with_traceback(template, data=data, action=action)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*pull request title*: ${pr_title}
|
||||
%if data['pullrequest']['commits']:
|
||||
*commits*: ${len(data['pullrequest']['commits'])}
|
||||
%endif
|
||||
'''))
|
||||
text = render_with_traceback(
|
||||
template,
|
||||
pr_title=data['pullrequest']['title'],
|
||||
data=data)
|
||||
|
||||
slack_data.title = title
|
||||
slack_data.text = text
|
||||
|
||||
return slack_data
|
||||
|
||||
def format_repo_push_event(self, event, data, slack_data) -> SlackData:
|
||||
branches_commits = self.aggregate_branch_data(
|
||||
data['push']['branches'], data['push']['commits'])
|
||||
|
||||
template = Template(r'''
|
||||
*${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
|
||||
''')
|
||||
title = render_with_traceback(template, data=data)
|
||||
|
||||
text = render_with_traceback(
|
||||
REPO_PUSH_TEMPLATE,
|
||||
data=data,
|
||||
branches_commits=branches_commits,
|
||||
html_to_slack_links=html_to_slack_links,
|
||||
)
|
||||
|
||||
slack_data.title = title
|
||||
slack_data.text = text
|
||||
|
||||
return slack_data
|
||||
|
||||
def format_repo_create_event(self, event, data, slack_data) -> SlackData:
|
||||
template = Template(r'''
|
||||
*${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
|
||||
''')
|
||||
title = render_with_traceback(template, data=data)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
repo_url: ${data['repo']['url']}
|
||||
repo_type: ${data['repo']['repo_type']}
|
||||
'''))
|
||||
text = render_with_traceback(template, data=data)
|
||||
|
||||
slack_data.title = title
|
||||
slack_data.text = text
|
||||
|
||||
return slack_data
|
||||
175
rhodecode/integrations/types/handlers/webhook.py
Normal file
175
rhodecode/integrations/types/handlers/webhook.py
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Copyright (C) 2012-2023 RhodeCode GmbH
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License, version 3
|
||||
# (only), as published by the Free Software Foundation.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This program is dual-licensed. If you wish to learn more about the
|
||||
# RhodeCode Enterprise Edition, including its added features, Support services,
|
||||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
import logging
|
||||
|
||||
from rhodecode import events
|
||||
from rhodecode.integrations.types.base import CommitParsingDataHandler, UrlTmpl
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebhookDataHandler(CommitParsingDataHandler):
|
||||
name = 'webhook'
|
||||
|
||||
def __init__(self, template_url, headers):
|
||||
self.template_url = template_url
|
||||
self.headers = headers
|
||||
|
||||
def get_base_parsed_template(self, data):
|
||||
"""
|
||||
initially parses the passed in template with some common variables
|
||||
available on ALL calls
|
||||
"""
|
||||
# note: make sure to update the `WEBHOOK_URL_VARS` if this changes
|
||||
common_vars = {
|
||||
'repo_name': data['repo']['repo_name'],
|
||||
'repo_type': data['repo']['repo_type'],
|
||||
'repo_id': data['repo']['repo_id'],
|
||||
'repo_url': data['repo']['url'],
|
||||
'username': data['actor']['username'],
|
||||
'user_id': data['actor']['user_id'],
|
||||
'event_name': data['name']
|
||||
}
|
||||
|
||||
extra_vars = {}
|
||||
for extra_key, extra_val in data['repo']['extra_fields'].items():
|
||||
extra_vars[f'extra__{extra_key}'] = extra_val
|
||||
common_vars.update(extra_vars)
|
||||
|
||||
template_url = self.template_url.replace('${extra:', '${extra__')
|
||||
for k, v in common_vars.items():
|
||||
template_url = UrlTmpl(template_url).safe_substitute(**{k: v})
|
||||
return template_url
|
||||
|
||||
def repo_push_event_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
url_calls = []
|
||||
|
||||
branches_commits = self.aggregate_branch_data(
|
||||
data['push']['branches'], data['push']['commits'])
|
||||
if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url:
|
||||
# call it multiple times, for each branch if used in variables
|
||||
for branch, commit_ids in branches_commits.items():
|
||||
branch_url = UrlTmpl(url).safe_substitute(branch=branch)
|
||||
|
||||
if '${branch_head}' in branch_url:
|
||||
# last commit in the aggregate is the head of the branch
|
||||
branch_head = commit_ids['branch_head']
|
||||
branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head)
|
||||
|
||||
# call further down for each commit if used
|
||||
if '${commit_id}' in branch_url:
|
||||
for commit_data in commit_ids['commits']:
|
||||
commit_id = commit_data['raw_id']
|
||||
commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id)
|
||||
# register per-commit call
|
||||
log.debug(
|
||||
'register %s call(%s) to url %s',
|
||||
self.name, event, commit_url)
|
||||
url_calls.append(
|
||||
(commit_url, self.headers, data))
|
||||
|
||||
else:
|
||||
# register per-branch call
|
||||
log.debug('register %s call(%s) to url %s',
|
||||
self.name, event, branch_url)
|
||||
url_calls.append((branch_url, self.headers, data))
|
||||
|
||||
else:
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
url_calls.append((url, self.headers, data))
|
||||
|
||||
return url_calls
|
||||
|
||||
def repo_commit_comment_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
comment_vars = [
|
||||
('commit_comment_id', data['comment']['comment_id']),
|
||||
('commit_comment_text', data['comment']['comment_text']),
|
||||
('commit_comment_type', data['comment']['comment_type']),
|
||||
|
||||
('commit_comment_f_path', data['comment']['comment_f_path']),
|
||||
('commit_comment_line_no', data['comment']['comment_line_no']),
|
||||
|
||||
('commit_comment_commit_id', data['commit']['commit_id']),
|
||||
('commit_comment_commit_branch', data['commit']['commit_branch']),
|
||||
('commit_comment_commit_message', data['commit']['commit_message']),
|
||||
]
|
||||
for k, v in comment_vars:
|
||||
url = UrlTmpl(url).safe_substitute(**{k: v})
|
||||
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def repo_commit_comment_edit_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
comment_vars = [
|
||||
('commit_comment_id', data['comment']['comment_id']),
|
||||
('commit_comment_text', data['comment']['comment_text']),
|
||||
('commit_comment_type', data['comment']['comment_type']),
|
||||
|
||||
('commit_comment_f_path', data['comment']['comment_f_path']),
|
||||
('commit_comment_line_no', data['comment']['comment_line_no']),
|
||||
|
||||
('commit_comment_commit_id', data['commit']['commit_id']),
|
||||
('commit_comment_commit_branch', data['commit']['commit_branch']),
|
||||
('commit_comment_commit_message', data['commit']['commit_message']),
|
||||
]
|
||||
for k, v in comment_vars:
|
||||
url = UrlTmpl(url).safe_substitute(**{k: v})
|
||||
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def repo_create_event_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def pull_request_event_handler(self, event, data):
|
||||
url = self.get_base_parsed_template(data)
|
||||
log.debug('register %s call(%s) to url %s', self.name, event, url)
|
||||
pr_vars = [
|
||||
('pull_request_id', data['pullrequest']['pull_request_id']),
|
||||
('pull_request_title', data['pullrequest']['title']),
|
||||
('pull_request_url', data['pullrequest']['url']),
|
||||
('pull_request_shadow_url', data['pullrequest']['shadow_url']),
|
||||
('pull_request_commits_uid', data['pullrequest']['commits_uid']),
|
||||
]
|
||||
for k, v in pr_vars:
|
||||
url = UrlTmpl(url).safe_substitute(**{k: v})
|
||||
|
||||
return [(url, self.headers, data)]
|
||||
|
||||
def __call__(self, event, data):
|
||||
|
||||
if isinstance(event, events.RepoPushEvent):
|
||||
return self.repo_push_event_handler(event, data)
|
||||
elif isinstance(event, events.RepoCreateEvent):
|
||||
return self.repo_create_event_handler(event, data)
|
||||
elif isinstance(event, events.RepoCommitCommentEvent):
|
||||
return self.repo_commit_comment_handler(event, data)
|
||||
elif isinstance(event, events.RepoCommitCommentEditEvent):
|
||||
return self.repo_commit_comment_edit_handler(event, data)
|
||||
elif isinstance(event, events.PullRequestEvent):
|
||||
return self.pull_request_event_handler(event, data)
|
||||
else:
|
||||
raise ValueError(
|
||||
f'event type `{event.__class__}` has no handler defined')
|
||||
|
|
@ -17,15 +17,13 @@
|
|||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
|
||||
import re
|
||||
|
||||
import time
|
||||
import textwrap
|
||||
import logging
|
||||
|
||||
import deform
|
||||
import requests
|
||||
import deform # noqa
|
||||
import deform.widget
|
||||
import colander
|
||||
from mako.template import Template
|
||||
|
||||
from rhodecode import events
|
||||
from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
|
||||
|
|
@ -34,36 +32,16 @@ from rhodecode.lib import helpers as h
|
|||
from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
|
||||
from rhodecode.lib.colander_utils import strip_whitespace
|
||||
from rhodecode.integrations.types.base import (
|
||||
IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
|
||||
requests_retry_call)
|
||||
IntegrationTypeBase,
|
||||
requests_retry_call,
|
||||
)
|
||||
|
||||
from rhodecode.integrations.types.handlers.slack import SlackDataHandler, SlackData
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def html_to_slack_links(message):
|
||||
return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
|
||||
r'<\1|\2>', message)
|
||||
|
||||
|
||||
REPO_PUSH_TEMPLATE = Template('''
|
||||
<%
|
||||
def branch_text(branch):
|
||||
if branch:
|
||||
return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
|
||||
else:
|
||||
## case for SVN no branch push...
|
||||
return 'to trunk'
|
||||
%> \
|
||||
|
||||
% for branch, branch_commits in branches_commits.items():
|
||||
${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
|
||||
% for commit in branch_commits['commits']:
|
||||
`<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
|
||||
% endfor
|
||||
% endfor
|
||||
''')
|
||||
|
||||
|
||||
class SlackSettingsSchema(colander.Schema):
|
||||
service = colander.SchemaNode(
|
||||
colander.String(),
|
||||
|
|
@ -111,7 +89,7 @@ class SlackSettingsSchema(colander.Schema):
|
|||
)
|
||||
|
||||
|
||||
class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
|
||||
class SlackIntegrationType(IntegrationTypeBase):
|
||||
key = 'slack'
|
||||
display_name = _('Slack')
|
||||
description = _('Send events such as repo pushes and pull requests to '
|
||||
|
|
@ -132,45 +110,6 @@ class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
|
|||
events.RepoCreateEvent,
|
||||
]
|
||||
|
||||
def send_event(self, event):
|
||||
log.debug('handling event %s with integration %s', event.name, self)
|
||||
|
||||
if event.__class__ not in self.valid_events:
|
||||
log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
|
||||
return
|
||||
|
||||
if not self.event_enabled(event):
|
||||
return
|
||||
|
||||
data = event.as_dict()
|
||||
|
||||
# defaults
|
||||
title = '*%s* caused a *%s* event' % (
|
||||
data['actor']['username'], event.name)
|
||||
text = '*%s* caused a *%s* event' % (
|
||||
data['actor']['username'], event.name)
|
||||
fields = None
|
||||
overrides = None
|
||||
|
||||
if isinstance(event, events.PullRequestCommentEvent):
|
||||
(title, text, fields, overrides) \
|
||||
= self.format_pull_request_comment_event(event, data)
|
||||
elif isinstance(event, events.PullRequestCommentEditEvent):
|
||||
(title, text, fields, overrides) \
|
||||
= self.format_pull_request_comment_event(event, data)
|
||||
elif isinstance(event, events.PullRequestReviewEvent):
|
||||
title, text = self.format_pull_request_review_event(event, data)
|
||||
elif isinstance(event, events.PullRequestEvent):
|
||||
title, text = self.format_pull_request_event(event, data)
|
||||
elif isinstance(event, events.RepoPushEvent):
|
||||
title, text = self.format_repo_push_event(data)
|
||||
elif isinstance(event, events.RepoCreateEvent):
|
||||
title, text = self.format_repo_create_event(data)
|
||||
else:
|
||||
log.error('unhandled event type: %r', event)
|
||||
|
||||
run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
|
||||
|
||||
def settings_schema(self):
|
||||
schema = SlackSettingsSchema()
|
||||
schema.add(colander.SchemaNode(
|
||||
|
|
@ -186,137 +125,31 @@ class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
|
|||
|
||||
return schema
|
||||
|
||||
def format_pull_request_comment_event(self, event, data):
|
||||
comment_text = data['comment']['text']
|
||||
if len(comment_text) > 200:
|
||||
comment_text = '<{comment_url}|{comment_text}...>'.format(
|
||||
comment_text=comment_text[:200],
|
||||
comment_url=data['comment']['url'],
|
||||
)
|
||||
def send_event(self, event):
|
||||
log.debug('handling event %s with integration %s', event.name, self)
|
||||
|
||||
fields = None
|
||||
overrides = None
|
||||
status_text = None
|
||||
if event.__class__ not in self.valid_events:
|
||||
log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
|
||||
return
|
||||
|
||||
if data['comment']['status']:
|
||||
status_color = {
|
||||
'approved': '#0ac878',
|
||||
'rejected': '#e85e4d'}.get(data['comment']['status'])
|
||||
if not self.event_enabled(event):
|
||||
return
|
||||
|
||||
if status_color:
|
||||
overrides = {"color": status_color}
|
||||
data = event.as_dict()
|
||||
|
||||
status_text = data['comment']['status']
|
||||
|
||||
if data['comment']['file']:
|
||||
fields = [
|
||||
{
|
||||
"title": "file",
|
||||
"value": data['comment']['file']
|
||||
},
|
||||
{
|
||||
"title": "line",
|
||||
"value": data['comment']['line']
|
||||
}
|
||||
]
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
|
||||
'''))
|
||||
title = render_with_traceback(
|
||||
template, data=data, comment=event.comment)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*pull request title*: ${pr_title}
|
||||
% if status_text:
|
||||
*submitted status*: `${status_text}`
|
||||
% endif
|
||||
>>> ${comment_text}
|
||||
'''))
|
||||
text = render_with_traceback(
|
||||
template,
|
||||
comment_text=comment_text,
|
||||
pr_title=data['pullrequest']['title'],
|
||||
status_text=status_text)
|
||||
|
||||
return title, text, fields, overrides
|
||||
|
||||
def format_pull_request_review_event(self, event, data):
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
|
||||
'''))
|
||||
title = render_with_traceback(template, data=data)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*pull request title*: ${pr_title}
|
||||
'''))
|
||||
text = render_with_traceback(
|
||||
template,
|
||||
pr_title=data['pullrequest']['title'])
|
||||
|
||||
return title, text
|
||||
|
||||
def format_pull_request_event(self, event, data):
|
||||
action = {
|
||||
events.PullRequestCloseEvent: 'closed',
|
||||
events.PullRequestMergeEvent: 'merged',
|
||||
events.PullRequestUpdateEvent: 'updated',
|
||||
events.PullRequestCreateEvent: 'created',
|
||||
}.get(event.__class__, str(event.__class__))
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
|
||||
'''))
|
||||
title = render_with_traceback(template, data=data, action=action)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
*pull request title*: ${pr_title}
|
||||
%if data['pullrequest']['commits']:
|
||||
*commits*: ${len(data['pullrequest']['commits'])}
|
||||
%endif
|
||||
'''))
|
||||
text = render_with_traceback(
|
||||
template,
|
||||
pr_title=data['pullrequest']['title'],
|
||||
data=data)
|
||||
|
||||
return title, text
|
||||
|
||||
def format_repo_push_event(self, data):
|
||||
branches_commits = self.aggregate_branch_data(
|
||||
data['push']['branches'], data['push']['commits'])
|
||||
|
||||
template = Template(r'''
|
||||
*${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
|
||||
''')
|
||||
title = render_with_traceback(template, data=data)
|
||||
|
||||
text = render_with_traceback(
|
||||
REPO_PUSH_TEMPLATE,
|
||||
data=data,
|
||||
branches_commits=branches_commits,
|
||||
html_to_slack_links=html_to_slack_links,
|
||||
)
|
||||
|
||||
return title, text
|
||||
|
||||
def format_repo_create_event(self, data):
|
||||
template = Template(r'''
|
||||
*${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
|
||||
''')
|
||||
title = render_with_traceback(template, data=data)
|
||||
|
||||
template = Template(textwrap.dedent(r'''
|
||||
repo_url: ${data['repo']['url']}
|
||||
repo_type: ${data['repo']['repo_type']}
|
||||
'''))
|
||||
text = render_with_traceback(template, data=data)
|
||||
|
||||
return title, text
|
||||
handler = SlackDataHandler()
|
||||
slack_data = handler(event, data)
|
||||
# title, text, fields, overrides
|
||||
run_task(post_text_to_slack, self.settings, slack_data)
|
||||
|
||||
|
||||
@async_task(ignore_result=True, base=RequestContextTask)
|
||||
def post_text_to_slack(settings, title, text, fields=None, overrides=None):
|
||||
def post_text_to_slack(settings, slack_data: SlackData):
|
||||
title = slack_data.title
|
||||
text = slack_data.text
|
||||
fields = slack_data.fields
|
||||
overrides = slack_data.overrides
|
||||
|
||||
log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
|
||||
|
||||
fields = fields or []
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
||||
|
||||
|
||||
|
||||
import deform # noqa
|
||||
import deform.widget
|
||||
import logging
|
||||
import colander
|
||||
|
|
@ -28,8 +28,14 @@ from rhodecode.lib.colander_utils import strip_whitespace
|
|||
from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc
|
||||
from rhodecode.translation import _
|
||||
from rhodecode.integrations.types.base import (
|
||||
IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
|
||||
WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
|
||||
IntegrationTypeBase,
|
||||
get_auth,
|
||||
get_web_token,
|
||||
get_url_vars,
|
||||
WEBHOOK_URL_VARS,
|
||||
requests_retry_call,
|
||||
)
|
||||
from rhodecode.integrations.types.handlers.webhook import WebhookDataHandler
|
||||
from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
|
||||
from rhodecode.model.validation_schema import widgets
|
||||
|
||||
|
|
@ -57,7 +63,7 @@ class WebhookSettingsSchema(colander.Schema):
|
|||
widget=widgets.CodeMirrorWidget(
|
||||
help_block_collapsable_name='Show url variables',
|
||||
help_block_collapsable=(
|
||||
'E.g http://my-serv.com/trigger_job/${{event_name}}'
|
||||
'E.g https://my-serv.com/trigger_job/${{event_name}}'
|
||||
'?PR_ID=${{pull_request_id}}'
|
||||
'\nFull list of vars:\n{}'.format(URL_VARS)),
|
||||
codemirror_mode='text',
|
||||
|
|
@ -189,11 +195,11 @@ class WebhookIntegrationType(IntegrationTypeBase):
|
|||
url_calls = handler(event, data)
|
||||
log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls])
|
||||
|
||||
run_task(post_to_webhook, url_calls, self.settings)
|
||||
run_task(post_to_webhook, self.settings, url_calls)
|
||||
|
||||
|
||||
@async_task(ignore_result=True, base=RequestContextTask)
|
||||
def post_to_webhook(url_calls, settings):
|
||||
def post_to_webhook(settings, url_calls):
|
||||
"""
|
||||
Example data::
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue