Show More
@@ -0,0 +1,17 b'' | |||||
|
1 | # Copyright (C) 2012-2023 RhodeCode GmbH | |||
|
2 | # | |||
|
3 | # This program is free software: you can redistribute it and/or modify | |||
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
5 | # (only), as published by the Free Software Foundation. | |||
|
6 | # | |||
|
7 | # This program is distributed in the hope that it will be useful, | |||
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
10 | # GNU General Public License for more details. | |||
|
11 | # | |||
|
12 | # You should have received a copy of the GNU Affero General Public License | |||
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
14 | # | |||
|
15 | # This program is dual-licensed. If you wish to learn more about the | |||
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
@@ -0,0 +1,242 b'' | |||||
|
1 | # Copyright (C) 2012-2023 RhodeCode GmbH | |||
|
2 | # | |||
|
3 | # This program is free software: you can redistribute it and/or modify | |||
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
5 | # (only), as published by the Free Software Foundation. | |||
|
6 | # | |||
|
7 | # This program is distributed in the hope that it will be useful, | |||
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
10 | # GNU General Public License for more details. | |||
|
11 | # | |||
|
12 | # You should have received a copy of the GNU Affero General Public License | |||
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
14 | # | |||
|
15 | # This program is dual-licensed. If you wish to learn more about the | |||
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
18 | import re | |||
|
19 | import textwrap | |||
|
20 | import dataclasses | |||
|
21 | import logging | |||
|
22 | import typing | |||
|
23 | ||||
|
24 | from mako.template import Template | |||
|
25 | ||||
|
26 | from rhodecode import events | |||
|
27 | from rhodecode.integrations.types.base import CommitParsingDataHandler, render_with_traceback | |||
|
28 | ||||
|
29 | log = logging.getLogger(__name__) | |||
|
30 | ||||
|
31 | ||||
|
32 | @dataclasses.dataclass | |||
|
33 | class SlackData: | |||
|
34 | title: str | |||
|
35 | text: str | |||
|
36 | fields: list[dict] | None = None | |||
|
37 | overrides: dict | None = None | |||
|
38 | ||||
|
39 | ||||
|
40 | def html_to_slack_links(message): | |||
|
41 | return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(r'<\1|\2>', message) | |||
|
42 | ||||
|
43 | ||||
|
44 | REPO_PUSH_TEMPLATE = Template(''' | |||
|
45 | <% | |||
|
46 | def branch_text(branch): | |||
|
47 | if branch: | |||
|
48 | return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name']) | |||
|
49 | else: | |||
|
50 | ## case for SVN no branch push... | |||
|
51 | return 'to trunk' | |||
|
52 | %> \ | |||
|
53 | ||||
|
54 | % for branch, branch_commits in branches_commits.items(): | |||
|
55 | ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)} | |||
|
56 | % for commit in branch_commits['commits']: | |||
|
57 | `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links} | |||
|
58 | % endfor | |||
|
59 | % endfor | |||
|
60 | ''') | |||
|
61 | ||||
|
62 | ||||
|
63 | class SlackDataHandler(CommitParsingDataHandler): | |||
|
64 | name = 'slack' | |||
|
65 | ||||
|
66 | def __init__(self): | |||
|
67 | pass | |||
|
68 | ||||
|
69 | def __call__(self, event: events.RhodecodeEvent, data): | |||
|
70 | if not isinstance(event, events.RhodecodeEvent): | |||
|
71 | raise TypeError(f"event {event} is not subtype of events.RhodecodeEvent") | |||
|
72 | ||||
|
73 | actor = data["actor"]["username"] | |||
|
74 | default_title = f'*{actor}* caused a *{event.name}* event' | |||
|
75 | default_text = f'*{actor}* caused a *{event.name}* event' | |||
|
76 | ||||
|
77 | default_slack_data = SlackData(title=default_title, text=default_text) | |||
|
78 | ||||
|
79 | if isinstance(event, events.PullRequestCommentEvent): | |||
|
80 | return self.format_pull_request_comment_event( | |||
|
81 | event, data, default_slack_data | |||
|
82 | ) | |||
|
83 | elif isinstance(event, events.PullRequestCommentEditEvent): | |||
|
84 | return self.format_pull_request_comment_event( | |||
|
85 | event, data, default_slack_data | |||
|
86 | ) | |||
|
87 | elif isinstance(event, events.PullRequestReviewEvent): | |||
|
88 | return self.format_pull_request_review_event(event, data, default_slack_data) | |||
|
89 | elif isinstance(event, events.PullRequestEvent): | |||
|
90 | return self.format_pull_request_event(event, data, default_slack_data) | |||
|
91 | elif isinstance(event, events.RepoPushEvent): | |||
|
92 | return self.format_repo_push_event(event, data, default_slack_data) | |||
|
93 | elif isinstance(event, events.RepoCreateEvent): | |||
|
94 | return self.format_repo_create_event(event, data, default_slack_data) | |||
|
95 | else: | |||
|
96 | raise ValueError( | |||
|
97 | f'event type `{event.__class__}` has no handler defined') | |||
|
98 | ||||
|
99 | def format_pull_request_comment_event(self, event, data, slack_data): | |||
|
100 | comment_text = data['comment']['text'] | |||
|
101 | if len(comment_text) > 200: | |||
|
102 | comment_text = '<{comment_url}|{comment_text}...>'.format( | |||
|
103 | comment_text=comment_text[:200], | |||
|
104 | comment_url=data['comment']['url'], | |||
|
105 | ) | |||
|
106 | ||||
|
107 | fields = None | |||
|
108 | overrides = None | |||
|
109 | status_text = None | |||
|
110 | ||||
|
111 | if data['comment']['status']: | |||
|
112 | status_color = { | |||
|
113 | 'approved': '#0ac878', | |||
|
114 | 'rejected': '#e85e4d'}.get(data['comment']['status']) | |||
|
115 | ||||
|
116 | if status_color: | |||
|
117 | overrides = {"color": status_color} | |||
|
118 | ||||
|
119 | status_text = data['comment']['status'] | |||
|
120 | ||||
|
121 | if data['comment']['file']: | |||
|
122 | fields = [ | |||
|
123 | { | |||
|
124 | "title": "file", | |||
|
125 | "value": data['comment']['file'] | |||
|
126 | }, | |||
|
127 | { | |||
|
128 | "title": "line", | |||
|
129 | "value": data['comment']['line'] | |||
|
130 | } | |||
|
131 | ] | |||
|
132 | ||||
|
133 | template = Template(textwrap.dedent(r''' | |||
|
134 | *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>: | |||
|
135 | ''')) | |||
|
136 | title = render_with_traceback( | |||
|
137 | template, data=data, comment=event.comment) | |||
|
138 | ||||
|
139 | template = Template(textwrap.dedent(r''' | |||
|
140 | *pull request title*: ${pr_title} | |||
|
141 | % if status_text: | |||
|
142 | *submitted status*: `${status_text}` | |||
|
143 | % endif | |||
|
144 | >>> ${comment_text} | |||
|
145 | ''')) | |||
|
146 | text = render_with_traceback( | |||
|
147 | template, | |||
|
148 | comment_text=comment_text, | |||
|
149 | pr_title=data['pullrequest']['title'], | |||
|
150 | status_text=status_text) | |||
|
151 | ||||
|
152 | slack_data.title = title | |||
|
153 | slack_data.text = text | |||
|
154 | slack_data.fields = fields | |||
|
155 | slack_data.overrides = overrides | |||
|
156 | ||||
|
157 | return slack_data | |||
|
158 | ||||
|
159 | def format_pull_request_review_event(self, event, data, slack_data) -> SlackData: | |||
|
160 | template = Template(textwrap.dedent(r''' | |||
|
161 | *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>: | |||
|
162 | ''')) | |||
|
163 | title = render_with_traceback(template, data=data) | |||
|
164 | ||||
|
165 | template = Template(textwrap.dedent(r''' | |||
|
166 | *pull request title*: ${pr_title} | |||
|
167 | ''')) | |||
|
168 | text = render_with_traceback( | |||
|
169 | template, | |||
|
170 | pr_title=data['pullrequest']['title']) | |||
|
171 | ||||
|
172 | slack_data.title = title | |||
|
173 | slack_data.text = text | |||
|
174 | ||||
|
175 | return slack_data | |||
|
176 | ||||
|
177 | def format_pull_request_event(self, event, data, slack_data) -> SlackData: | |||
|
178 | action = { | |||
|
179 | events.PullRequestCloseEvent: 'closed', | |||
|
180 | events.PullRequestMergeEvent: 'merged', | |||
|
181 | events.PullRequestUpdateEvent: 'updated', | |||
|
182 | events.PullRequestCreateEvent: 'created', | |||
|
183 | }.get(event.__class__, str(event.__class__)) | |||
|
184 | ||||
|
185 | template = Template(textwrap.dedent(r''' | |||
|
186 | *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>: | |||
|
187 | ''')) | |||
|
188 | title = render_with_traceback(template, data=data, action=action) | |||
|
189 | ||||
|
190 | template = Template(textwrap.dedent(r''' | |||
|
191 | *pull request title*: ${pr_title} | |||
|
192 | %if data['pullrequest']['commits']: | |||
|
193 | *commits*: ${len(data['pullrequest']['commits'])} | |||
|
194 | %endif | |||
|
195 | ''')) | |||
|
196 | text = render_with_traceback( | |||
|
197 | template, | |||
|
198 | pr_title=data['pullrequest']['title'], | |||
|
199 | data=data) | |||
|
200 | ||||
|
201 | slack_data.title = title | |||
|
202 | slack_data.text = text | |||
|
203 | ||||
|
204 | return slack_data | |||
|
205 | ||||
|
206 | def format_repo_push_event(self, event, data, slack_data) -> SlackData: | |||
|
207 | branches_commits = self.aggregate_branch_data( | |||
|
208 | data['push']['branches'], data['push']['commits']) | |||
|
209 | ||||
|
210 | template = Template(r''' | |||
|
211 | *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>: | |||
|
212 | ''') | |||
|
213 | title = render_with_traceback(template, data=data) | |||
|
214 | ||||
|
215 | text = render_with_traceback( | |||
|
216 | REPO_PUSH_TEMPLATE, | |||
|
217 | data=data, | |||
|
218 | branches_commits=branches_commits, | |||
|
219 | html_to_slack_links=html_to_slack_links, | |||
|
220 | ) | |||
|
221 | ||||
|
222 | slack_data.title = title | |||
|
223 | slack_data.text = text | |||
|
224 | ||||
|
225 | return slack_data | |||
|
226 | ||||
|
227 | def format_repo_create_event(self, event, data, slack_data) -> SlackData: | |||
|
228 | template = Template(r''' | |||
|
229 | *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}: | |||
|
230 | ''') | |||
|
231 | title = render_with_traceback(template, data=data) | |||
|
232 | ||||
|
233 | template = Template(textwrap.dedent(r''' | |||
|
234 | repo_url: ${data['repo']['url']} | |||
|
235 | repo_type: ${data['repo']['repo_type']} | |||
|
236 | ''')) | |||
|
237 | text = render_with_traceback(template, data=data) | |||
|
238 | ||||
|
239 | slack_data.title = title | |||
|
240 | slack_data.text = text | |||
|
241 | ||||
|
242 | return slack_data No newline at end of file |
@@ -0,0 +1,175 b'' | |||||
|
1 | # Copyright (C) 2012-2023 RhodeCode GmbH | |||
|
2 | # | |||
|
3 | # This program is free software: you can redistribute it and/or modify | |||
|
4 | # it under the terms of the GNU Affero General Public License, version 3 | |||
|
5 | # (only), as published by the Free Software Foundation. | |||
|
6 | # | |||
|
7 | # This program is distributed in the hope that it will be useful, | |||
|
8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
10 | # GNU General Public License for more details. | |||
|
11 | # | |||
|
12 | # You should have received a copy of the GNU Affero General Public License | |||
|
13 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
14 | # | |||
|
15 | # This program is dual-licensed. If you wish to learn more about the | |||
|
16 | # RhodeCode Enterprise Edition, including its added features, Support services, | |||
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |||
|
18 | ||||
|
19 | import logging | |||
|
20 | ||||
|
21 | from rhodecode import events | |||
|
22 | from rhodecode.integrations.types.base import CommitParsingDataHandler, UrlTmpl | |||
|
23 | ||||
|
24 | ||||
|
25 | log = logging.getLogger(__name__) | |||
|
26 | ||||
|
27 | ||||
|
28 | class WebhookDataHandler(CommitParsingDataHandler): | |||
|
29 | name = 'webhook' | |||
|
30 | ||||
|
31 | def __init__(self, template_url, headers): | |||
|
32 | self.template_url = template_url | |||
|
33 | self.headers = headers | |||
|
34 | ||||
|
35 | def get_base_parsed_template(self, data): | |||
|
36 | """ | |||
|
37 | initially parses the passed in template with some common variables | |||
|
38 | available on ALL calls | |||
|
39 | """ | |||
|
40 | # note: make sure to update the `WEBHOOK_URL_VARS` if this changes | |||
|
41 | common_vars = { | |||
|
42 | 'repo_name': data['repo']['repo_name'], | |||
|
43 | 'repo_type': data['repo']['repo_type'], | |||
|
44 | 'repo_id': data['repo']['repo_id'], | |||
|
45 | 'repo_url': data['repo']['url'], | |||
|
46 | 'username': data['actor']['username'], | |||
|
47 | 'user_id': data['actor']['user_id'], | |||
|
48 | 'event_name': data['name'] | |||
|
49 | } | |||
|
50 | ||||
|
51 | extra_vars = {} | |||
|
52 | for extra_key, extra_val in data['repo']['extra_fields'].items(): | |||
|
53 | extra_vars[f'extra__{extra_key}'] = extra_val | |||
|
54 | common_vars.update(extra_vars) | |||
|
55 | ||||
|
56 | template_url = self.template_url.replace('${extra:', '${extra__') | |||
|
57 | for k, v in common_vars.items(): | |||
|
58 | template_url = UrlTmpl(template_url).safe_substitute(**{k: v}) | |||
|
59 | return template_url | |||
|
60 | ||||
|
61 | def repo_push_event_handler(self, event, data): | |||
|
62 | url = self.get_base_parsed_template(data) | |||
|
63 | url_calls = [] | |||
|
64 | ||||
|
65 | branches_commits = self.aggregate_branch_data( | |||
|
66 | data['push']['branches'], data['push']['commits']) | |||
|
67 | if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url: | |||
|
68 | # call it multiple times, for each branch if used in variables | |||
|
69 | for branch, commit_ids in branches_commits.items(): | |||
|
70 | branch_url = UrlTmpl(url).safe_substitute(branch=branch) | |||
|
71 | ||||
|
72 | if '${branch_head}' in branch_url: | |||
|
73 | # last commit in the aggregate is the head of the branch | |||
|
74 | branch_head = commit_ids['branch_head'] | |||
|
75 | branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head) | |||
|
76 | ||||
|
77 | # call further down for each commit if used | |||
|
78 | if '${commit_id}' in branch_url: | |||
|
79 | for commit_data in commit_ids['commits']: | |||
|
80 | commit_id = commit_data['raw_id'] | |||
|
81 | commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id) | |||
|
82 | # register per-commit call | |||
|
83 | log.debug( | |||
|
84 | 'register %s call(%s) to url %s', | |||
|
85 | self.name, event, commit_url) | |||
|
86 | url_calls.append( | |||
|
87 | (commit_url, self.headers, data)) | |||
|
88 | ||||
|
89 | else: | |||
|
90 | # register per-branch call | |||
|
91 | log.debug('register %s call(%s) to url %s', | |||
|
92 | self.name, event, branch_url) | |||
|
93 | url_calls.append((branch_url, self.headers, data)) | |||
|
94 | ||||
|
95 | else: | |||
|
96 | log.debug('register %s call(%s) to url %s', self.name, event, url) | |||
|
97 | url_calls.append((url, self.headers, data)) | |||
|
98 | ||||
|
99 | return url_calls | |||
|
100 | ||||
|
101 | def repo_commit_comment_handler(self, event, data): | |||
|
102 | url = self.get_base_parsed_template(data) | |||
|
103 | log.debug('register %s call(%s) to url %s', self.name, event, url) | |||
|
104 | comment_vars = [ | |||
|
105 | ('commit_comment_id', data['comment']['comment_id']), | |||
|
106 | ('commit_comment_text', data['comment']['comment_text']), | |||
|
107 | ('commit_comment_type', data['comment']['comment_type']), | |||
|
108 | ||||
|
109 | ('commit_comment_f_path', data['comment']['comment_f_path']), | |||
|
110 | ('commit_comment_line_no', data['comment']['comment_line_no']), | |||
|
111 | ||||
|
112 | ('commit_comment_commit_id', data['commit']['commit_id']), | |||
|
113 | ('commit_comment_commit_branch', data['commit']['commit_branch']), | |||
|
114 | ('commit_comment_commit_message', data['commit']['commit_message']), | |||
|
115 | ] | |||
|
116 | for k, v in comment_vars: | |||
|
117 | url = UrlTmpl(url).safe_substitute(**{k: v}) | |||
|
118 | ||||
|
119 | return [(url, self.headers, data)] | |||
|
120 | ||||
|
121 | def repo_commit_comment_edit_handler(self, event, data): | |||
|
122 | url = self.get_base_parsed_template(data) | |||
|
123 | log.debug('register %s call(%s) to url %s', self.name, event, url) | |||
|
124 | comment_vars = [ | |||
|
125 | ('commit_comment_id', data['comment']['comment_id']), | |||
|
126 | ('commit_comment_text', data['comment']['comment_text']), | |||
|
127 | ('commit_comment_type', data['comment']['comment_type']), | |||
|
128 | ||||
|
129 | ('commit_comment_f_path', data['comment']['comment_f_path']), | |||
|
130 | ('commit_comment_line_no', data['comment']['comment_line_no']), | |||
|
131 | ||||
|
132 | ('commit_comment_commit_id', data['commit']['commit_id']), | |||
|
133 | ('commit_comment_commit_branch', data['commit']['commit_branch']), | |||
|
134 | ('commit_comment_commit_message', data['commit']['commit_message']), | |||
|
135 | ] | |||
|
136 | for k, v in comment_vars: | |||
|
137 | url = UrlTmpl(url).safe_substitute(**{k: v}) | |||
|
138 | ||||
|
139 | return [(url, self.headers, data)] | |||
|
140 | ||||
|
141 | def repo_create_event_handler(self, event, data): | |||
|
142 | url = self.get_base_parsed_template(data) | |||
|
143 | log.debug('register %s call(%s) to url %s', self.name, event, url) | |||
|
144 | return [(url, self.headers, data)] | |||
|
145 | ||||
|
146 | def pull_request_event_handler(self, event, data): | |||
|
147 | url = self.get_base_parsed_template(data) | |||
|
148 | log.debug('register %s call(%s) to url %s', self.name, event, url) | |||
|
149 | pr_vars = [ | |||
|
150 | ('pull_request_id', data['pullrequest']['pull_request_id']), | |||
|
151 | ('pull_request_title', data['pullrequest']['title']), | |||
|
152 | ('pull_request_url', data['pullrequest']['url']), | |||
|
153 | ('pull_request_shadow_url', data['pullrequest']['shadow_url']), | |||
|
154 | ('pull_request_commits_uid', data['pullrequest']['commits_uid']), | |||
|
155 | ] | |||
|
156 | for k, v in pr_vars: | |||
|
157 | url = UrlTmpl(url).safe_substitute(**{k: v}) | |||
|
158 | ||||
|
159 | return [(url, self.headers, data)] | |||
|
160 | ||||
|
161 | def __call__(self, event, data): | |||
|
162 | ||||
|
163 | if isinstance(event, events.RepoPushEvent): | |||
|
164 | return self.repo_push_event_handler(event, data) | |||
|
165 | elif isinstance(event, events.RepoCreateEvent): | |||
|
166 | return self.repo_create_event_handler(event, data) | |||
|
167 | elif isinstance(event, events.RepoCommitCommentEvent): | |||
|
168 | return self.repo_commit_comment_handler(event, data) | |||
|
169 | elif isinstance(event, events.RepoCommitCommentEditEvent): | |||
|
170 | return self.repo_commit_comment_edit_handler(event, data) | |||
|
171 | elif isinstance(event, events.PullRequestEvent): | |||
|
172 | return self.pull_request_event_handler(event, data) | |||
|
173 | else: | |||
|
174 | raise ValueError( | |||
|
175 | f'event type `{event.__class__}` has no handler defined') |
@@ -18,7 +18,10 b'' | |||||
18 |
|
18 | |||
19 | import logging |
|
19 | import logging | |
20 |
|
20 | |||
21 |
from rhodecode.events.base import |
|
21 | from rhodecode.events.base import ( | |
|
22 | RhodeCodeIntegrationEvent, | |||
|
23 | RhodecodeEvent | |||
|
24 | ) | |||
22 |
|
25 | |||
23 | from rhodecode.events.base import ( # pragma: no cover |
|
26 | from rhodecode.events.base import ( # pragma: no cover | |
24 | FtsBuild |
|
27 | FtsBuild |
@@ -17,6 +17,7 b'' | |||||
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
18 | import logging |
|
18 | import logging | |
19 | import datetime |
|
19 | import datetime | |
|
20 | import typing | |||
20 |
|
21 | |||
21 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
22 | from zope.cachedescriptors.property import Lazy as LazyProperty | |
22 | from pyramid.threadlocal import get_current_request |
|
23 | from pyramid.threadlocal import get_current_request |
@@ -19,7 +19,7 b' import sys' | |||||
19 | import logging |
|
19 | import logging | |
20 |
|
20 | |||
21 | from rhodecode.integrations.registry import IntegrationTypeRegistry |
|
21 | from rhodecode.integrations.registry import IntegrationTypeRegistry | |
22 |
from rhodecode.integrations.types import webhook, slack, |
|
22 | from rhodecode.integrations.types import webhook, slack, email, base | |
23 | from rhodecode.lib.exc_tracking import store_exception |
|
23 | from rhodecode.lib.exc_tracking import store_exception | |
24 |
|
24 | |||
25 | log = logging.getLogger(__name__) |
|
25 | log = logging.getLogger(__name__) | |
@@ -35,8 +35,6 b' integration_type_registry.register_integ' | |||||
35 | integration_type_registry.register_integration_type( |
|
35 | integration_type_registry.register_integration_type( | |
36 | slack.SlackIntegrationType) |
|
36 | slack.SlackIntegrationType) | |
37 | integration_type_registry.register_integration_type( |
|
37 | integration_type_registry.register_integration_type( | |
38 | hipchat.HipchatIntegrationType) |
|
|||
39 | integration_type_registry.register_integration_type( |
|
|||
40 | email.EmailIntegrationType) |
|
38 | email.EmailIntegrationType) | |
41 |
|
39 | |||
42 |
|
40 |
@@ -20,17 +20,18 b' import colander' | |||||
20 | import string |
|
20 | import string | |
21 | import collections |
|
21 | import collections | |
22 | import logging |
|
22 | import logging | |
|
23 | ||||
23 | import requests |
|
24 | import requests | |
24 | import urllib.request |
|
25 | import urllib.request | |
25 | import urllib.parse |
|
26 | import urllib.parse | |
26 | import urllib.error |
|
27 | import urllib.error | |
27 | from requests.adapters import HTTPAdapter |
|
28 | from requests.adapters import HTTPAdapter | |
28 |
from requests.packages.urllib3.util |
|
29 | from requests.packages.urllib3.util import Retry | |
29 |
|
30 | |||
30 | from mako import exceptions |
|
31 | from mako import exceptions | |
31 |
|
32 | |||
32 | from rhodecode.lib.utils2 import safe_str |
|
33 | ||
33 |
from rhodecode. |
|
34 | from rhodecode.lib.str_utils import safe_str | |
34 |
|
35 | |||
35 |
|
36 | |||
36 | log = logging.getLogger(__name__) |
|
37 | log = logging.getLogger(__name__) | |
@@ -238,157 +239,6 b' class CommitParsingDataHandler(object):' | |||||
238 | return branches_commits |
|
239 | return branches_commits | |
239 |
|
240 | |||
240 |
|
241 | |||
241 | class WebhookDataHandler(CommitParsingDataHandler): |
|
|||
242 | name = 'webhook' |
|
|||
243 |
|
||||
244 | def __init__(self, template_url, headers): |
|
|||
245 | self.template_url = template_url |
|
|||
246 | self.headers = headers |
|
|||
247 |
|
||||
248 | def get_base_parsed_template(self, data): |
|
|||
249 | """ |
|
|||
250 | initially parses the passed in template with some common variables |
|
|||
251 | available on ALL calls |
|
|||
252 | """ |
|
|||
253 | # note: make sure to update the `WEBHOOK_URL_VARS` if this changes |
|
|||
254 | common_vars = { |
|
|||
255 | 'repo_name': data['repo']['repo_name'], |
|
|||
256 | 'repo_type': data['repo']['repo_type'], |
|
|||
257 | 'repo_id': data['repo']['repo_id'], |
|
|||
258 | 'repo_url': data['repo']['url'], |
|
|||
259 | 'username': data['actor']['username'], |
|
|||
260 | 'user_id': data['actor']['user_id'], |
|
|||
261 | 'event_name': data['name'] |
|
|||
262 | } |
|
|||
263 |
|
||||
264 | extra_vars = {} |
|
|||
265 | for extra_key, extra_val in data['repo']['extra_fields'].items(): |
|
|||
266 | extra_vars[f'extra__{extra_key}'] = extra_val |
|
|||
267 | common_vars.update(extra_vars) |
|
|||
268 |
|
||||
269 | template_url = self.template_url.replace('${extra:', '${extra__') |
|
|||
270 | for k, v in common_vars.items(): |
|
|||
271 | template_url = UrlTmpl(template_url).safe_substitute(**{k: v}) |
|
|||
272 | return template_url |
|
|||
273 |
|
||||
274 | def repo_push_event_handler(self, event, data): |
|
|||
275 | url = self.get_base_parsed_template(data) |
|
|||
276 | url_calls = [] |
|
|||
277 |
|
||||
278 | branches_commits = self.aggregate_branch_data( |
|
|||
279 | data['push']['branches'], data['push']['commits']) |
|
|||
280 | if '${branch}' in url or '${branch_head}' in url or '${commit_id}' in url: |
|
|||
281 | # call it multiple times, for each branch if used in variables |
|
|||
282 | for branch, commit_ids in branches_commits.items(): |
|
|||
283 | branch_url = UrlTmpl(url).safe_substitute(branch=branch) |
|
|||
284 |
|
||||
285 | if '${branch_head}' in branch_url: |
|
|||
286 | # last commit in the aggregate is the head of the branch |
|
|||
287 | branch_head = commit_ids['branch_head'] |
|
|||
288 | branch_url = UrlTmpl(branch_url).safe_substitute(branch_head=branch_head) |
|
|||
289 |
|
||||
290 | # call further down for each commit if used |
|
|||
291 | if '${commit_id}' in branch_url: |
|
|||
292 | for commit_data in commit_ids['commits']: |
|
|||
293 | commit_id = commit_data['raw_id'] |
|
|||
294 | commit_url = UrlTmpl(branch_url).safe_substitute(commit_id=commit_id) |
|
|||
295 | # register per-commit call |
|
|||
296 | log.debug( |
|
|||
297 | 'register %s call(%s) to url %s', |
|
|||
298 | self.name, event, commit_url) |
|
|||
299 | url_calls.append( |
|
|||
300 | (commit_url, self.headers, data)) |
|
|||
301 |
|
||||
302 | else: |
|
|||
303 | # register per-branch call |
|
|||
304 | log.debug('register %s call(%s) to url %s', |
|
|||
305 | self.name, event, branch_url) |
|
|||
306 | url_calls.append((branch_url, self.headers, data)) |
|
|||
307 |
|
||||
308 | else: |
|
|||
309 | log.debug('register %s call(%s) to url %s', self.name, event, url) |
|
|||
310 | url_calls.append((url, self.headers, data)) |
|
|||
311 |
|
||||
312 | return url_calls |
|
|||
313 |
|
||||
314 | def repo_commit_comment_handler(self, event, data): |
|
|||
315 | url = self.get_base_parsed_template(data) |
|
|||
316 | log.debug('register %s call(%s) to url %s', self.name, event, url) |
|
|||
317 | comment_vars = [ |
|
|||
318 | ('commit_comment_id', data['comment']['comment_id']), |
|
|||
319 | ('commit_comment_text', data['comment']['comment_text']), |
|
|||
320 | ('commit_comment_type', data['comment']['comment_type']), |
|
|||
321 |
|
||||
322 | ('commit_comment_f_path', data['comment']['comment_f_path']), |
|
|||
323 | ('commit_comment_line_no', data['comment']['comment_line_no']), |
|
|||
324 |
|
||||
325 | ('commit_comment_commit_id', data['commit']['commit_id']), |
|
|||
326 | ('commit_comment_commit_branch', data['commit']['commit_branch']), |
|
|||
327 | ('commit_comment_commit_message', data['commit']['commit_message']), |
|
|||
328 | ] |
|
|||
329 | for k, v in comment_vars: |
|
|||
330 | url = UrlTmpl(url).safe_substitute(**{k: v}) |
|
|||
331 |
|
||||
332 | return [(url, self.headers, data)] |
|
|||
333 |
|
||||
334 | def repo_commit_comment_edit_handler(self, event, data): |
|
|||
335 | url = self.get_base_parsed_template(data) |
|
|||
336 | log.debug('register %s call(%s) to url %s', self.name, event, url) |
|
|||
337 | comment_vars = [ |
|
|||
338 | ('commit_comment_id', data['comment']['comment_id']), |
|
|||
339 | ('commit_comment_text', data['comment']['comment_text']), |
|
|||
340 | ('commit_comment_type', data['comment']['comment_type']), |
|
|||
341 |
|
||||
342 | ('commit_comment_f_path', data['comment']['comment_f_path']), |
|
|||
343 | ('commit_comment_line_no', data['comment']['comment_line_no']), |
|
|||
344 |
|
||||
345 | ('commit_comment_commit_id', data['commit']['commit_id']), |
|
|||
346 | ('commit_comment_commit_branch', data['commit']['commit_branch']), |
|
|||
347 | ('commit_comment_commit_message', data['commit']['commit_message']), |
|
|||
348 | ] |
|
|||
349 | for k, v in comment_vars: |
|
|||
350 | url = UrlTmpl(url).safe_substitute(**{k: v}) |
|
|||
351 |
|
||||
352 | return [(url, self.headers, data)] |
|
|||
353 |
|
||||
354 | def repo_create_event_handler(self, event, data): |
|
|||
355 | url = self.get_base_parsed_template(data) |
|
|||
356 | log.debug('register %s call(%s) to url %s', self.name, event, url) |
|
|||
357 | return [(url, self.headers, data)] |
|
|||
358 |
|
||||
359 | def pull_request_event_handler(self, event, data): |
|
|||
360 | url = self.get_base_parsed_template(data) |
|
|||
361 | log.debug('register %s call(%s) to url %s', self.name, event, url) |
|
|||
362 | pr_vars = [ |
|
|||
363 | ('pull_request_id', data['pullrequest']['pull_request_id']), |
|
|||
364 | ('pull_request_title', data['pullrequest']['title']), |
|
|||
365 | ('pull_request_url', data['pullrequest']['url']), |
|
|||
366 | ('pull_request_shadow_url', data['pullrequest']['shadow_url']), |
|
|||
367 | ('pull_request_commits_uid', data['pullrequest']['commits_uid']), |
|
|||
368 | ] |
|
|||
369 | for k, v in pr_vars: |
|
|||
370 | url = UrlTmpl(url).safe_substitute(**{k: v}) |
|
|||
371 |
|
||||
372 | return [(url, self.headers, data)] |
|
|||
373 |
|
||||
374 | def __call__(self, event, data): |
|
|||
375 | from rhodecode import events |
|
|||
376 |
|
||||
377 | if isinstance(event, events.RepoPushEvent): |
|
|||
378 | return self.repo_push_event_handler(event, data) |
|
|||
379 | elif isinstance(event, events.RepoCreateEvent): |
|
|||
380 | return self.repo_create_event_handler(event, data) |
|
|||
381 | elif isinstance(event, events.RepoCommitCommentEvent): |
|
|||
382 | return self.repo_commit_comment_handler(event, data) |
|
|||
383 | elif isinstance(event, events.RepoCommitCommentEditEvent): |
|
|||
384 | return self.repo_commit_comment_edit_handler(event, data) |
|
|||
385 | elif isinstance(event, events.PullRequestEvent): |
|
|||
386 | return self.pull_request_event_handler(event, data) |
|
|||
387 | else: |
|
|||
388 | raise ValueError( |
|
|||
389 | f'event type `{event.__class__}` has no handler defined') |
|
|||
390 |
|
||||
391 |
|
||||
392 | def get_auth(settings): |
|
242 | def get_auth(settings): | |
393 | from requests.auth import HTTPBasicAuth |
|
243 | from requests.auth import HTTPBasicAuth | |
394 | username = settings.get('username') |
|
244 | username = settings.get('username') |
@@ -17,15 +17,13 b'' | |||||
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
18 |
|
18 | |||
19 |
|
19 | |||
20 | import re |
|
20 | ||
21 | import time |
|
21 | import time | |
22 | import textwrap |
|
|||
23 | import logging |
|
22 | import logging | |
24 |
|
23 | |||
25 | import deform |
|
24 | import deform # noqa | |
26 | import requests |
|
25 | import deform.widget | |
27 | import colander |
|
26 | import colander | |
28 | from mako.template import Template |
|
|||
29 |
|
27 | |||
30 | from rhodecode import events |
|
28 | from rhodecode import events | |
31 | from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc |
|
29 | from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc | |
@@ -34,34 +32,14 b' from rhodecode.lib import helpers as h' | |||||
34 | from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask |
|
32 | from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask | |
35 | from rhodecode.lib.colander_utils import strip_whitespace |
|
33 | from rhodecode.lib.colander_utils import strip_whitespace | |
36 | from rhodecode.integrations.types.base import ( |
|
34 | from rhodecode.integrations.types.base import ( | |
37 | IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback, |
|
35 | IntegrationTypeBase, | |
38 |
requests_retry_call |
|
36 | requests_retry_call, | |
39 |
|
37 | ) | ||
40 | log = logging.getLogger(__name__) |
|
|||
41 |
|
38 | |||
42 |
|
39 | from rhodecode.integrations.types.handlers.slack import SlackDataHandler, SlackData | ||
43 | def html_to_slack_links(message): |
|
|||
44 | return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub( |
|
|||
45 | r'<\1|\2>', message) |
|
|||
46 |
|
40 | |||
47 |
|
41 | |||
48 | REPO_PUSH_TEMPLATE = Template(''' |
|
42 | log = logging.getLogger(__name__) | |
49 | <% |
|
|||
50 | def branch_text(branch): |
|
|||
51 | if branch: |
|
|||
52 | return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name']) |
|
|||
53 | else: |
|
|||
54 | ## case for SVN no branch push... |
|
|||
55 | return 'to trunk' |
|
|||
56 | %> \ |
|
|||
57 |
|
||||
58 | % for branch, branch_commits in branches_commits.items(): |
|
|||
59 | ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)} |
|
|||
60 | % for commit in branch_commits['commits']: |
|
|||
61 | `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links} |
|
|||
62 | % endfor |
|
|||
63 | % endfor |
|
|||
64 | ''') |
|
|||
65 |
|
43 | |||
66 |
|
44 | |||
67 | class SlackSettingsSchema(colander.Schema): |
|
45 | class SlackSettingsSchema(colander.Schema): | |
@@ -111,7 +89,7 b' class SlackSettingsSchema(colander.Schem' | |||||
111 | ) |
|
89 | ) | |
112 |
|
90 | |||
113 |
|
91 | |||
114 |
class SlackIntegrationType(IntegrationTypeBase |
|
92 | class SlackIntegrationType(IntegrationTypeBase): | |
115 | key = 'slack' |
|
93 | key = 'slack' | |
116 | display_name = _('Slack') |
|
94 | display_name = _('Slack') | |
117 | description = _('Send events such as repo pushes and pull requests to ' |
|
95 | description = _('Send events such as repo pushes and pull requests to ' | |
@@ -132,45 +110,6 b' class SlackIntegrationType(IntegrationTy' | |||||
132 | events.RepoCreateEvent, |
|
110 | events.RepoCreateEvent, | |
133 | ] |
|
111 | ] | |
134 |
|
112 | |||
135 | def send_event(self, event): |
|
|||
136 | log.debug('handling event %s with integration %s', event.name, self) |
|
|||
137 |
|
||||
138 | if event.__class__ not in self.valid_events: |
|
|||
139 | log.debug('event %r not present in valid event list (%s)', event, self.valid_events) |
|
|||
140 | return |
|
|||
141 |
|
||||
142 | if not self.event_enabled(event): |
|
|||
143 | return |
|
|||
144 |
|
||||
145 | data = event.as_dict() |
|
|||
146 |
|
||||
147 | # defaults |
|
|||
148 | title = '*%s* caused a *%s* event' % ( |
|
|||
149 | data['actor']['username'], event.name) |
|
|||
150 | text = '*%s* caused a *%s* event' % ( |
|
|||
151 | data['actor']['username'], event.name) |
|
|||
152 | fields = None |
|
|||
153 | overrides = None |
|
|||
154 |
|
||||
155 | if isinstance(event, events.PullRequestCommentEvent): |
|
|||
156 | (title, text, fields, overrides) \ |
|
|||
157 | = self.format_pull_request_comment_event(event, data) |
|
|||
158 | elif isinstance(event, events.PullRequestCommentEditEvent): |
|
|||
159 | (title, text, fields, overrides) \ |
|
|||
160 | = self.format_pull_request_comment_event(event, data) |
|
|||
161 | elif isinstance(event, events.PullRequestReviewEvent): |
|
|||
162 | title, text = self.format_pull_request_review_event(event, data) |
|
|||
163 | elif isinstance(event, events.PullRequestEvent): |
|
|||
164 | title, text = self.format_pull_request_event(event, data) |
|
|||
165 | elif isinstance(event, events.RepoPushEvent): |
|
|||
166 | title, text = self.format_repo_push_event(data) |
|
|||
167 | elif isinstance(event, events.RepoCreateEvent): |
|
|||
168 | title, text = self.format_repo_create_event(data) |
|
|||
169 | else: |
|
|||
170 | log.error('unhandled event type: %r', event) |
|
|||
171 |
|
||||
172 | run_task(post_text_to_slack, self.settings, title, text, fields, overrides) |
|
|||
173 |
|
||||
174 | def settings_schema(self): |
|
113 | def settings_schema(self): | |
175 | schema = SlackSettingsSchema() |
|
114 | schema = SlackSettingsSchema() | |
176 | schema.add(colander.SchemaNode( |
|
115 | schema.add(colander.SchemaNode( | |
@@ -186,137 +125,31 b' class SlackIntegrationType(IntegrationTy' | |||||
186 |
|
125 | |||
187 | return schema |
|
126 | return schema | |
188 |
|
127 | |||
189 |
def |
|
128 | def send_event(self, event): | |
190 | comment_text = data['comment']['text'] |
|
129 | log.debug('handling event %s with integration %s', event.name, self) | |
191 | if len(comment_text) > 200: |
|
|||
192 | comment_text = '<{comment_url}|{comment_text}...>'.format( |
|
|||
193 | comment_text=comment_text[:200], |
|
|||
194 | comment_url=data['comment']['url'], |
|
|||
195 | ) |
|
|||
196 |
|
||||
197 | fields = None |
|
|||
198 | overrides = None |
|
|||
199 | status_text = None |
|
|||
200 |
|
||||
201 | if data['comment']['status']: |
|
|||
202 | status_color = { |
|
|||
203 | 'approved': '#0ac878', |
|
|||
204 | 'rejected': '#e85e4d'}.get(data['comment']['status']) |
|
|||
205 |
|
||||
206 | if status_color: |
|
|||
207 | overrides = {"color": status_color} |
|
|||
208 |
|
||||
209 | status_text = data['comment']['status'] |
|
|||
210 |
|
130 | |||
211 | if data['comment']['file']: |
|
131 | if event.__class__ not in self.valid_events: | |
212 | fields = [ |
|
132 | log.debug('event %r not present in valid event list (%s)', event, self.valid_events) | |
213 |
|
|
133 | return | |
214 | "title": "file", |
|
|||
215 | "value": data['comment']['file'] |
|
|||
216 | }, |
|
|||
217 | { |
|
|||
218 | "title": "line", |
|
|||
219 | "value": data['comment']['line'] |
|
|||
220 | } |
|
|||
221 | ] |
|
|||
222 |
|
||||
223 | template = Template(textwrap.dedent(r''' |
|
|||
224 | *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>: |
|
|||
225 | ''')) |
|
|||
226 | title = render_with_traceback( |
|
|||
227 | template, data=data, comment=event.comment) |
|
|||
228 |
|
||||
229 | template = Template(textwrap.dedent(r''' |
|
|||
230 | *pull request title*: ${pr_title} |
|
|||
231 | % if status_text: |
|
|||
232 | *submitted status*: `${status_text}` |
|
|||
233 | % endif |
|
|||
234 | >>> ${comment_text} |
|
|||
235 | ''')) |
|
|||
236 | text = render_with_traceback( |
|
|||
237 | template, |
|
|||
238 | comment_text=comment_text, |
|
|||
239 | pr_title=data['pullrequest']['title'], |
|
|||
240 | status_text=status_text) |
|
|||
241 |
|
||||
242 | return title, text, fields, overrides |
|
|||
243 |
|
||||
244 | def format_pull_request_review_event(self, event, data): |
|
|||
245 | template = Template(textwrap.dedent(r''' |
|
|||
246 | *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>: |
|
|||
247 | ''')) |
|
|||
248 | title = render_with_traceback(template, data=data) |
|
|||
249 |
|
134 | |||
250 | template = Template(textwrap.dedent(r''' |
|
135 | if not self.event_enabled(event): | |
251 | *pull request title*: ${pr_title} |
|
136 | return | |
252 | ''')) |
|
|||
253 | text = render_with_traceback( |
|
|||
254 | template, |
|
|||
255 | pr_title=data['pullrequest']['title']) |
|
|||
256 |
|
||||
257 | return title, text |
|
|||
258 |
|
137 | |||
259 | def format_pull_request_event(self, event, data): |
|
138 | data = event.as_dict() | |
260 | action = { |
|
|||
261 | events.PullRequestCloseEvent: 'closed', |
|
|||
262 | events.PullRequestMergeEvent: 'merged', |
|
|||
263 | events.PullRequestUpdateEvent: 'updated', |
|
|||
264 | events.PullRequestCreateEvent: 'created', |
|
|||
265 | }.get(event.__class__, str(event.__class__)) |
|
|||
266 |
|
||||
267 | template = Template(textwrap.dedent(r''' |
|
|||
268 | *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>: |
|
|||
269 | ''')) |
|
|||
270 | title = render_with_traceback(template, data=data, action=action) |
|
|||
271 |
|
||||
272 | template = Template(textwrap.dedent(r''' |
|
|||
273 | *pull request title*: ${pr_title} |
|
|||
274 | %if data['pullrequest']['commits']: |
|
|||
275 | *commits*: ${len(data['pullrequest']['commits'])} |
|
|||
276 | %endif |
|
|||
277 | ''')) |
|
|||
278 | text = render_with_traceback( |
|
|||
279 | template, |
|
|||
280 | pr_title=data['pullrequest']['title'], |
|
|||
281 | data=data) |
|
|||
282 |
|
139 | |||
283 | return title, text |
|
140 | handler = SlackDataHandler() | |
284 |
|
141 | slack_data = handler(event, data) | ||
285 | def format_repo_push_event(self, data): |
|
142 | # title, text, fields, overrides | |
286 | branches_commits = self.aggregate_branch_data( |
|
143 | run_task(post_text_to_slack, self.settings, slack_data) | |
287 | data['push']['branches'], data['push']['commits']) |
|
|||
288 |
|
||||
289 | template = Template(r''' |
|
|||
290 | *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>: |
|
|||
291 | ''') |
|
|||
292 | title = render_with_traceback(template, data=data) |
|
|||
293 |
|
||||
294 | text = render_with_traceback( |
|
|||
295 | REPO_PUSH_TEMPLATE, |
|
|||
296 | data=data, |
|
|||
297 | branches_commits=branches_commits, |
|
|||
298 | html_to_slack_links=html_to_slack_links, |
|
|||
299 | ) |
|
|||
300 |
|
||||
301 | return title, text |
|
|||
302 |
|
||||
303 | def format_repo_create_event(self, data): |
|
|||
304 | template = Template(r''' |
|
|||
305 | *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}: |
|
|||
306 | ''') |
|
|||
307 | title = render_with_traceback(template, data=data) |
|
|||
308 |
|
||||
309 | template = Template(textwrap.dedent(r''' |
|
|||
310 | repo_url: ${data['repo']['url']} |
|
|||
311 | repo_type: ${data['repo']['repo_type']} |
|
|||
312 | ''')) |
|
|||
313 | text = render_with_traceback(template, data=data) |
|
|||
314 |
|
||||
315 | return title, text |
|
|||
316 |
|
144 | |||
317 |
|
145 | |||
318 | @async_task(ignore_result=True, base=RequestContextTask) |
|
146 | @async_task(ignore_result=True, base=RequestContextTask) | |
319 | def post_text_to_slack(settings, title, text, fields=None, overrides=None): |
|
147 | def post_text_to_slack(settings, slack_data: SlackData): | |
|
148 | title = slack_data.title | |||
|
149 | text = slack_data.text | |||
|
150 | fields = slack_data.fields | |||
|
151 | overrides = slack_data.overrides | |||
|
152 | ||||
320 | log.debug('sending %s (%s) to slack %s', title, text, settings['service']) |
|
153 | log.debug('sending %s (%s) to slack %s', title, text, settings['service']) | |
321 |
|
154 | |||
322 | fields = fields or [] |
|
155 | fields = fields or [] |
@@ -17,7 +17,7 b'' | |||||
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
18 |
|
18 | |||
19 |
|
19 | |||
20 |
|
20 | import deform # noqa | ||
21 | import deform.widget |
|
21 | import deform.widget | |
22 | import logging |
|
22 | import logging | |
23 | import colander |
|
23 | import colander | |
@@ -28,8 +28,14 b' from rhodecode.lib.colander_utils import' | |||||
28 | from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc |
|
28 | from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc | |
29 | from rhodecode.translation import _ |
|
29 | from rhodecode.translation import _ | |
30 | from rhodecode.integrations.types.base import ( |
|
30 | from rhodecode.integrations.types.base import ( | |
31 | IntegrationTypeBase, get_auth, get_web_token, get_url_vars, |
|
31 | IntegrationTypeBase, | |
32 | WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call) |
|
32 | get_auth, | |
|
33 | get_web_token, | |||
|
34 | get_url_vars, | |||
|
35 | WEBHOOK_URL_VARS, | |||
|
36 | requests_retry_call, | |||
|
37 | ) | |||
|
38 | from rhodecode.integrations.types.handlers.webhook import WebhookDataHandler | |||
33 | from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask |
|
39 | from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask | |
34 | from rhodecode.model.validation_schema import widgets |
|
40 | from rhodecode.model.validation_schema import widgets | |
35 |
|
41 | |||
@@ -57,7 +63,7 b' class WebhookSettingsSchema(colander.Sch' | |||||
57 | widget=widgets.CodeMirrorWidget( |
|
63 | widget=widgets.CodeMirrorWidget( | |
58 | help_block_collapsable_name='Show url variables', |
|
64 | help_block_collapsable_name='Show url variables', | |
59 | help_block_collapsable=( |
|
65 | help_block_collapsable=( | |
60 | 'E.g http://my-serv.com/trigger_job/${{event_name}}' |
|
66 | 'E.g https://my-serv.com/trigger_job/${{event_name}}' | |
61 | '?PR_ID=${{pull_request_id}}' |
|
67 | '?PR_ID=${{pull_request_id}}' | |
62 | '\nFull list of vars:\n{}'.format(URL_VARS)), |
|
68 | '\nFull list of vars:\n{}'.format(URL_VARS)), | |
63 | codemirror_mode='text', |
|
69 | codemirror_mode='text', | |
@@ -189,11 +195,11 b' class WebhookIntegrationType(Integration' | |||||
189 | url_calls = handler(event, data) |
|
195 | url_calls = handler(event, data) | |
190 | log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls]) |
|
196 | log.debug('Webhook: calling following urls: %s', [x[0] for x in url_calls]) | |
191 |
|
197 | |||
192 |
run_task(post_to_webhook, |
|
198 | run_task(post_to_webhook, self.settings, url_calls) | |
193 |
|
199 | |||
194 |
|
200 | |||
195 | @async_task(ignore_result=True, base=RequestContextTask) |
|
201 | @async_task(ignore_result=True, base=RequestContextTask) | |
196 |
def post_to_webhook( |
|
202 | def post_to_webhook(settings, url_calls): | |
197 | """ |
|
203 | """ | |
198 | Example data:: |
|
204 | Example data:: | |
199 |
|
205 |
@@ -32,23 +32,27 b' def repo_push_event(backend, user_regula' | |||||
32 | {'message': 'this is because 5b23c3532 broke stuff'}, |
|
32 | {'message': 'this is because 5b23c3532 broke stuff'}, | |
33 | {'message': 'last commit'}, |
|
33 | {'message': 'last commit'}, | |
34 | ] |
|
34 | ] | |
35 |
|
|
35 | r = backend.create_repo(commits) | |
36 | repo = backend.create_repo() |
|
36 | ||
|
37 | commit_ids = list(backend.commit_ids.values()) | |||
|
38 | repo_name = backend.repo_name | |||
|
39 | alias = backend.alias | |||
|
40 | ||||
37 | scm_extras = AttributeDict({ |
|
41 | scm_extras = AttributeDict({ | |
38 | 'ip': '127.0.0.1', |
|
42 | 'ip': '127.0.0.1', | |
39 | 'username': user_regular.username, |
|
43 | 'username': user_regular.username, | |
40 | 'user_id': user_regular.user_id, |
|
44 | 'user_id': user_regular.user_id, | |
41 | 'action': '', |
|
45 | 'action': '', | |
42 |
'repository': |
|
46 | 'repository': repo_name, | |
43 |
'scm': |
|
47 | 'scm': alias, | |
44 | 'config': '', |
|
48 | 'config': '', | |
45 | 'repo_store': '', |
|
49 | 'repo_store': '', | |
46 |
'server_url': 'http:// |
|
50 | 'server_url': 'http://httpbin:9090', | |
47 | 'make_lock': None, |
|
51 | 'make_lock': None, | |
48 | 'locked_by': [None], |
|
52 | 'locked_by': [None], | |
49 | 'commit_ids': commit_ids, |
|
53 | 'commit_ids': commit_ids, | |
50 | }) |
|
54 | }) | |
51 |
|
55 | |||
52 |
return events.RepoPushEvent(repo_name= |
|
56 | return events.RepoPushEvent(repo_name=repo_name, | |
53 | pushed_commit_ids=commit_ids, |
|
57 | pushed_commit_ids=commit_ids, | |
54 | extras=scm_extras) |
|
58 | extras=scm_extras) |
@@ -1,4 +1,3 b'' | |||||
1 |
|
||||
2 |
|
|
1 | # Copyright (C) 2010-2023 RhodeCode GmbH | |
3 | # |
|
2 | # | |
4 | # This program is free software: you can redistribute it and/or modify |
|
3 | # This program is free software: you can redistribute it and/or modify |
@@ -1,4 +1,3 b'' | |||||
1 |
|
||||
2 |
|
|
1 | # Copyright (C) 2010-2023 RhodeCode GmbH | |
3 | # |
|
2 | # | |
4 | # This program is free software: you can redistribute it and/or modify |
|
3 | # This program is free software: you can redistribute it and/or modify | |
@@ -18,11 +17,43 b'' | |||||
18 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
17 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
19 |
|
18 | |||
20 | import pytest |
|
19 | import pytest | |
|
20 | import mock | |||
21 | from mock import patch |
|
21 | from mock import patch | |
22 |
|
22 | |||
23 | from rhodecode import events |
|
23 | from rhodecode import events | |
|
24 | from rhodecode.integrations.types.handlers.slack import SlackDataHandler | |||
24 | from rhodecode.model.db import Session, Integration |
|
25 | from rhodecode.model.db import Session, Integration | |
25 | from rhodecode.integrations.types.slack import SlackIntegrationType |
|
26 | from rhodecode.integrations.types.slack import SlackIntegrationType | |
|
27 | from rhodecode.tests import GIT_REPO | |||
|
28 | ||||
|
29 | ||||
|
30 | @pytest.fixture() | |||
|
31 | def base_slack_data(): | |||
|
32 | return { | |||
|
33 | "pullrequest": { | |||
|
34 | "url": "https://example.com/pr1", | |||
|
35 | "pull_request_id": "1", | |||
|
36 | "title": "started pr", | |||
|
37 | "status": "new", | |||
|
38 | "commits": ["1", "2"], | |||
|
39 | "shadow_url": "http://shadow-url", | |||
|
40 | }, | |||
|
41 | "actor": {"username": "foo-user"}, | |||
|
42 | "comment": { | |||
|
43 | "comment_id": 1, | |||
|
44 | "text": "test-comment", | |||
|
45 | "status": "approved", | |||
|
46 | "file": "text.py", | |||
|
47 | "line": "1", | |||
|
48 | "type": "note", | |||
|
49 | }, | |||
|
50 | "push": {"branches": "", "commits": []}, | |||
|
51 | "repo": { | |||
|
52 | "url": "https://example.com/repo1", | |||
|
53 | "repo_name": GIT_REPO, | |||
|
54 | "repo_type": "git", | |||
|
55 | }, | |||
|
56 | } | |||
26 |
|
57 | |||
27 |
|
58 | |||
28 | @pytest.fixture() |
|
59 | @pytest.fixture() | |
@@ -52,14 +83,66 b' def slack_integration(request, app, slac' | |||||
52 | return integration |
|
83 | return integration | |
53 |
|
84 | |||
54 |
|
85 | |||
|
86 | @pytest.fixture() | |||
|
87 | def slack_integration_empty(request, app, slack_settings): | |||
|
88 | slack_settings['events'] = [] | |||
|
89 | integration = Integration() | |||
|
90 | integration.name = 'test slack integration' | |||
|
91 | integration.enabled = True | |||
|
92 | integration.integration_type = SlackIntegrationType.key | |||
|
93 | integration.settings = slack_settings | |||
|
94 | Session().add(integration) | |||
|
95 | Session().commit() | |||
|
96 | request.addfinalizer(lambda: Session().delete(integration)) | |||
|
97 | return integration | |||
|
98 | ||||
|
99 | ||||
55 | def test_slack_push(slack_integration, repo_push_event): |
|
100 | def test_slack_push(slack_integration, repo_push_event): | |
|
101 | ||||
56 | with patch('rhodecode.integrations.types.slack.post_text_to_slack') as call: |
|
102 | with patch('rhodecode.integrations.types.slack.post_text_to_slack') as call: | |
57 | events.trigger(repo_push_event) |
|
103 | events.trigger(repo_push_event) | |
58 | assert 'pushed to' in call.call_args[0][1] |
|
104 | ||
|
105 | assert 'pushed to' in call.call_args[0][1].title | |||
|
106 | # specific commit was parsed and serialized | |||
|
107 | assert 'change that fixes #41' in call.call_args[0][1].text | |||
59 |
|
108 | |||
60 | slack_integration.settings['events'] = [] |
|
109 | ||
61 | Session().commit() |
|
110 | def test_slack_push_no_events(slack_integration_empty, repo_push_event): | |
|
111 | ||||
|
112 | assert Integration.get(slack_integration_empty.integration_id).settings['events'] == [] | |||
62 |
|
113 | |||
63 | with patch('rhodecode.integrations.types.slack.post_text_to_slack') as call: |
|
114 | with patch('rhodecode.integrations.types.slack.post_text_to_slack') as call: | |
64 | events.trigger(repo_push_event) |
|
115 | events.trigger(repo_push_event) | |
65 | assert not call.call_args |
|
116 | assert not call.call_args | |
|
117 | ||||
|
118 | ||||
|
119 | def test_slack_data_handler_wrong_event(): | |||
|
120 | handler = SlackDataHandler() | |||
|
121 | data = {"actor": {"username": "foo-user"}} | |||
|
122 | with pytest.raises(ValueError): | |||
|
123 | handler(events.RhodecodeEvent(), data) | |||
|
124 | ||||
|
125 | ||||
|
126 | @pytest.mark.parametrize("event_type, args", [ | |||
|
127 | ( | |||
|
128 | events.PullRequestCommentEvent, | |||
|
129 | (mock.MagicMock(name="pull-request"), mock.MagicMock(name="comment")), | |||
|
130 | ), | |||
|
131 | ( | |||
|
132 | events.PullRequestCommentEditEvent, | |||
|
133 | (mock.MagicMock(name="pull-request"), mock.MagicMock(name="comment")), | |||
|
134 | ), | |||
|
135 | ( | |||
|
136 | events.PullRequestReviewEvent, | |||
|
137 | (mock.MagicMock(name="pull-request"), mock.MagicMock(name="status")), | |||
|
138 | ), | |||
|
139 | ( | |||
|
140 | events.RepoPushEvent, | |||
|
141 | (GIT_REPO, mock.MagicMock(name="pushed_commit_ids"), mock.MagicMock(name="extras")), | |||
|
142 | ), | |||
|
143 | (events.PullRequestEvent, (mock.MagicMock(), )), | |||
|
144 | (events.RepoCreateEvent, (mock.MagicMock(), )), | |||
|
145 | ]) | |||
|
146 | def test_slack_data_handler(app, event_type: events.RhodecodeEvent, args, base_slack_data): | |||
|
147 | handler = SlackDataHandler() | |||
|
148 | handler(event_type(*args), base_slack_data) |
@@ -18,10 +18,13 b'' | |||||
18 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
18 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
19 |
|
19 | |||
20 | import pytest |
|
20 | import pytest | |
|
21 | import mock | |||
|
22 | from mock import patch | |||
21 |
|
23 | |||
22 | from rhodecode import events |
|
24 | from rhodecode import events | |
23 | from rhodecode.lib.utils2 import AttributeDict |
|
25 | from rhodecode.lib.utils2 import AttributeDict | |
24 | from rhodecode.integrations.types.webhook import WebhookDataHandler |
|
26 | from rhodecode.integrations.types.webhook import WebhookDataHandler | |
|
27 | from rhodecode.tests import GIT_REPO | |||
25 |
|
28 | |||
26 |
|
29 | |||
27 | @pytest.fixture() |
|
30 | @pytest.fixture() | |
@@ -38,7 +41,33 b' def base_data():' | |||||
38 | 'actor': { |
|
41 | 'actor': { | |
39 | 'username': 'actor_name', |
|
42 | 'username': 'actor_name', | |
40 | 'user_id': 1 |
|
43 | 'user_id': 1 | |
41 | } |
|
44 | }, | |
|
45 | "pullrequest": { | |||
|
46 | "url": "https://example.com/pr1", | |||
|
47 | "pull_request_id": "1", | |||
|
48 | "title": "started pr", | |||
|
49 | "status": "new", | |||
|
50 | "commits_uid": ["1", "2"], | |||
|
51 | "shadow_url": "http://shadow-url", | |||
|
52 | }, | |||
|
53 | "comment": { | |||
|
54 | "comment_id": 1, | |||
|
55 | "comment_text": "test-comment", | |||
|
56 | "status": "approved", | |||
|
57 | "comment_f_path": "text.py", | |||
|
58 | "comment_line_no": "1", | |||
|
59 | "comment_type": "note", | |||
|
60 | }, | |||
|
61 | "commit": { | |||
|
62 | "commit_id": "efefef", | |||
|
63 | "commit_branch": "master", | |||
|
64 | "commit_message": "changed foo" | |||
|
65 | }, | |||
|
66 | "push": { | |||
|
67 | "branches": "", | |||
|
68 | "commits": [] | |||
|
69 | }, | |||
|
70 | ||||
42 | } |
|
71 | } | |
43 |
|
72 | |||
44 |
|
73 | |||
@@ -131,3 +160,37 b' def test_webook_parse_url_for_push_event' | |||||
131 | urls = handler(repo_push_event, base_data) |
|
160 | urls = handler(repo_push_event, base_data) | |
132 | assert urls == [ |
|
161 | assert urls == [ | |
133 | (url, headers, base_data) for url in expected_urls] |
|
162 | (url, headers, base_data) for url in expected_urls] | |
|
163 | ||||
|
164 | ||||
|
165 | @pytest.mark.parametrize("event_type, args", [ | |||
|
166 | ( | |||
|
167 | events.RepoPushEvent, | |||
|
168 | (GIT_REPO, mock.MagicMock(name="pushed_commit_ids"), mock.MagicMock(name="extras")), | |||
|
169 | ), | |||
|
170 | ( | |||
|
171 | events.RepoCreateEvent, | |||
|
172 | (GIT_REPO,), | |||
|
173 | ), | |||
|
174 | ( | |||
|
175 | events.RepoCommitCommentEvent, | |||
|
176 | (GIT_REPO, mock.MagicMock(name="commit"), mock.MagicMock(name="comment")), | |||
|
177 | ), | |||
|
178 | ( | |||
|
179 | events.RepoCommitCommentEditEvent, | |||
|
180 | (GIT_REPO, mock.MagicMock(name="commit"), mock.MagicMock(name="comment")), | |||
|
181 | ), | |||
|
182 | ( | |||
|
183 | events.PullRequestEvent, | |||
|
184 | (mock.MagicMock(), ), | |||
|
185 | ), | |||
|
186 | ]) | |||
|
187 | def test_webhook_data_handler(app, event_type: events.RhodecodeEvent, args, base_data): | |||
|
188 | handler = WebhookDataHandler( | |||
|
189 | template_url='http://server.com/${branch}/${commit_id}', | |||
|
190 | headers={'exmaple-header': 'header-values'} | |||
|
191 | ) | |||
|
192 | handler(event_type(*args), base_data) | |||
|
193 | ||||
|
194 | ||||
|
195 | ||||
|
196 |
@@ -331,7 +331,7 b' class TestCommits(BackendTestMixin):' | |||||
331 | assert line_no == 1 |
|
331 | assert line_no == 1 | |
332 | assert commit_id == file_added_commit.raw_id |
|
332 | assert commit_id == file_added_commit.raw_id | |
333 | assert commit_loader() == file_added_commit |
|
333 | assert commit_loader() == file_added_commit | |
334 | assert 'Foobar 3' in line |
|
334 | assert b'Foobar 3' in line | |
335 |
|
335 | |||
336 | def test_get_file_annotate_does_not_exist(self): |
|
336 | def test_get_file_annotate_does_not_exist(self): | |
337 | file_added_commit = self.repo.get_commit(commit_idx=2) |
|
337 | file_added_commit = self.repo.get_commit(commit_idx=2) |
General Comments 0
You need to be logged in to leave comments.
Login now