integrations: restructure code and added better coverage.

This commit is contained in:
RhodeCode Admin 2023-08-03 20:51:50 +02:00
parent f935770cbf
commit d59ca21bfc
14 changed files with 646 additions and 372 deletions

View file

@ -18,7 +18,10 @@
import logging
from rhodecode.events.base import RhodeCodeIntegrationEvent
from rhodecode.events.base import (
RhodeCodeIntegrationEvent,
RhodecodeEvent
)
from rhodecode.events.base import ( # pragma: no cover
FtsBuild

View file

@ -17,6 +17,7 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import datetime
import typing
from zope.cachedescriptors.property import Lazy as LazyProperty
from pyramid.threadlocal import get_current_request

View file

@ -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)

View file

@ -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')

View 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/

View 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

View 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')

View file

@ -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 []

View file

@ -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::

View file

@ -32,23 +32,27 @@ def repo_push_event(backend, user_regular):
{'message': 'this is because 5b23c3532 broke stuff'},
{'message': 'last commit'},
]
commit_ids = backend.create_master_repo(commits).values()
repo = backend.create_repo()
r = backend.create_repo(commits)
commit_ids = list(backend.commit_ids.values())
repo_name = backend.repo_name
alias = backend.alias
scm_extras = AttributeDict({
'ip': '127.0.0.1',
'username': user_regular.username,
'user_id': user_regular.user_id,
'action': '',
'repository': repo.repo_name,
'scm': repo.scm_instance().alias,
'repository': repo_name,
'scm': alias,
'config': '',
'repo_store': '',
'server_url': 'http://example.com',
'server_url': 'http://httpbin:9090',
'make_lock': None,
'locked_by': [None],
'commit_ids': commit_ids,
})
return events.RepoPushEvent(repo_name=repo.repo_name,
return events.RepoPushEvent(repo_name=repo_name,
pushed_commit_ids=commit_ids,
extras=scm_extras)

View file

@ -1,4 +1,3 @@
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify

View file

@ -1,4 +1,3 @@
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
@ -18,11 +17,43 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
import mock
from mock import patch
from rhodecode import events
from rhodecode.integrations.types.handlers.slack import SlackDataHandler
from rhodecode.model.db import Session, Integration
from rhodecode.integrations.types.slack import SlackIntegrationType
from rhodecode.tests import GIT_REPO
@pytest.fixture()
def base_slack_data():
return {
"pullrequest": {
"url": "https://example.com/pr1",
"pull_request_id": "1",
"title": "started pr",
"status": "new",
"commits": ["1", "2"],
"shadow_url": "http://shadow-url",
},
"actor": {"username": "foo-user"},
"comment": {
"comment_id": 1,
"text": "test-comment",
"status": "approved",
"file": "text.py",
"line": "1",
"type": "note",
},
"push": {"branches": "", "commits": []},
"repo": {
"url": "https://example.com/repo1",
"repo_name": GIT_REPO,
"repo_type": "git",
},
}
@pytest.fixture()
@ -52,14 +83,66 @@ def slack_integration(request, app, slack_settings):
return integration
@pytest.fixture()
def slack_integration_empty(request, app, slack_settings):
slack_settings['events'] = []
integration = Integration()
integration.name = 'test slack integration'
integration.enabled = True
integration.integration_type = SlackIntegrationType.key
integration.settings = slack_settings
Session().add(integration)
Session().commit()
request.addfinalizer(lambda: Session().delete(integration))
return integration
def test_slack_push(slack_integration, repo_push_event):
with patch('rhodecode.integrations.types.slack.post_text_to_slack') as call:
events.trigger(repo_push_event)
assert 'pushed to' in call.call_args[0][1]
slack_integration.settings['events'] = []
Session().commit()
assert 'pushed to' in call.call_args[0][1].title
# specific commit was parsed and serialized
assert 'change that fixes #41' in call.call_args[0][1].text
def test_slack_push_no_events(slack_integration_empty, repo_push_event):
assert Integration.get(slack_integration_empty.integration_id).settings['events'] == []
with patch('rhodecode.integrations.types.slack.post_text_to_slack') as call:
events.trigger(repo_push_event)
assert not call.call_args
def test_slack_data_handler_wrong_event():
handler = SlackDataHandler()
data = {"actor": {"username": "foo-user"}}
with pytest.raises(ValueError):
handler(events.RhodecodeEvent(), data)
@pytest.mark.parametrize("event_type, args", [
(
events.PullRequestCommentEvent,
(mock.MagicMock(name="pull-request"), mock.MagicMock(name="comment")),
),
(
events.PullRequestCommentEditEvent,
(mock.MagicMock(name="pull-request"), mock.MagicMock(name="comment")),
),
(
events.PullRequestReviewEvent,
(mock.MagicMock(name="pull-request"), mock.MagicMock(name="status")),
),
(
events.RepoPushEvent,
(GIT_REPO, mock.MagicMock(name="pushed_commit_ids"), mock.MagicMock(name="extras")),
),
(events.PullRequestEvent, (mock.MagicMock(), )),
(events.RepoCreateEvent, (mock.MagicMock(), )),
])
def test_slack_data_handler(app, event_type: events.RhodecodeEvent, args, base_slack_data):
handler = SlackDataHandler()
handler(event_type(*args), base_slack_data)

View file

@ -18,10 +18,13 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
import mock
from mock import patch
from rhodecode import events
from rhodecode.lib.utils2 import AttributeDict
from rhodecode.integrations.types.webhook import WebhookDataHandler
from rhodecode.tests import GIT_REPO
@pytest.fixture()
@ -38,7 +41,33 @@ def base_data():
'actor': {
'username': 'actor_name',
'user_id': 1
}
},
"pullrequest": {
"url": "https://example.com/pr1",
"pull_request_id": "1",
"title": "started pr",
"status": "new",
"commits_uid": ["1", "2"],
"shadow_url": "http://shadow-url",
},
"comment": {
"comment_id": 1,
"comment_text": "test-comment",
"status": "approved",
"comment_f_path": "text.py",
"comment_line_no": "1",
"comment_type": "note",
},
"commit": {
"commit_id": "efefef",
"commit_branch": "master",
"commit_message": "changed foo"
},
"push": {
"branches": "",
"commits": []
},
}
@ -131,3 +160,37 @@ def test_webook_parse_url_for_push_event(
urls = handler(repo_push_event, base_data)
assert urls == [
(url, headers, base_data) for url in expected_urls]
@pytest.mark.parametrize("event_type, args", [
(
events.RepoPushEvent,
(GIT_REPO, mock.MagicMock(name="pushed_commit_ids"), mock.MagicMock(name="extras")),
),
(
events.RepoCreateEvent,
(GIT_REPO,),
),
(
events.RepoCommitCommentEvent,
(GIT_REPO, mock.MagicMock(name="commit"), mock.MagicMock(name="comment")),
),
(
events.RepoCommitCommentEditEvent,
(GIT_REPO, mock.MagicMock(name="commit"), mock.MagicMock(name="comment")),
),
(
events.PullRequestEvent,
(mock.MagicMock(), ),
),
])
def test_webhook_data_handler(app, event_type: events.RhodecodeEvent, args, base_data):
handler = WebhookDataHandler(
template_url='http://server.com/${branch}/${commit_id}',
headers={'exmaple-header': 'header-values'}
)
handler(event_type(*args), base_data)

View file

@ -331,7 +331,7 @@ class TestCommits(BackendTestMixin):
assert line_no == 1
assert commit_id == file_added_commit.raw_id
assert commit_loader() == file_added_commit
assert 'Foobar 3' in line
assert b'Foobar 3' in line
def test_get_file_annotate_does_not_exist(self):
file_added_commit = self.repo.get_commit(commit_idx=2)