##// END OF EJS Templates
integrations: use a common logic for parsing the commits branches inside the integrations that require it....
dan -
r2644:97776fe4 default
parent child Browse files
Show More
@@ -1,288 +1,299 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import colander
21 import colander
22 import string
22 import string
23 import collections
23 import collections
24 import logging
24 import logging
25 from rhodecode.translation import _
25 from rhodecode.translation import _
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 class IntegrationTypeBase(object):
30 class IntegrationTypeBase(object):
31 """ Base class for IntegrationType plugins """
31 """ Base class for IntegrationType plugins """
32 is_dummy = False
32 is_dummy = False
33 description = ''
33 description = ''
34
34
35 @classmethod
35 @classmethod
36 def icon(cls):
36 def icon(cls):
37 return '''
37 return '''
38 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
38 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
39 <svg
39 <svg
40 xmlns:dc="http://purl.org/dc/elements/1.1/"
40 xmlns:dc="http://purl.org/dc/elements/1.1/"
41 xmlns:cc="http://creativecommons.org/ns#"
41 xmlns:cc="http://creativecommons.org/ns#"
42 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
42 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
43 xmlns:svg="http://www.w3.org/2000/svg"
43 xmlns:svg="http://www.w3.org/2000/svg"
44 xmlns="http://www.w3.org/2000/svg"
44 xmlns="http://www.w3.org/2000/svg"
45 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
45 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
46 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
46 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
47 viewBox="0 -256 1792 1792"
47 viewBox="0 -256 1792 1792"
48 id="svg3025"
48 id="svg3025"
49 version="1.1"
49 version="1.1"
50 inkscape:version="0.48.3.1 r9886"
50 inkscape:version="0.48.3.1 r9886"
51 width="100%"
51 width="100%"
52 height="100%"
52 height="100%"
53 sodipodi:docname="cog_font_awesome.svg">
53 sodipodi:docname="cog_font_awesome.svg">
54 <metadata
54 <metadata
55 id="metadata3035">
55 id="metadata3035">
56 <rdf:RDF>
56 <rdf:RDF>
57 <cc:Work
57 <cc:Work
58 rdf:about="">
58 rdf:about="">
59 <dc:format>image/svg+xml</dc:format>
59 <dc:format>image/svg+xml</dc:format>
60 <dc:type
60 <dc:type
61 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
61 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
62 </cc:Work>
62 </cc:Work>
63 </rdf:RDF>
63 </rdf:RDF>
64 </metadata>
64 </metadata>
65 <defs
65 <defs
66 id="defs3033" />
66 id="defs3033" />
67 <sodipodi:namedview
67 <sodipodi:namedview
68 pagecolor="#ffffff"
68 pagecolor="#ffffff"
69 bordercolor="#666666"
69 bordercolor="#666666"
70 borderopacity="1"
70 borderopacity="1"
71 objecttolerance="10"
71 objecttolerance="10"
72 gridtolerance="10"
72 gridtolerance="10"
73 guidetolerance="10"
73 guidetolerance="10"
74 inkscape:pageopacity="0"
74 inkscape:pageopacity="0"
75 inkscape:pageshadow="2"
75 inkscape:pageshadow="2"
76 inkscape:window-width="640"
76 inkscape:window-width="640"
77 inkscape:window-height="480"
77 inkscape:window-height="480"
78 id="namedview3031"
78 id="namedview3031"
79 showgrid="false"
79 showgrid="false"
80 inkscape:zoom="0.13169643"
80 inkscape:zoom="0.13169643"
81 inkscape:cx="896"
81 inkscape:cx="896"
82 inkscape:cy="896"
82 inkscape:cy="896"
83 inkscape:window-x="0"
83 inkscape:window-x="0"
84 inkscape:window-y="25"
84 inkscape:window-y="25"
85 inkscape:window-maximized="0"
85 inkscape:window-maximized="0"
86 inkscape:current-layer="svg3025" />
86 inkscape:current-layer="svg3025" />
87 <g
87 <g
88 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
88 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
89 id="g3027">
89 id="g3027">
90 <path
90 <path
91 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
91 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
92 id="path3029"
92 id="path3029"
93 inkscape:connector-curvature="0"
93 inkscape:connector-curvature="0"
94 style="fill:currentColor" />
94 style="fill:currentColor" />
95 </g>
95 </g>
96 </svg>
96 </svg>
97 '''
97 '''
98
98
99 def __init__(self, settings):
99 def __init__(self, settings):
100 """
100 """
101 :param settings: dict of settings to be used for the integration
101 :param settings: dict of settings to be used for the integration
102 """
102 """
103 self.settings = settings
103 self.settings = settings
104
104
105 def settings_schema(self):
105 def settings_schema(self):
106 """
106 """
107 A colander schema of settings for the integration type
107 A colander schema of settings for the integration type
108 """
108 """
109 return colander.Schema()
109 return colander.Schema()
110
110
111
111
112 class EEIntegration(IntegrationTypeBase):
112 class EEIntegration(IntegrationTypeBase):
113 description = 'Integration available in RhodeCode EE edition.'
113 description = 'Integration available in RhodeCode EE edition.'
114 is_dummy = True
114 is_dummy = True
115
115
116 def __init__(self, name, key, settings=None):
116 def __init__(self, name, key, settings=None):
117 self.display_name = name
117 self.display_name = name
118 self.key = key
118 self.key = key
119 super(EEIntegration, self).__init__(settings)
119 super(EEIntegration, self).__init__(settings)
120
120
121
121
122 # Helpers #
122 # Helpers #
123 WEBHOOK_URL_VARS = [
123 WEBHOOK_URL_VARS = [
124 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
124 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
125 ('repo_name', 'Full name of the repository'),
125 ('repo_name', 'Full name of the repository'),
126 ('repo_type', 'VCS type of repository'),
126 ('repo_type', 'VCS type of repository'),
127 ('repo_id', 'Unique id of repository'),
127 ('repo_id', 'Unique id of repository'),
128 ('repo_url', 'Repository url'),
128 ('repo_url', 'Repository url'),
129 # extra repo fields
129 # extra repo fields
130 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
130 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
131
131
132 # special attrs below that we handle, using multi-call
132 # special attrs below that we handle, using multi-call
133 ('branch', 'Name of each brach submitted, if any.'),
133 ('branch', 'Name of each brach submitted, if any.'),
134 ('commit_id', 'Id of each commit submitted, if any.'),
134 ('commit_id', 'Id of each commit submitted, if any.'),
135
135
136 # pr events vars
136 # pr events vars
137 ('pull_request_id', 'Unique ID of the pull request.'),
137 ('pull_request_id', 'Unique ID of the pull request.'),
138 ('pull_request_title', 'Title of the pull request.'),
138 ('pull_request_title', 'Title of the pull request.'),
139 ('pull_request_url', 'Pull request url.'),
139 ('pull_request_url', 'Pull request url.'),
140 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
140 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
141 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
141 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
142 'Changes after PR update'),
142 'Changes after PR update'),
143
143
144 # user who triggers the call
144 # user who triggers the call
145 ('username', 'User who triggered the call.'),
145 ('username', 'User who triggered the call.'),
146 ('user_id', 'User id who triggered the call.'),
146 ('user_id', 'User id who triggered the call.'),
147 ]
147 ]
148
148
149 # common vars for url template used for CI plugins. Shared with webhook
149 # common vars for url template used for CI plugins. Shared with webhook
150 CI_URL_VARS = WEBHOOK_URL_VARS
150 CI_URL_VARS = WEBHOOK_URL_VARS
151
151
152
152
153 class WebhookDataHandler(object):
153 class CommitParsingDataHandler(object):
154
155 def aggregate_branch_data(self, branches, commits):
156 branch_data = collections.OrderedDict()
157 for obj in branches:
158 branch_data[obj['name']] = obj
159
160 branches_commits = collections.OrderedDict()
161 for commit in commits:
162 if commit.get('git_ref_change'):
163 # special case for GIT that allows creating tags,
164 # deleting branches without associated commit
165 continue
166 commit_branch = commit['branch']
167
168 if commit_branch not in branches_commits:
169 _branch = branch_data[commit_branch] \
170 if commit_branch else commit_branch
171 branch_commits = {'branch': _branch,
172 'commits': []}
173 branches_commits[commit_branch] = branch_commits
174
175 branch_commits = branches_commits[commit_branch]
176 branch_commits['commits'].append(commit)
177 return branches_commits
178
179
180 class WebhookDataHandler(CommitParsingDataHandler):
154 name = 'webhook'
181 name = 'webhook'
155
182
156 def __init__(self, template_url, headers):
183 def __init__(self, template_url, headers):
157 self.template_url = template_url
184 self.template_url = template_url
158 self.headers = headers
185 self.headers = headers
159
186
160 def get_base_parsed_template(self, data):
187 def get_base_parsed_template(self, data):
161 """
188 """
162 initially parses the passed in template with some common variables
189 initially parses the passed in template with some common variables
163 available on ALL calls
190 available on ALL calls
164 """
191 """
165 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
192 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
166 common_vars = {
193 common_vars = {
167 'repo_name': data['repo']['repo_name'],
194 'repo_name': data['repo']['repo_name'],
168 'repo_type': data['repo']['repo_type'],
195 'repo_type': data['repo']['repo_type'],
169 'repo_id': data['repo']['repo_id'],
196 'repo_id': data['repo']['repo_id'],
170 'repo_url': data['repo']['url'],
197 'repo_url': data['repo']['url'],
171 'username': data['actor']['username'],
198 'username': data['actor']['username'],
172 'user_id': data['actor']['user_id'],
199 'user_id': data['actor']['user_id'],
173 'event_name': data['name']
200 'event_name': data['name']
174 }
201 }
175
202
176 extra_vars = {}
203 extra_vars = {}
177 for extra_key, extra_val in data['repo']['extra_fields'].items():
204 for extra_key, extra_val in data['repo']['extra_fields'].items():
178 extra_vars['extra__{}'.format(extra_key)] = extra_val
205 extra_vars['extra__{}'.format(extra_key)] = extra_val
179 common_vars.update(extra_vars)
206 common_vars.update(extra_vars)
180
207
181 template_url = self.template_url.replace('${extra:', '${extra__')
208 template_url = self.template_url.replace('${extra:', '${extra__')
182 return string.Template(template_url).safe_substitute(**common_vars)
209 return string.Template(template_url).safe_substitute(**common_vars)
183
210
184 def repo_push_event_handler(self, event, data):
211 def repo_push_event_handler(self, event, data):
185 url = self.get_base_parsed_template(data)
212 url = self.get_base_parsed_template(data)
186 url_cals = []
213 url_cals = []
187 branch_data = collections.OrderedDict()
188 for obj in data['push']['branches']:
189 branch_data[obj['name']] = obj
190
214
191 branches_commits = collections.OrderedDict()
215 branches_commits = self.aggregate_branch_data(
192 for commit in data['push']['commits']:
216 data['push']['branches'], data['push']['commits'])
193 if commit.get('git_ref_change'):
194 # special case for GIT that allows creating tags,
195 # deleting branches without associated commit
196 continue
197
198 if commit['branch'] not in branches_commits:
199 branch_commits = {'branch': branch_data[commit['branch']],
200 'commits': []}
201 branches_commits[commit['branch']] = branch_commits
202
203 branch_commits = branches_commits[commit['branch']]
204 branch_commits['commits'].append(commit)
205
206 if '${branch}' in url:
217 if '${branch}' in url:
207 # call it multiple times, for each branch if used in variables
218 # call it multiple times, for each branch if used in variables
208 for branch, commit_ids in branches_commits.items():
219 for branch, commit_ids in branches_commits.items():
209 branch_url = string.Template(url).safe_substitute(branch=branch)
220 branch_url = string.Template(url).safe_substitute(branch=branch)
210 # call further down for each commit if used
221 # call further down for each commit if used
211 if '${commit_id}' in branch_url:
222 if '${commit_id}' in branch_url:
212 for commit_data in commit_ids['commits']:
223 for commit_data in commit_ids['commits']:
213 commit_id = commit_data['raw_id']
224 commit_id = commit_data['raw_id']
214 commit_url = string.Template(branch_url).safe_substitute(
225 commit_url = string.Template(branch_url).safe_substitute(
215 commit_id=commit_id)
226 commit_id=commit_id)
216 # register per-commit call
227 # register per-commit call
217 log.debug(
228 log.debug(
218 'register %s call(%s) to url %s',
229 'register %s call(%s) to url %s',
219 self.name, event, commit_url)
230 self.name, event, commit_url)
220 url_cals.append(
231 url_cals.append(
221 (commit_url, self.headers, data))
232 (commit_url, self.headers, data))
222
233
223 else:
234 else:
224 # register per-branch call
235 # register per-branch call
225 log.debug(
236 log.debug(
226 'register %s call(%s) to url %s',
237 'register %s call(%s) to url %s',
227 self.name, event, branch_url)
238 self.name, event, branch_url)
228 url_cals.append(
239 url_cals.append(
229 (branch_url, self.headers, data))
240 (branch_url, self.headers, data))
230
241
231 else:
242 else:
232 log.debug(
243 log.debug(
233 'register %s call(%s) to url %s', self.name, event, url)
244 'register %s call(%s) to url %s', self.name, event, url)
234 url_cals.append((url, self.headers, data))
245 url_cals.append((url, self.headers, data))
235
246
236 return url_cals
247 return url_cals
237
248
238 def repo_create_event_handler(self, event, data):
249 def repo_create_event_handler(self, event, data):
239 url = self.get_base_parsed_template(data)
250 url = self.get_base_parsed_template(data)
240 log.debug(
251 log.debug(
241 'register %s call(%s) to url %s', self.name, event, url)
252 'register %s call(%s) to url %s', self.name, event, url)
242 return [(url, self.headers, data)]
253 return [(url, self.headers, data)]
243
254
244 def pull_request_event_handler(self, event, data):
255 def pull_request_event_handler(self, event, data):
245 url = self.get_base_parsed_template(data)
256 url = self.get_base_parsed_template(data)
246 log.debug(
257 log.debug(
247 'register %s call(%s) to url %s', self.name, event, url)
258 'register %s call(%s) to url %s', self.name, event, url)
248 url = string.Template(url).safe_substitute(
259 url = string.Template(url).safe_substitute(
249 pull_request_id=data['pullrequest']['pull_request_id'],
260 pull_request_id=data['pullrequest']['pull_request_id'],
250 pull_request_title=data['pullrequest']['title'],
261 pull_request_title=data['pullrequest']['title'],
251 pull_request_url=data['pullrequest']['url'],
262 pull_request_url=data['pullrequest']['url'],
252 pull_request_shadow_url=data['pullrequest']['shadow_url'],
263 pull_request_shadow_url=data['pullrequest']['shadow_url'],
253 pull_request_commits_uid=data['pullrequest']['commits_uid'],
264 pull_request_commits_uid=data['pullrequest']['commits_uid'],
254 )
265 )
255 return [(url, self.headers, data)]
266 return [(url, self.headers, data)]
256
267
257 def __call__(self, event, data):
268 def __call__(self, event, data):
258 from rhodecode import events
269 from rhodecode import events
259
270
260 if isinstance(event, events.RepoPushEvent):
271 if isinstance(event, events.RepoPushEvent):
261 return self.repo_push_event_handler(event, data)
272 return self.repo_push_event_handler(event, data)
262 elif isinstance(event, events.RepoCreateEvent):
273 elif isinstance(event, events.RepoCreateEvent):
263 return self.repo_create_event_handler(event, data)
274 return self.repo_create_event_handler(event, data)
264 elif isinstance(event, events.PullRequestEvent):
275 elif isinstance(event, events.PullRequestEvent):
265 return self.pull_request_event_handler(event, data)
276 return self.pull_request_event_handler(event, data)
266 else:
277 else:
267 raise ValueError(
278 raise ValueError(
268 'event type `%s` not in supported list: %s' % (
279 'event type `%s` not in supported list: %s' % (
269 event.__class__, events))
280 event.__class__, events))
270
281
271
282
272 def get_auth(settings):
283 def get_auth(settings):
273 from requests.auth import HTTPBasicAuth
284 from requests.auth import HTTPBasicAuth
274 username = settings.get('username')
285 username = settings.get('username')
275 password = settings.get('password')
286 password = settings.get('password')
276 if username and password:
287 if username and password:
277 return HTTPBasicAuth(username, password)
288 return HTTPBasicAuth(username, password)
278 return None
289 return None
279
290
280
291
281 def get_web_token(settings):
292 def get_web_token(settings):
282 return settings['secret_token']
293 return settings['secret_token']
283
294
284
295
285 def get_url_vars(url_vars):
296 def get_url_vars(url_vars):
286 return '\n'.join(
297 return '\n'.join(
287 '{} - {}'.format('${' + key + '}', explanation)
298 '{} - {}'.format('${' + key + '}', explanation)
288 for key, explanation in url_vars)
299 for key, explanation in url_vars)
@@ -1,256 +1,246 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import deform
22 import deform
23 import logging
23 import logging
24 import requests
24 import requests
25 import colander
25 import colander
26 import textwrap
26 import textwrap
27 from collections import OrderedDict
28 from mako.template import Template
27 from mako.template import Template
29 from rhodecode import events
28 from rhodecode import events
30 from rhodecode.translation import _
29 from rhodecode.translation import _
31 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
32 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
31 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
33 from rhodecode.lib.colander_utils import strip_whitespace
32 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.types.base import (
34 IntegrationTypeBase, CommitParsingDataHandler)
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class HipchatSettingsSchema(colander.Schema):
39 class HipchatSettingsSchema(colander.Schema):
40 color_choices = [
40 color_choices = [
41 ('yellow', _('Yellow')),
41 ('yellow', _('Yellow')),
42 ('red', _('Red')),
42 ('red', _('Red')),
43 ('green', _('Green')),
43 ('green', _('Green')),
44 ('purple', _('Purple')),
44 ('purple', _('Purple')),
45 ('gray', _('Gray')),
45 ('gray', _('Gray')),
46 ]
46 ]
47
47
48 server_url = colander.SchemaNode(
48 server_url = colander.SchemaNode(
49 colander.String(),
49 colander.String(),
50 title=_('Hipchat server URL'),
50 title=_('Hipchat server URL'),
51 description=_('Hipchat integration url.'),
51 description=_('Hipchat integration url.'),
52 default='',
52 default='',
53 preparer=strip_whitespace,
53 preparer=strip_whitespace,
54 validator=colander.url,
54 validator=colander.url,
55 widget=deform.widget.TextInputWidget(
55 widget=deform.widget.TextInputWidget(
56 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
56 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
57 ),
57 ),
58 )
58 )
59 notify = colander.SchemaNode(
59 notify = colander.SchemaNode(
60 colander.Bool(),
60 colander.Bool(),
61 title=_('Notify'),
61 title=_('Notify'),
62 description=_('Make a notification to the users in room.'),
62 description=_('Make a notification to the users in room.'),
63 missing=False,
63 missing=False,
64 default=False,
64 default=False,
65 )
65 )
66 color = colander.SchemaNode(
66 color = colander.SchemaNode(
67 colander.String(),
67 colander.String(),
68 title=_('Color'),
68 title=_('Color'),
69 description=_('Background color of message.'),
69 description=_('Background color of message.'),
70 missing='',
70 missing='',
71 validator=colander.OneOf([x[0] for x in color_choices]),
71 validator=colander.OneOf([x[0] for x in color_choices]),
72 widget=deform.widget.Select2Widget(
72 widget=deform.widget.Select2Widget(
73 values=color_choices,
73 values=color_choices,
74 ),
74 ),
75 )
75 )
76
76
77
77
78 repo_push_template = Template('''
78 repo_push_template = Template('''
79 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
79 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
80 <br>
80 <br>
81 <ul>
81 <ul>
82 %for branch, branch_commits in branches_commits.items():
82 %for branch, branch_commits in branches_commits.items():
83 <li>
83 <li>
84 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
84 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
85 <ul>
85 <ul>
86 %for commit in branch_commits['commits']:
86 %for commit in branch_commits['commits']:
87 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
87 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
88 %endfor
88 %endfor
89 </ul>
89 </ul>
90 </li>
90 </li>
91 %endfor
91 %endfor
92 ''')
92 ''')
93
93
94
94
95 class HipchatIntegrationType(IntegrationTypeBase):
95 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
96 key = 'hipchat'
96 key = 'hipchat'
97 display_name = _('Hipchat')
97 display_name = _('Hipchat')
98 description = _('Send events such as repo pushes and pull requests to '
98 description = _('Send events such as repo pushes and pull requests to '
99 'your hipchat channel.')
99 'your hipchat channel.')
100
100
101 @classmethod
101 @classmethod
102 def icon(cls):
102 def icon(cls):
103 return '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
103 return '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
104
104
105 valid_events = [
105 valid_events = [
106 events.PullRequestCloseEvent,
106 events.PullRequestCloseEvent,
107 events.PullRequestMergeEvent,
107 events.PullRequestMergeEvent,
108 events.PullRequestUpdateEvent,
108 events.PullRequestUpdateEvent,
109 events.PullRequestCommentEvent,
109 events.PullRequestCommentEvent,
110 events.PullRequestReviewEvent,
110 events.PullRequestReviewEvent,
111 events.PullRequestCreateEvent,
111 events.PullRequestCreateEvent,
112 events.RepoPushEvent,
112 events.RepoPushEvent,
113 events.RepoCreateEvent,
113 events.RepoCreateEvent,
114 ]
114 ]
115
115
116 def send_event(self, event):
116 def send_event(self, event):
117 if event.__class__ not in self.valid_events:
117 if event.__class__ not in self.valid_events:
118 log.debug('event not valid: %r' % event)
118 log.debug('event not valid: %r' % event)
119 return
119 return
120
120
121 if event.name not in self.settings['events']:
121 if event.name not in self.settings['events']:
122 log.debug('event ignored: %r' % event)
122 log.debug('event ignored: %r' % event)
123 return
123 return
124
124
125 data = event.as_dict()
125 data = event.as_dict()
126
126
127 text = '<b>%s<b> caused a <b>%s</b> event' % (
127 text = '<b>%s<b> caused a <b>%s</b> event' % (
128 data['actor']['username'], event.name)
128 data['actor']['username'], event.name)
129
129
130 log.debug('handling hipchat event for %s' % event.name)
130 log.debug('handling hipchat event for %s' % event.name)
131
131
132 if isinstance(event, events.PullRequestCommentEvent):
132 if isinstance(event, events.PullRequestCommentEvent):
133 text = self.format_pull_request_comment_event(event, data)
133 text = self.format_pull_request_comment_event(event, data)
134 elif isinstance(event, events.PullRequestReviewEvent):
134 elif isinstance(event, events.PullRequestReviewEvent):
135 text = self.format_pull_request_review_event(event, data)
135 text = self.format_pull_request_review_event(event, data)
136 elif isinstance(event, events.PullRequestEvent):
136 elif isinstance(event, events.PullRequestEvent):
137 text = self.format_pull_request_event(event, data)
137 text = self.format_pull_request_event(event, data)
138 elif isinstance(event, events.RepoPushEvent):
138 elif isinstance(event, events.RepoPushEvent):
139 text = self.format_repo_push_event(data)
139 text = self.format_repo_push_event(data)
140 elif isinstance(event, events.RepoCreateEvent):
140 elif isinstance(event, events.RepoCreateEvent):
141 text = self.format_repo_create_event(data)
141 text = self.format_repo_create_event(data)
142 else:
142 else:
143 log.error('unhandled event type: %r' % event)
143 log.error('unhandled event type: %r' % event)
144
144
145 run_task(post_text_to_hipchat, self.settings, text)
145 run_task(post_text_to_hipchat, self.settings, text)
146
146
147 def settings_schema(self):
147 def settings_schema(self):
148 schema = HipchatSettingsSchema()
148 schema = HipchatSettingsSchema()
149 schema.add(colander.SchemaNode(
149 schema.add(colander.SchemaNode(
150 colander.Set(),
150 colander.Set(),
151 widget=deform.widget.CheckboxChoiceWidget(
151 widget=deform.widget.CheckboxChoiceWidget(
152 values=sorted(
152 values=sorted(
153 [(e.name, e.display_name) for e in self.valid_events]
153 [(e.name, e.display_name) for e in self.valid_events]
154 )
154 )
155 ),
155 ),
156 description="Events activated for this integration",
156 description="Events activated for this integration",
157 name='events'
157 name='events'
158 ))
158 ))
159
159
160 return schema
160 return schema
161
161
162 def format_pull_request_comment_event(self, event, data):
162 def format_pull_request_comment_event(self, event, data):
163 comment_text = data['comment']['text']
163 comment_text = data['comment']['text']
164 if len(comment_text) > 200:
164 if len(comment_text) > 200:
165 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
165 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
166 comment_text=h.html_escape(comment_text[:200]),
166 comment_text=h.html_escape(comment_text[:200]),
167 comment_url=data['comment']['url'],
167 comment_url=data['comment']['url'],
168 )
168 )
169
169
170 comment_status = ''
170 comment_status = ''
171 if data['comment']['status']:
171 if data['comment']['status']:
172 comment_status = '[{}]: '.format(data['comment']['status'])
172 comment_status = '[{}]: '.format(data['comment']['status'])
173
173
174 return (textwrap.dedent(
174 return (textwrap.dedent(
175 '''
175 '''
176 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
176 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
177 >>> {comment_status}{comment_text}
177 >>> {comment_status}{comment_text}
178 ''').format(
178 ''').format(
179 comment_status=comment_status,
179 comment_status=comment_status,
180 user=data['actor']['username'],
180 user=data['actor']['username'],
181 number=data['pullrequest']['pull_request_id'],
181 number=data['pullrequest']['pull_request_id'],
182 pr_url=data['pullrequest']['url'],
182 pr_url=data['pullrequest']['url'],
183 pr_status=data['pullrequest']['status'],
183 pr_status=data['pullrequest']['status'],
184 pr_title=h.html_escape(data['pullrequest']['title']),
184 pr_title=h.html_escape(data['pullrequest']['title']),
185 comment_text=h.html_escape(comment_text)
185 comment_text=h.html_escape(comment_text)
186 )
186 )
187 )
187 )
188
188
189 def format_pull_request_review_event(self, event, data):
189 def format_pull_request_review_event(self, event, data):
190 return (textwrap.dedent(
190 return (textwrap.dedent(
191 '''
191 '''
192 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
192 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
193 ''').format(
193 ''').format(
194 user=data['actor']['username'],
194 user=data['actor']['username'],
195 number=data['pullrequest']['pull_request_id'],
195 number=data['pullrequest']['pull_request_id'],
196 pr_url=data['pullrequest']['url'],
196 pr_url=data['pullrequest']['url'],
197 pr_status=data['pullrequest']['status'],
197 pr_status=data['pullrequest']['status'],
198 pr_title=h.html_escape(data['pullrequest']['title']),
198 pr_title=h.html_escape(data['pullrequest']['title']),
199 )
199 )
200 )
200 )
201
201
202 def format_pull_request_event(self, event, data):
202 def format_pull_request_event(self, event, data):
203 action = {
203 action = {
204 events.PullRequestCloseEvent: 'closed',
204 events.PullRequestCloseEvent: 'closed',
205 events.PullRequestMergeEvent: 'merged',
205 events.PullRequestMergeEvent: 'merged',
206 events.PullRequestUpdateEvent: 'updated',
206 events.PullRequestUpdateEvent: 'updated',
207 events.PullRequestCreateEvent: 'created',
207 events.PullRequestCreateEvent: 'created',
208 }.get(event.__class__, str(event.__class__))
208 }.get(event.__class__, str(event.__class__))
209
209
210 return ('Pull request <a href="{url}">#{number}</a> - {title} '
210 return ('Pull request <a href="{url}">#{number}</a> - {title} '
211 '{action} by <b>{user}</b>').format(
211 '{action} by <b>{user}</b>').format(
212 user=data['actor']['username'],
212 user=data['actor']['username'],
213 number=data['pullrequest']['pull_request_id'],
213 number=data['pullrequest']['pull_request_id'],
214 url=data['pullrequest']['url'],
214 url=data['pullrequest']['url'],
215 title=h.html_escape(data['pullrequest']['title']),
215 title=h.html_escape(data['pullrequest']['title']),
216 action=action
216 action=action
217 )
217 )
218
218
219 def format_repo_push_event(self, data):
219 def format_repo_push_event(self, data):
220 branch_data = {branch['name']: branch
220 branches_commits = self.aggregate_branch_data(
221 for branch in data['push']['branches']}
221 data['push']['branches'], data['push']['commits'])
222
223 branches_commits = OrderedDict()
224 for commit in data['push']['commits']:
225 if commit['branch'] not in branches_commits:
226 branch_commits = {'branch': branch_data[commit['branch']],
227 'commits': []}
228 branches_commits[commit['branch']] = branch_commits
229
230 branch_commits = branches_commits[commit['branch']]
231 branch_commits['commits'].append(commit)
232
222
233 result = repo_push_template.render(
223 result = repo_push_template.render(
234 data=data,
224 data=data,
235 branches_commits=branches_commits,
225 branches_commits=branches_commits,
236 )
226 )
237 return result
227 return result
238
228
239 def format_repo_create_event(self, data):
229 def format_repo_create_event(self, data):
240 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
230 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
241 data['repo']['url'],
231 data['repo']['url'],
242 h.html_escape(data['repo']['repo_name']),
232 h.html_escape(data['repo']['repo_name']),
243 data['repo']['repo_type'],
233 data['repo']['repo_type'],
244 data['actor']['username'],
234 data['actor']['username'],
245 )
235 )
246
236
247
237
248 @async_task(ignore_result=True, base=RequestContextTask)
238 @async_task(ignore_result=True, base=RequestContextTask)
249 def post_text_to_hipchat(settings, text):
239 def post_text_to_hipchat(settings, text):
250 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
240 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
251 resp = requests.post(settings['server_url'], json={
241 resp = requests.post(settings['server_url'], json={
252 "message": text,
242 "message": text,
253 "color": settings.get('color', 'yellow'),
243 "color": settings.get('color', 'yellow'),
254 "notify": settings.get('notify', False),
244 "notify": settings.get('notify', False),
255 })
245 })
256 resp.raise_for_status() # raise exception on a failed request
246 resp.raise_for_status() # raise exception on a failed request
@@ -1,338 +1,329 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import re
22 import re
23 import time
23 import time
24 import textwrap
24 import textwrap
25 import logging
25 import logging
26
26
27 import deform
27 import deform
28 import requests
28 import requests
29 import colander
29 import colander
30 from mako.template import Template
30 from mako.template import Template
31 from collections import OrderedDict
32
31
33 from rhodecode import events
32 from rhodecode import events
34 from rhodecode.translation import _
33 from rhodecode.translation import _
35 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
36 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
37 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.types.base import (
38 IntegrationTypeBase, CommitParsingDataHandler)
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class SlackSettingsSchema(colander.Schema):
43 class SlackSettingsSchema(colander.Schema):
44 service = colander.SchemaNode(
44 service = colander.SchemaNode(
45 colander.String(),
45 colander.String(),
46 title=_('Slack service URL'),
46 title=_('Slack service URL'),
47 description=h.literal(_(
47 description=h.literal(_(
48 'This can be setup at the '
48 'This can be setup at the '
49 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
50 'slack app manager</a>')),
50 'slack app manager</a>')),
51 default='',
51 default='',
52 preparer=strip_whitespace,
52 preparer=strip_whitespace,
53 validator=colander.url,
53 validator=colander.url,
54 widget=deform.widget.TextInputWidget(
54 widget=deform.widget.TextInputWidget(
55 placeholder='https://hooks.slack.com/services/...',
55 placeholder='https://hooks.slack.com/services/...',
56 ),
56 ),
57 )
57 )
58 username = colander.SchemaNode(
58 username = colander.SchemaNode(
59 colander.String(),
59 colander.String(),
60 title=_('Username'),
60 title=_('Username'),
61 description=_('Username to show notifications coming from.'),
61 description=_('Username to show notifications coming from.'),
62 missing='Rhodecode',
62 missing='Rhodecode',
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 widget=deform.widget.TextInputWidget(
64 widget=deform.widget.TextInputWidget(
65 placeholder='Rhodecode'
65 placeholder='Rhodecode'
66 ),
66 ),
67 )
67 )
68 channel = colander.SchemaNode(
68 channel = colander.SchemaNode(
69 colander.String(),
69 colander.String(),
70 title=_('Channel'),
70 title=_('Channel'),
71 description=_('Channel to send notifications to.'),
71 description=_('Channel to send notifications to.'),
72 missing='',
72 missing='',
73 preparer=strip_whitespace,
73 preparer=strip_whitespace,
74 widget=deform.widget.TextInputWidget(
74 widget=deform.widget.TextInputWidget(
75 placeholder='#general'
75 placeholder='#general'
76 ),
76 ),
77 )
77 )
78 icon_emoji = colander.SchemaNode(
78 icon_emoji = colander.SchemaNode(
79 colander.String(),
79 colander.String(),
80 title=_('Emoji'),
80 title=_('Emoji'),
81 description=_('Emoji to use eg. :studio_microphone:'),
81 description=_('Emoji to use eg. :studio_microphone:'),
82 missing='',
82 missing='',
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 widget=deform.widget.TextInputWidget(
84 widget=deform.widget.TextInputWidget(
85 placeholder=':studio_microphone:'
85 placeholder=':studio_microphone:'
86 ),
86 ),
87 )
87 )
88
88
89
89
90 class SlackIntegrationType(IntegrationTypeBase):
90 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
91 key = 'slack'
91 key = 'slack'
92 display_name = _('Slack')
92 display_name = _('Slack')
93 description = _('Send events such as repo pushes and pull requests to '
93 description = _('Send events such as repo pushes and pull requests to '
94 'your slack channel.')
94 'your slack channel.')
95
95
96 @classmethod
96 @classmethod
97 def icon(cls):
97 def icon(cls):
98 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
98 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
99
99
100 valid_events = [
100 valid_events = [
101 events.PullRequestCloseEvent,
101 events.PullRequestCloseEvent,
102 events.PullRequestMergeEvent,
102 events.PullRequestMergeEvent,
103 events.PullRequestUpdateEvent,
103 events.PullRequestUpdateEvent,
104 events.PullRequestCommentEvent,
104 events.PullRequestCommentEvent,
105 events.PullRequestReviewEvent,
105 events.PullRequestReviewEvent,
106 events.PullRequestCreateEvent,
106 events.PullRequestCreateEvent,
107 events.RepoPushEvent,
107 events.RepoPushEvent,
108 events.RepoCreateEvent,
108 events.RepoCreateEvent,
109 ]
109 ]
110
110
111 def send_event(self, event):
111 def send_event(self, event):
112 if event.__class__ not in self.valid_events:
112 if event.__class__ not in self.valid_events:
113 log.debug('event not valid: %r' % event)
113 log.debug('event not valid: %r' % event)
114 return
114 return
115
115
116 if event.name not in self.settings['events']:
116 if event.name not in self.settings['events']:
117 log.debug('event ignored: %r' % event)
117 log.debug('event ignored: %r' % event)
118 return
118 return
119
119
120 data = event.as_dict()
120 data = event.as_dict()
121
121
122 # defaults
122 # defaults
123 title = '*%s* caused a *%s* event' % (
123 title = '*%s* caused a *%s* event' % (
124 data['actor']['username'], event.name)
124 data['actor']['username'], event.name)
125 text = '*%s* caused a *%s* event' % (
125 text = '*%s* caused a *%s* event' % (
126 data['actor']['username'], event.name)
126 data['actor']['username'], event.name)
127 fields = None
127 fields = None
128 overrides = None
128 overrides = None
129
129
130 log.debug('handling slack event for %s' % event.name)
130 log.debug('handling slack event for %s' % event.name)
131
131
132 if isinstance(event, events.PullRequestCommentEvent):
132 if isinstance(event, events.PullRequestCommentEvent):
133 (title, text, fields, overrides) \
133 (title, text, fields, overrides) \
134 = self.format_pull_request_comment_event(event, data)
134 = self.format_pull_request_comment_event(event, data)
135 elif isinstance(event, events.PullRequestReviewEvent):
135 elif isinstance(event, events.PullRequestReviewEvent):
136 title, text = self.format_pull_request_review_event(event, data)
136 title, text = self.format_pull_request_review_event(event, data)
137 elif isinstance(event, events.PullRequestEvent):
137 elif isinstance(event, events.PullRequestEvent):
138 title, text = self.format_pull_request_event(event, data)
138 title, text = self.format_pull_request_event(event, data)
139 elif isinstance(event, events.RepoPushEvent):
139 elif isinstance(event, events.RepoPushEvent):
140 title, text = self.format_repo_push_event(data)
140 title, text = self.format_repo_push_event(data)
141 elif isinstance(event, events.RepoCreateEvent):
141 elif isinstance(event, events.RepoCreateEvent):
142 title, text = self.format_repo_create_event(data)
142 title, text = self.format_repo_create_event(data)
143 else:
143 else:
144 log.error('unhandled event type: %r' % event)
144 log.error('unhandled event type: %r' % event)
145
145
146 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
146 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
147
147
148 def settings_schema(self):
148 def settings_schema(self):
149 schema = SlackSettingsSchema()
149 schema = SlackSettingsSchema()
150 schema.add(colander.SchemaNode(
150 schema.add(colander.SchemaNode(
151 colander.Set(),
151 colander.Set(),
152 widget=deform.widget.CheckboxChoiceWidget(
152 widget=deform.widget.CheckboxChoiceWidget(
153 values=sorted(
153 values=sorted(
154 [(e.name, e.display_name) for e in self.valid_events]
154 [(e.name, e.display_name) for e in self.valid_events]
155 )
155 )
156 ),
156 ),
157 description="Events activated for this integration",
157 description="Events activated for this integration",
158 name='events'
158 name='events'
159 ))
159 ))
160
160
161 return schema
161 return schema
162
162
163 def format_pull_request_comment_event(self, event, data):
163 def format_pull_request_comment_event(self, event, data):
164 comment_text = data['comment']['text']
164 comment_text = data['comment']['text']
165 if len(comment_text) > 200:
165 if len(comment_text) > 200:
166 comment_text = '<{comment_url}|{comment_text}...>'.format(
166 comment_text = '<{comment_url}|{comment_text}...>'.format(
167 comment_text=comment_text[:200],
167 comment_text=comment_text[:200],
168 comment_url=data['comment']['url'],
168 comment_url=data['comment']['url'],
169 )
169 )
170
170
171 fields = None
171 fields = None
172 overrides = None
172 overrides = None
173 status_text = None
173 status_text = None
174
174
175 if data['comment']['status']:
175 if data['comment']['status']:
176 status_color = {
176 status_color = {
177 'approved': '#0ac878',
177 'approved': '#0ac878',
178 'rejected': '#e85e4d'}.get(data['comment']['status'])
178 'rejected': '#e85e4d'}.get(data['comment']['status'])
179
179
180 if status_color:
180 if status_color:
181 overrides = {"color": status_color}
181 overrides = {"color": status_color}
182
182
183 status_text = data['comment']['status']
183 status_text = data['comment']['status']
184
184
185 if data['comment']['file']:
185 if data['comment']['file']:
186 fields = [
186 fields = [
187 {
187 {
188 "title": "file",
188 "title": "file",
189 "value": data['comment']['file']
189 "value": data['comment']['file']
190 },
190 },
191 {
191 {
192 "title": "line",
192 "title": "line",
193 "value": data['comment']['line']
193 "value": data['comment']['line']
194 }
194 }
195 ]
195 ]
196
196
197 title = Template(textwrap.dedent(r'''
197 title = Template(textwrap.dedent(r'''
198 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
198 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
199 ''')).render(data=data, comment=event.comment)
199 ''')).render(data=data, comment=event.comment)
200
200
201 text = Template(textwrap.dedent(r'''
201 text = Template(textwrap.dedent(r'''
202 *pull request title*: ${pr_title}
202 *pull request title*: ${pr_title}
203 % if status_text:
203 % if status_text:
204 *submitted status*: `${status_text}`
204 *submitted status*: `${status_text}`
205 % endif
205 % endif
206 >>> ${comment_text}
206 >>> ${comment_text}
207 ''')).render(comment_text=comment_text,
207 ''')).render(comment_text=comment_text,
208 pr_title=data['pullrequest']['title'],
208 pr_title=data['pullrequest']['title'],
209 status_text=status_text)
209 status_text=status_text)
210
210
211 return title, text, fields, overrides
211 return title, text, fields, overrides
212
212
213 def format_pull_request_review_event(self, event, data):
213 def format_pull_request_review_event(self, event, data):
214 title = Template(textwrap.dedent(r'''
214 title = Template(textwrap.dedent(r'''
215 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
215 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
216 ''')).render(data=data)
216 ''')).render(data=data)
217
217
218 text = Template(textwrap.dedent(r'''
218 text = Template(textwrap.dedent(r'''
219 *pull request title*: ${pr_title}
219 *pull request title*: ${pr_title}
220 ''')).render(
220 ''')).render(
221 pr_title=data['pullrequest']['title'],
221 pr_title=data['pullrequest']['title'],
222 )
222 )
223
223
224 return title, text
224 return title, text
225
225
226 def format_pull_request_event(self, event, data):
226 def format_pull_request_event(self, event, data):
227 action = {
227 action = {
228 events.PullRequestCloseEvent: 'closed',
228 events.PullRequestCloseEvent: 'closed',
229 events.PullRequestMergeEvent: 'merged',
229 events.PullRequestMergeEvent: 'merged',
230 events.PullRequestUpdateEvent: 'updated',
230 events.PullRequestUpdateEvent: 'updated',
231 events.PullRequestCreateEvent: 'created',
231 events.PullRequestCreateEvent: 'created',
232 }.get(event.__class__, str(event.__class__))
232 }.get(event.__class__, str(event.__class__))
233
233
234 title = Template(textwrap.dedent(r'''
234 title = Template(textwrap.dedent(r'''
235 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
235 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
236 ''')).render(data=data, action=action)
236 ''')).render(data=data, action=action)
237
237
238 text = Template(textwrap.dedent(r'''
238 text = Template(textwrap.dedent(r'''
239 *pull request title*: ${pr_title}
239 *pull request title*: ${pr_title}
240 %if data['pullrequest']['commits']:
240 %if data['pullrequest']['commits']:
241 *commits*: ${len(data['pullrequest']['commits'])}
241 *commits*: ${len(data['pullrequest']['commits'])}
242 %endif
242 %endif
243 ''')).render(
243 ''')).render(
244 pr_title=data['pullrequest']['title'],
244 pr_title=data['pullrequest']['title'],
245 data=data
245 data=data
246 )
246 )
247
247
248 return title, text
248 return title, text
249
249
250 def format_repo_push_event(self, data):
250 def format_repo_push_event(self, data):
251 branch_data = {branch['name']: branch
252 for branch in data['push']['branches']}
253
251
254 branches_commits = OrderedDict()
252 branches_commits = self.aggregate_branch_data(
255 for commit in data['push']['commits']:
253 data['push']['branches'], data['push']['commits'])
256 if commit['branch'] not in branches_commits:
257 branch_commits = {'branch': branch_data[commit['branch']],
258 'commits': []}
259 branches_commits[commit['branch']] = branch_commits
260
261 branch_commits = branches_commits[commit['branch']]
262 branch_commits['commits'].append(commit)
263
254
264 title = Template(r'''
255 title = Template(r'''
265 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
256 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
266 ''').render(data=data)
257 ''').render(data=data)
267
258
268 repo_push_template = Template(textwrap.dedent(r'''
259 repo_push_template = Template(textwrap.dedent(r'''
269 %for branch, branch_commits in branches_commits.items():
260 %for branch, branch_commits in branches_commits.items():
270 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
261 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
271 %for commit in branch_commits['commits']:
262 %for commit in branch_commits['commits']:
272 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
263 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
273 %endfor
264 %endfor
274 %endfor
265 %endfor
275 '''))
266 '''))
276
267
277 text = repo_push_template.render(
268 text = repo_push_template.render(
278 data=data,
269 data=data,
279 branches_commits=branches_commits,
270 branches_commits=branches_commits,
280 html_to_slack_links=html_to_slack_links,
271 html_to_slack_links=html_to_slack_links,
281 )
272 )
282
273
283 return title, text
274 return title, text
284
275
285 def format_repo_create_event(self, data):
276 def format_repo_create_event(self, data):
286 title = Template(r'''
277 title = Template(r'''
287 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
278 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
288 ''').render(data=data)
279 ''').render(data=data)
289
280
290 text = Template(textwrap.dedent(r'''
281 text = Template(textwrap.dedent(r'''
291 repo_url: ${data['repo']['url']}
282 repo_url: ${data['repo']['url']}
292 repo_type: ${data['repo']['repo_type']}
283 repo_type: ${data['repo']['repo_type']}
293 ''')).render(data=data)
284 ''')).render(data=data)
294
285
295 return title, text
286 return title, text
296
287
297
288
298 def html_to_slack_links(message):
289 def html_to_slack_links(message):
299 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
290 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
300 r'<\1|\2>', message)
291 r'<\1|\2>', message)
301
292
302
293
303 @async_task(ignore_result=True, base=RequestContextTask)
294 @async_task(ignore_result=True, base=RequestContextTask)
304 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
295 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
305 log.debug('sending %s (%s) to slack %s' % (
296 log.debug('sending %s (%s) to slack %s' % (
306 title, text, settings['service']))
297 title, text, settings['service']))
307
298
308 fields = fields or []
299 fields = fields or []
309 overrides = overrides or {}
300 overrides = overrides or {}
310
301
311 message_data = {
302 message_data = {
312 "fallback": text,
303 "fallback": text,
313 "color": "#427cc9",
304 "color": "#427cc9",
314 "pretext": title,
305 "pretext": title,
315 #"author_name": "Bobby Tables",
306 #"author_name": "Bobby Tables",
316 #"author_link": "http://flickr.com/bobby/",
307 #"author_link": "http://flickr.com/bobby/",
317 #"author_icon": "http://flickr.com/icons/bobby.jpg",
308 #"author_icon": "http://flickr.com/icons/bobby.jpg",
318 #"title": "Slack API Documentation",
309 #"title": "Slack API Documentation",
319 #"title_link": "https://api.slack.com/",
310 #"title_link": "https://api.slack.com/",
320 "text": text,
311 "text": text,
321 "fields": fields,
312 "fields": fields,
322 #"image_url": "http://my-website.com/path/to/image.jpg",
313 #"image_url": "http://my-website.com/path/to/image.jpg",
323 #"thumb_url": "http://example.com/path/to/thumb.png",
314 #"thumb_url": "http://example.com/path/to/thumb.png",
324 "footer": "RhodeCode",
315 "footer": "RhodeCode",
325 #"footer_icon": "",
316 #"footer_icon": "",
326 "ts": time.time(),
317 "ts": time.time(),
327 "mrkdwn_in": ["pretext", "text"]
318 "mrkdwn_in": ["pretext", "text"]
328 }
319 }
329 message_data.update(overrides)
320 message_data.update(overrides)
330 json_message = {
321 json_message = {
331 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
322 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
332 "channel": settings.get('channel', ''),
323 "channel": settings.get('channel', ''),
333 "username": settings.get('username', 'Rhodecode'),
324 "username": settings.get('username', 'Rhodecode'),
334 "attachments": [message_data]
325 "attachments": [message_data]
335 }
326 }
336
327
337 resp = requests.post(settings['service'], json=json_message)
328 resp = requests.post(settings['service'], json=json_message)
338 resp.raise_for_status() # raise exception on a failed request
329 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now