diff --git a/rhodecode/events/__init__.py b/rhodecode/events/__init__.py
--- a/rhodecode/events/__init__.py
+++ b/rhodecode/events/__init__.py
@@ -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
diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py
--- a/rhodecode/events/base.py
+++ b/rhodecode/events/base.py
@@ -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
diff --git a/rhodecode/integrations/__init__.py b/rhodecode/integrations/__init__.py
--- a/rhodecode/integrations/__init__.py
+++ b/rhodecode/integrations/__init__.py
@@ -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__)
@@ -35,8 +35,6 @@ integration_type_registry.register_integ
integration_type_registry.register_integration_type(
slack.SlackIntegrationType)
integration_type_registry.register_integration_type(
- hipchat.HipchatIntegrationType)
-integration_type_registry.register_integration_type(
email.EmailIntegrationType)
diff --git a/rhodecode/integrations/types/base.py b/rhodecode/integrations/types/base.py
--- a/rhodecode/integrations/types/base.py
+++ b/rhodecode/integrations/types/base.py
@@ -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')
diff --git a/rhodecode/integrations/types/handlers/__init__.py b/rhodecode/integrations/types/handlers/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/integrations/types/handlers/__init__.py
@@ -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 .
+#
+# 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/
diff --git a/rhodecode/integrations/types/handlers/slack.py b/rhodecode/integrations/types/handlers/slack.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/integrations/types/handlers/slack.py
@@ -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 .
+#
+# 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'(.+?)').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
\ No newline at end of file
diff --git a/rhodecode/integrations/types/handlers/webhook.py b/rhodecode/integrations/types/handlers/webhook.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/integrations/types/handlers/webhook.py
@@ -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 .
+#
+# 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')
diff --git a/rhodecode/integrations/types/slack.py b/rhodecode/integrations/types/slack.py
--- a/rhodecode/integrations/types/slack.py
+++ b/rhodecode/integrations/types/slack.py
@@ -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,34 +32,14 @@ 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)
-
-log = logging.getLogger(__name__)
+ IntegrationTypeBase,
+ requests_retry_call,
+)
-
-def html_to_slack_links(message):
- return re.compile(r'(.+?)').sub(
- r'<\1|\2>', message)
+from rhodecode.integrations.types.handlers.slack import SlackDataHandler, SlackData
-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
-''')
+log = logging.getLogger(__name__)
class SlackSettingsSchema(colander.Schema):
@@ -111,7 +89,7 @@ class SlackSettingsSchema(colander.Schem
)
-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(IntegrationTy
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(IntegrationTy
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'],
- )
-
- 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']
+ def send_event(self, event):
+ log.debug('handling event %s with integration %s', event.name, self)
- 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)
+ if event.__class__ not in self.valid_events:
+ log.debug('event %r not present in valid event list (%s)', event, self.valid_events)
+ return
- template = Template(textwrap.dedent(r'''
- *pull request title*: ${pr_title}
- '''))
- text = render_with_traceback(
- template,
- pr_title=data['pullrequest']['title'])
-
- return title, text
+ if not self.event_enabled(event):
+ return
- 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)
+ data = event.as_dict()
- 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 []
diff --git a/rhodecode/integrations/types/webhook.py b/rhodecode/integrations/types/webhook.py
--- a/rhodecode/integrations/types/webhook.py
+++ b/rhodecode/integrations/types/webhook.py
@@ -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
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.Sch
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(Integration
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::
diff --git a/rhodecode/tests/integrations/conftest.py b/rhodecode/tests/integrations/conftest.py
--- a/rhodecode/tests/integrations/conftest.py
+++ b/rhodecode/tests/integrations/conftest.py
@@ -32,23 +32,27 @@ def repo_push_event(backend, user_regula
{'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)
diff --git a/rhodecode/tests/integrations/test_integration.py b/rhodecode/tests/integrations/test_integration.py
--- a/rhodecode/tests/integrations/test_integration.py
+++ b/rhodecode/tests/integrations/test_integration.py
@@ -1,4 +1,3 @@
-
# Copyright (C) 2010-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
diff --git a/rhodecode/tests/integrations/test_slack.py b/rhodecode/tests/integrations/test_slack.py
--- a/rhodecode/tests/integrations/test_slack.py
+++ b/rhodecode/tests/integrations/test_slack.py
@@ -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, slac
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]
+
+ 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
- slack_integration.settings['events'] = []
- Session().commit()
+
+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)
diff --git a/rhodecode/tests/integrations/test_webhook.py b/rhodecode/tests/integrations/test_webhook.py
--- a/rhodecode/tests/integrations/test_webhook.py
+++ b/rhodecode/tests/integrations/test_webhook.py
@@ -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)
+
+
+
+
diff --git a/rhodecode/tests/vcs/test_commits.py b/rhodecode/tests/vcs/test_commits.py
--- a/rhodecode/tests/vcs/test_commits.py
+++ b/rhodecode/tests/vcs/test_commits.py
@@ -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)