##// END OF EJS Templates
integrations: added safe renderers with detailed traceback information.
dan -
r2646:f61fee89 default
parent child Browse files
Show More
@@ -1,299 +1,311 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import colander
22 22 import string
23 23 import collections
24 24 import logging
25
26 from mako import exceptions
27
25 28 from rhodecode.translation import _
26 29
30
27 31 log = logging.getLogger(__name__)
28 32
29 33
30 34 class IntegrationTypeBase(object):
31 35 """ Base class for IntegrationType plugins """
32 36 is_dummy = False
33 37 description = ''
34 38
35 39 @classmethod
36 40 def icon(cls):
37 41 return '''
38 42 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
39 43 <svg
40 44 xmlns:dc="http://purl.org/dc/elements/1.1/"
41 45 xmlns:cc="http://creativecommons.org/ns#"
42 46 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
43 47 xmlns:svg="http://www.w3.org/2000/svg"
44 48 xmlns="http://www.w3.org/2000/svg"
45 49 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
46 50 xmlns:inkscape="http://setwww.inkscape.org/namespaces/inkscape"
47 51 viewBox="0 -256 1792 1792"
48 52 id="svg3025"
49 53 version="1.1"
50 54 inkscape:version="0.48.3.1 r9886"
51 55 width="100%"
52 56 height="100%"
53 57 sodipodi:docname="cog_font_awesome.svg">
54 58 <metadata
55 59 id="metadata3035">
56 60 <rdf:RDF>
57 61 <cc:Work
58 62 rdf:about="">
59 63 <dc:format>image/svg+xml</dc:format>
60 64 <dc:type
61 65 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
62 66 </cc:Work>
63 67 </rdf:RDF>
64 68 </metadata>
65 69 <defs
66 70 id="defs3033" />
67 71 <sodipodi:namedview
68 72 pagecolor="#ffffff"
69 73 bordercolor="#666666"
70 74 borderopacity="1"
71 75 objecttolerance="10"
72 76 gridtolerance="10"
73 77 guidetolerance="10"
74 78 inkscape:pageopacity="0"
75 79 inkscape:pageshadow="2"
76 80 inkscape:window-width="640"
77 81 inkscape:window-height="480"
78 82 id="namedview3031"
79 83 showgrid="false"
80 84 inkscape:zoom="0.13169643"
81 85 inkscape:cx="896"
82 86 inkscape:cy="896"
83 87 inkscape:window-x="0"
84 88 inkscape:window-y="25"
85 89 inkscape:window-maximized="0"
86 90 inkscape:current-layer="svg3025" />
87 91 <g
88 92 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
89 93 id="g3027">
90 94 <path
91 95 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 96 id="path3029"
93 97 inkscape:connector-curvature="0"
94 98 style="fill:currentColor" />
95 99 </g>
96 100 </svg>
97 101 '''
98 102
99 103 def __init__(self, settings):
100 104 """
101 105 :param settings: dict of settings to be used for the integration
102 106 """
103 107 self.settings = settings
104 108
105 109 def settings_schema(self):
106 110 """
107 111 A colander schema of settings for the integration type
108 112 """
109 113 return colander.Schema()
110 114
111 115
112 116 class EEIntegration(IntegrationTypeBase):
113 117 description = 'Integration available in RhodeCode EE edition.'
114 118 is_dummy = True
115 119
116 120 def __init__(self, name, key, settings=None):
117 121 self.display_name = name
118 122 self.key = key
119 123 super(EEIntegration, self).__init__(settings)
120 124
121 125
122 126 # Helpers #
123 127 WEBHOOK_URL_VARS = [
124 128 ('event_name', 'Unique name of the event type, e.g pullrequest-update'),
125 129 ('repo_name', 'Full name of the repository'),
126 130 ('repo_type', 'VCS type of repository'),
127 131 ('repo_id', 'Unique id of repository'),
128 132 ('repo_url', 'Repository url'),
129 133 # extra repo fields
130 134 ('extra:<extra_key_name>', 'Extra repo variables, read from its settings.'),
131 135
132 136 # special attrs below that we handle, using multi-call
133 137 ('branch', 'Name of each brach submitted, if any.'),
134 138 ('commit_id', 'Id of each commit submitted, if any.'),
135 139
136 140 # pr events vars
137 141 ('pull_request_id', 'Unique ID of the pull request.'),
138 142 ('pull_request_title', 'Title of the pull request.'),
139 143 ('pull_request_url', 'Pull request url.'),
140 144 ('pull_request_shadow_url', 'Pull request shadow repo clone url.'),
141 145 ('pull_request_commits_uid', 'Calculated UID of all commits inside the PR. '
142 146 'Changes after PR update'),
143 147
144 148 # user who triggers the call
145 149 ('username', 'User who triggered the call.'),
146 150 ('user_id', 'User id who triggered the call.'),
147 151 ]
148 152
149 153 # common vars for url template used for CI plugins. Shared with webhook
150 154 CI_URL_VARS = WEBHOOK_URL_VARS
151 155
152 156
153 157 class CommitParsingDataHandler(object):
154 158
155 159 def aggregate_branch_data(self, branches, commits):
156 160 branch_data = collections.OrderedDict()
157 161 for obj in branches:
158 162 branch_data[obj['name']] = obj
159 163
160 164 branches_commits = collections.OrderedDict()
161 165 for commit in commits:
162 166 if commit.get('git_ref_change'):
163 167 # special case for GIT that allows creating tags,
164 168 # deleting branches without associated commit
165 169 continue
166 170 commit_branch = commit['branch']
167 171
168 172 if commit_branch not in branches_commits:
169 173 _branch = branch_data[commit_branch] \
170 174 if commit_branch else commit_branch
171 175 branch_commits = {'branch': _branch,
172 176 'commits': []}
173 177 branches_commits[commit_branch] = branch_commits
174 178
175 179 branch_commits = branches_commits[commit_branch]
176 180 branch_commits['commits'].append(commit)
177 181 return branches_commits
178 182
179 183
180 184 class WebhookDataHandler(CommitParsingDataHandler):
181 185 name = 'webhook'
182 186
183 187 def __init__(self, template_url, headers):
184 188 self.template_url = template_url
185 189 self.headers = headers
186 190
187 191 def get_base_parsed_template(self, data):
188 192 """
189 193 initially parses the passed in template with some common variables
190 194 available on ALL calls
191 195 """
192 196 # note: make sure to update the `WEBHOOK_URL_VARS` if this changes
193 197 common_vars = {
194 198 'repo_name': data['repo']['repo_name'],
195 199 'repo_type': data['repo']['repo_type'],
196 200 'repo_id': data['repo']['repo_id'],
197 201 'repo_url': data['repo']['url'],
198 202 'username': data['actor']['username'],
199 203 'user_id': data['actor']['user_id'],
200 204 'event_name': data['name']
201 205 }
202 206
203 207 extra_vars = {}
204 208 for extra_key, extra_val in data['repo']['extra_fields'].items():
205 209 extra_vars['extra__{}'.format(extra_key)] = extra_val
206 210 common_vars.update(extra_vars)
207 211
208 212 template_url = self.template_url.replace('${extra:', '${extra__')
209 213 return string.Template(template_url).safe_substitute(**common_vars)
210 214
211 215 def repo_push_event_handler(self, event, data):
212 216 url = self.get_base_parsed_template(data)
213 217 url_cals = []
214 218
215 219 branches_commits = self.aggregate_branch_data(
216 220 data['push']['branches'], data['push']['commits'])
217 221 if '${branch}' in url:
218 222 # call it multiple times, for each branch if used in variables
219 223 for branch, commit_ids in branches_commits.items():
220 224 branch_url = string.Template(url).safe_substitute(branch=branch)
221 225 # call further down for each commit if used
222 226 if '${commit_id}' in branch_url:
223 227 for commit_data in commit_ids['commits']:
224 228 commit_id = commit_data['raw_id']
225 229 commit_url = string.Template(branch_url).safe_substitute(
226 230 commit_id=commit_id)
227 231 # register per-commit call
228 232 log.debug(
229 233 'register %s call(%s) to url %s',
230 234 self.name, event, commit_url)
231 235 url_cals.append(
232 236 (commit_url, self.headers, data))
233 237
234 238 else:
235 239 # register per-branch call
236 240 log.debug(
237 241 'register %s call(%s) to url %s',
238 242 self.name, event, branch_url)
239 243 url_cals.append(
240 244 (branch_url, self.headers, data))
241 245
242 246 else:
243 247 log.debug(
244 248 'register %s call(%s) to url %s', self.name, event, url)
245 249 url_cals.append((url, self.headers, data))
246 250
247 251 return url_cals
248 252
249 253 def repo_create_event_handler(self, event, data):
250 254 url = self.get_base_parsed_template(data)
251 255 log.debug(
252 256 'register %s call(%s) to url %s', self.name, event, url)
253 257 return [(url, self.headers, data)]
254 258
255 259 def pull_request_event_handler(self, event, data):
256 260 url = self.get_base_parsed_template(data)
257 261 log.debug(
258 262 'register %s call(%s) to url %s', self.name, event, url)
259 263 url = string.Template(url).safe_substitute(
260 264 pull_request_id=data['pullrequest']['pull_request_id'],
261 265 pull_request_title=data['pullrequest']['title'],
262 266 pull_request_url=data['pullrequest']['url'],
263 267 pull_request_shadow_url=data['pullrequest']['shadow_url'],
264 268 pull_request_commits_uid=data['pullrequest']['commits_uid'],
265 269 )
266 270 return [(url, self.headers, data)]
267 271
268 272 def __call__(self, event, data):
269 273 from rhodecode import events
270 274
271 275 if isinstance(event, events.RepoPushEvent):
272 276 return self.repo_push_event_handler(event, data)
273 277 elif isinstance(event, events.RepoCreateEvent):
274 278 return self.repo_create_event_handler(event, data)
275 279 elif isinstance(event, events.PullRequestEvent):
276 280 return self.pull_request_event_handler(event, data)
277 281 else:
278 282 raise ValueError(
279 283 'event type `%s` not in supported list: %s' % (
280 284 event.__class__, events))
281 285
282 286
283 287 def get_auth(settings):
284 288 from requests.auth import HTTPBasicAuth
285 289 username = settings.get('username')
286 290 password = settings.get('password')
287 291 if username and password:
288 292 return HTTPBasicAuth(username, password)
289 293 return None
290 294
291 295
292 296 def get_web_token(settings):
293 297 return settings['secret_token']
294 298
295 299
296 300 def get_url_vars(url_vars):
297 301 return '\n'.join(
298 302 '{} - {}'.format('${' + key + '}', explanation)
299 303 for key, explanation in url_vars)
304
305
306 def render_with_traceback(template, *args, **kwargs):
307 try:
308 return template.render(*args, **kwargs)
309 except Exception:
310 log.error(exceptions.text_error_template().render())
311 raise
@@ -1,297 +1,300 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from __future__ import unicode_literals
22 22 import deform
23 23 import logging
24 24 import colander
25 25
26 26 from mako.template import Template
27 27
28 28 from rhodecode import events
29 29 from rhodecode.translation import _
30 30 from rhodecode.lib.celerylib import run_task
31 31 from rhodecode.lib.celerylib import tasks
32 from rhodecode.integrations.types.base import IntegrationTypeBase
32 from rhodecode.integrations.types.base import (
33 IntegrationTypeBase, render_with_traceback)
33 34
34 35
35 36 log = logging.getLogger(__name__)
36 37
37 38 repo_push_template_plaintext = Template('''
38 39 Commits:
39 40
40 41 % for commit in data['push']['commits']:
41 42 ${commit['url']} by ${commit['author']} at ${commit['date']}
42 43 ${commit['message']}
43 44 ----
44 45
45 46 % endfor
46 47 ''')
47 48
48 49 ## TODO (marcink): think about putting this into a file, or use base.mako email template
49 50
50 51 repo_push_template_html = Template('''
51 52 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
52 53 <html xmlns="http://www.w3.org/1999/xhtml">
53 54 <head>
54 55 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
55 56 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
56 57 <title>${subject}</title>
57 58 <style type="text/css">
58 59 /* Based on The MailChimp Reset INLINE: Yes. */
59 60 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
60 61 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
61 62 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
62 63 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
63 64 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
64 65 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
65 66 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
66 67 /* End reset */
67 68
68 69 /* defaults for images*/
69 70 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
70 71 a img {border:none;}
71 72 .image_fix {display:block;}
72 73
73 74 body {line-height:1.2em;}
74 75 p {margin: 0 0 20px;}
75 76 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
76 77 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
77 78 a:focus {outline:none;}
78 79 a:hover {color: #305b91;}
79 80 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
80 81 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
81 82 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
82 83 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
83 84 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
84 85 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
85 86 input:focus {outline: 1px solid #979797}
86 87 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
87 88 /* Put your iPhone 4g styles in here */
88 89 }
89 90
90 91 /* Android targeting */
91 92 @media only screen and (-webkit-device-pixel-ratio:.75){
92 93 /* Put CSS for low density (ldpi) Android layouts in here */
93 94 }
94 95 @media only screen and (-webkit-device-pixel-ratio:1){
95 96 /* Put CSS for medium density (mdpi) Android layouts in here */
96 97 }
97 98 @media only screen and (-webkit-device-pixel-ratio:1.5){
98 99 /* Put CSS for high density (hdpi) Android layouts in here */
99 100 }
100 101 /* end Android targeting */
101 102
102 103 </style>
103 104
104 105 <!-- Targeting Windows Mobile -->
105 106 <!--[if IEMobile 7]>
106 107 <style type="text/css">
107 108
108 109 </style>
109 110 <![endif]-->
110 111
111 112 <!--[if gte mso 9]>
112 113 <style>
113 114 /* Target Outlook 2007 and 2010 */
114 115 </style>
115 116 <![endif]-->
116 117 </head>
117 118 <body>
118 119 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
119 120 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
120 121 <tr>
121 122 <td valign="top" style="padding:0;">
122 123 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
123 124 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
124 125 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
125 126 ${'RhodeCode'}
126 127 </a>
127 128 </td></tr>
128 129 <tr>
129 130 <td style="padding:15px;" valign="top">
130 131 % if data['push']['commits']:
131 132 % for commit in data['push']['commits']:
132 133 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
133 134 ${commit['message_html']} <br/>
134 135 <br/>
135 136 % endfor
136 137 % else:
137 138 No commit data
138 139 % endif
139 140 </td>
140 141 </tr>
141 142 </table>
142 143 </td>
143 144 </tr>
144 145 </table>
145 146 <!-- End of wrapper table -->
146 147 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
147 148 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
148 149 </a></p>
149 150 </body>
150 151 </html>
151 152 ''')
152 153
153 154
154 155 class EmailSettingsSchema(colander.Schema):
155 156 @colander.instantiate(validator=colander.Length(min=1))
156 157 class recipients(colander.SequenceSchema):
157 158 title = _('Recipients')
158 159 description = _('Email addresses to send push events to')
159 160 widget = deform.widget.SequenceWidget(min_len=1)
160 161
161 162 recipient = colander.SchemaNode(
162 163 colander.String(),
163 164 title=_('Email address'),
164 165 description=_('Email address'),
165 166 default='',
166 167 validator=colander.Email(),
167 168 widget=deform.widget.TextInputWidget(
168 169 placeholder='user@domain.com',
169 170 ),
170 171 )
171 172
172 173
173 174 class EmailIntegrationType(IntegrationTypeBase):
174 175 key = 'email'
175 176 display_name = _('Email')
176 177 description = _('Send repo push summaries to a list of recipients via email')
177 178
178 179 @classmethod
179 180 def icon(cls):
180 181 return '''
181 182 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
182 183 <svg
183 184 xmlns:dc="http://purl.org/dc/elements/1.1/"
184 185 xmlns:cc="http://creativecommons.org/ns#"
185 186 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
186 187 xmlns:svg="http://www.w3.org/2000/svg"
187 188 xmlns="http://www.w3.org/2000/svg"
188 189 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
189 190 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
190 191 viewBox="0 -256 1850 1850"
191 192 id="svg2989"
192 193 version="1.1"
193 194 inkscape:version="0.48.3.1 r9886"
194 195 width="100%"
195 196 height="100%"
196 197 sodipodi:docname="envelope_font_awesome.svg">
197 198 <metadata
198 199 id="metadata2999">
199 200 <rdf:RDF>
200 201 <cc:Work
201 202 rdf:about="">
202 203 <dc:format>image/svg+xml</dc:format>
203 204 <dc:type
204 205 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
205 206 </cc:Work>
206 207 </rdf:RDF>
207 208 </metadata>
208 209 <defs
209 210 id="defs2997" />
210 211 <sodipodi:namedview
211 212 pagecolor="#ffffff"
212 213 bordercolor="#666666"
213 214 borderopacity="1"
214 215 objecttolerance="10"
215 216 gridtolerance="10"
216 217 guidetolerance="10"
217 218 inkscape:pageopacity="0"
218 219 inkscape:pageshadow="2"
219 220 inkscape:window-width="640"
220 221 inkscape:window-height="480"
221 222 id="namedview2995"
222 223 showgrid="false"
223 224 inkscape:zoom="0.13169643"
224 225 inkscape:cx="896"
225 226 inkscape:cy="896"
226 227 inkscape:window-x="0"
227 228 inkscape:window-y="25"
228 229 inkscape:window-maximized="0"
229 230 inkscape:current-layer="svg2989" />
230 231 <g
231 232 transform="matrix(1,0,0,-1,37.966102,1282.678)"
232 233 id="g2991">
233 234 <path
234 235 d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
235 236 id="path2993"
236 237 inkscape:connector-curvature="0"
237 238 style="fill:currentColor" />
238 239 </g>
239 240 </svg>
240 241 '''
241 242
242 243 def settings_schema(self):
243 244 schema = EmailSettingsSchema()
244 245 return schema
245 246
246 247 def send_event(self, event):
247 248 data = event.as_dict()
248 249 log.debug('got event: %r', event)
249 250
250 251 if isinstance(event, events.RepoPushEvent):
251 252 repo_push_handler(data, self.settings)
252 253 else:
253 254 log.debug('ignoring event: %r', event)
254 255
255 256
256 257 def repo_push_handler(data, settings):
257 258 commit_num = len(data['push']['commits'])
258 259 server_url = data['server_url']
259 260
260 261 if commit_num == 1:
261 262 if data['push']['branches']:
262 263 _subject = '[{repo_name}] {author} pushed {commit_num} commit on branches: {branches}'
263 264 else:
264 265 _subject = '[{repo_name}] {author} pushed {commit_num} commit'
265 266 subject = _subject.format(
266 267 author=data['actor']['username'],
267 268 repo_name=data['repo']['repo_name'],
268 269 commit_num=commit_num,
269 270 branches=', '.join(
270 271 branch['name'] for branch in data['push']['branches'])
271 272 )
272 273 else:
273 274 if data['push']['branches']:
274 275 _subject = '[{repo_name}] {author} pushed {commit_num} commits on branches: {branches}'
275 276 else:
276 277 _subject = '[{repo_name}] {author} pushed {commit_num} commits'
277 278 subject = _subject.format(
278 279 author=data['actor']['username'],
279 280 repo_name=data['repo']['repo_name'],
280 281 commit_num=commit_num,
281 282 branches=', '.join(
282 283 branch['name'] for branch in data['push']['branches']))
283 284
284 email_body_plaintext = repo_push_template_plaintext.render(
285 email_body_plaintext = render_with_traceback(
286 repo_push_template_plaintext,
285 287 data=data,
286 288 subject=subject,
287 289 instance_url=server_url)
288 290
289 email_body_html = repo_push_template_html.render(
291 email_body_html = render_with_traceback(
292 repo_push_template_html,
290 293 data=data,
291 294 subject=subject,
292 295 instance_url=server_url)
293 296
294 297 for email_address in settings['recipients']:
295 298 run_task(
296 299 tasks.send_email, email_address, subject,
297 300 email_body_plaintext, email_body_html)
@@ -1,246 +1,247 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from __future__ import unicode_literals
22 22 import deform
23 23 import logging
24 24 import requests
25 25 import colander
26 26 import textwrap
27 27 from mako.template import Template
28 28 from rhodecode import events
29 29 from rhodecode.translation import _
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
32 32 from rhodecode.lib.colander_utils import strip_whitespace
33 33 from rhodecode.integrations.types.base import (
34 IntegrationTypeBase, CommitParsingDataHandler)
34 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback)
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class HipchatSettingsSchema(colander.Schema):
40 40 color_choices = [
41 41 ('yellow', _('Yellow')),
42 42 ('red', _('Red')),
43 43 ('green', _('Green')),
44 44 ('purple', _('Purple')),
45 45 ('gray', _('Gray')),
46 46 ]
47 47
48 48 server_url = colander.SchemaNode(
49 49 colander.String(),
50 50 title=_('Hipchat server URL'),
51 51 description=_('Hipchat integration url.'),
52 52 default='',
53 53 preparer=strip_whitespace,
54 54 validator=colander.url,
55 55 widget=deform.widget.TextInputWidget(
56 56 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
57 57 ),
58 58 )
59 59 notify = colander.SchemaNode(
60 60 colander.Bool(),
61 61 title=_('Notify'),
62 62 description=_('Make a notification to the users in room.'),
63 63 missing=False,
64 64 default=False,
65 65 )
66 66 color = colander.SchemaNode(
67 67 colander.String(),
68 68 title=_('Color'),
69 69 description=_('Background color of message.'),
70 70 missing='',
71 71 validator=colander.OneOf([x[0] for x in color_choices]),
72 72 widget=deform.widget.Select2Widget(
73 73 values=color_choices,
74 74 ),
75 75 )
76 76
77 77
78 78 repo_push_template = Template('''
79 79 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
80 80 <br>
81 81 <ul>
82 82 %for branch, branch_commits in branches_commits.items():
83 83 <li>
84 84 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
85 85 <ul>
86 86 %for commit in branch_commits['commits']:
87 87 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
88 88 %endfor
89 89 </ul>
90 90 </li>
91 91 %endfor
92 92 ''')
93 93
94 94
95 95 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
96 96 key = 'hipchat'
97 97 display_name = _('Hipchat')
98 98 description = _('Send events such as repo pushes and pull requests to '
99 99 'your hipchat channel.')
100 100
101 101 @classmethod
102 102 def icon(cls):
103 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 105 valid_events = [
106 106 events.PullRequestCloseEvent,
107 107 events.PullRequestMergeEvent,
108 108 events.PullRequestUpdateEvent,
109 109 events.PullRequestCommentEvent,
110 110 events.PullRequestReviewEvent,
111 111 events.PullRequestCreateEvent,
112 112 events.RepoPushEvent,
113 113 events.RepoCreateEvent,
114 114 ]
115 115
116 116 def send_event(self, event):
117 117 if event.__class__ not in self.valid_events:
118 118 log.debug('event not valid: %r' % event)
119 119 return
120 120
121 121 if event.name not in self.settings['events']:
122 122 log.debug('event ignored: %r' % event)
123 123 return
124 124
125 125 data = event.as_dict()
126 126
127 127 text = '<b>%s<b> caused a <b>%s</b> event' % (
128 128 data['actor']['username'], event.name)
129 129
130 130 log.debug('handling hipchat event for %s' % event.name)
131 131
132 132 if isinstance(event, events.PullRequestCommentEvent):
133 133 text = self.format_pull_request_comment_event(event, data)
134 134 elif isinstance(event, events.PullRequestReviewEvent):
135 135 text = self.format_pull_request_review_event(event, data)
136 136 elif isinstance(event, events.PullRequestEvent):
137 137 text = self.format_pull_request_event(event, data)
138 138 elif isinstance(event, events.RepoPushEvent):
139 139 text = self.format_repo_push_event(data)
140 140 elif isinstance(event, events.RepoCreateEvent):
141 141 text = self.format_repo_create_event(data)
142 142 else:
143 143 log.error('unhandled event type: %r' % event)
144 144
145 145 run_task(post_text_to_hipchat, self.settings, text)
146 146
147 147 def settings_schema(self):
148 148 schema = HipchatSettingsSchema()
149 149 schema.add(colander.SchemaNode(
150 150 colander.Set(),
151 151 widget=deform.widget.CheckboxChoiceWidget(
152 152 values=sorted(
153 153 [(e.name, e.display_name) for e in self.valid_events]
154 154 )
155 155 ),
156 156 description="Events activated for this integration",
157 157 name='events'
158 158 ))
159 159
160 160 return schema
161 161
162 162 def format_pull_request_comment_event(self, event, data):
163 163 comment_text = data['comment']['text']
164 164 if len(comment_text) > 200:
165 165 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
166 166 comment_text=h.html_escape(comment_text[:200]),
167 167 comment_url=data['comment']['url'],
168 168 )
169 169
170 170 comment_status = ''
171 171 if data['comment']['status']:
172 172 comment_status = '[{}]: '.format(data['comment']['status'])
173 173
174 174 return (textwrap.dedent(
175 175 '''
176 176 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
177 177 >>> {comment_status}{comment_text}
178 178 ''').format(
179 179 comment_status=comment_status,
180 180 user=data['actor']['username'],
181 181 number=data['pullrequest']['pull_request_id'],
182 182 pr_url=data['pullrequest']['url'],
183 183 pr_status=data['pullrequest']['status'],
184 184 pr_title=h.html_escape(data['pullrequest']['title']),
185 185 comment_text=h.html_escape(comment_text)
186 186 )
187 187 )
188 188
189 189 def format_pull_request_review_event(self, event, data):
190 190 return (textwrap.dedent(
191 191 '''
192 192 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
193 193 ''').format(
194 194 user=data['actor']['username'],
195 195 number=data['pullrequest']['pull_request_id'],
196 196 pr_url=data['pullrequest']['url'],
197 197 pr_status=data['pullrequest']['status'],
198 198 pr_title=h.html_escape(data['pullrequest']['title']),
199 199 )
200 200 )
201 201
202 202 def format_pull_request_event(self, event, data):
203 203 action = {
204 204 events.PullRequestCloseEvent: 'closed',
205 205 events.PullRequestMergeEvent: 'merged',
206 206 events.PullRequestUpdateEvent: 'updated',
207 207 events.PullRequestCreateEvent: 'created',
208 208 }.get(event.__class__, str(event.__class__))
209 209
210 210 return ('Pull request <a href="{url}">#{number}</a> - {title} '
211 211 '{action} by <b>{user}</b>').format(
212 212 user=data['actor']['username'],
213 213 number=data['pullrequest']['pull_request_id'],
214 214 url=data['pullrequest']['url'],
215 215 title=h.html_escape(data['pullrequest']['title']),
216 216 action=action
217 217 )
218 218
219 219 def format_repo_push_event(self, data):
220 220 branches_commits = self.aggregate_branch_data(
221 221 data['push']['branches'], data['push']['commits'])
222 222
223 result = repo_push_template.render(
223 result = render_with_traceback(
224 repo_push_template,
224 225 data=data,
225 226 branches_commits=branches_commits,
226 227 )
227 228 return result
228 229
229 230 def format_repo_create_event(self, data):
230 231 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
231 232 data['repo']['url'],
232 233 h.html_escape(data['repo']['repo_name']),
233 234 data['repo']['repo_type'],
234 235 data['actor']['username'],
235 236 )
236 237
237 238
238 239 @async_task(ignore_result=True, base=RequestContextTask)
239 240 def post_text_to_hipchat(settings, text):
240 241 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
241 242 resp = requests.post(settings['server_url'], json={
242 243 "message": text,
243 244 "color": settings.get('color', 'yellow'),
244 245 "notify": settings.get('notify', False),
245 246 })
246 247 resp.raise_for_status() # raise exception on a failed request
@@ -1,329 +1,342 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 from __future__ import unicode_literals
22 22 import re
23 23 import time
24 24 import textwrap
25 25 import logging
26 26
27 27 import deform
28 28 import requests
29 29 import colander
30 30 from mako.template import Template
31 31
32 32 from rhodecode import events
33 33 from rhodecode.translation import _
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 36 from rhodecode.lib.colander_utils import strip_whitespace
37 37 from rhodecode.integrations.types.base import (
38 IntegrationTypeBase, CommitParsingDataHandler)
38 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback)
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class SlackSettingsSchema(colander.Schema):
44 44 service = colander.SchemaNode(
45 45 colander.String(),
46 46 title=_('Slack service URL'),
47 47 description=h.literal(_(
48 48 'This can be setup at the '
49 49 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
50 50 'slack app manager</a>')),
51 51 default='',
52 52 preparer=strip_whitespace,
53 53 validator=colander.url,
54 54 widget=deform.widget.TextInputWidget(
55 55 placeholder='https://hooks.slack.com/services/...',
56 56 ),
57 57 )
58 58 username = colander.SchemaNode(
59 59 colander.String(),
60 60 title=_('Username'),
61 61 description=_('Username to show notifications coming from.'),
62 62 missing='Rhodecode',
63 63 preparer=strip_whitespace,
64 64 widget=deform.widget.TextInputWidget(
65 65 placeholder='Rhodecode'
66 66 ),
67 67 )
68 68 channel = colander.SchemaNode(
69 69 colander.String(),
70 70 title=_('Channel'),
71 71 description=_('Channel to send notifications to.'),
72 72 missing='',
73 73 preparer=strip_whitespace,
74 74 widget=deform.widget.TextInputWidget(
75 75 placeholder='#general'
76 76 ),
77 77 )
78 78 icon_emoji = colander.SchemaNode(
79 79 colander.String(),
80 80 title=_('Emoji'),
81 81 description=_('Emoji to use eg. :studio_microphone:'),
82 82 missing='',
83 83 preparer=strip_whitespace,
84 84 widget=deform.widget.TextInputWidget(
85 85 placeholder=':studio_microphone:'
86 86 ),
87 87 )
88 88
89 89
90 90 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
91 91 key = 'slack'
92 92 display_name = _('Slack')
93 93 description = _('Send events such as repo pushes and pull requests to '
94 94 'your slack channel.')
95 95
96 96 @classmethod
97 97 def icon(cls):
98 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 100 valid_events = [
101 101 events.PullRequestCloseEvent,
102 102 events.PullRequestMergeEvent,
103 103 events.PullRequestUpdateEvent,
104 104 events.PullRequestCommentEvent,
105 105 events.PullRequestReviewEvent,
106 106 events.PullRequestCreateEvent,
107 107 events.RepoPushEvent,
108 108 events.RepoCreateEvent,
109 109 ]
110 110
111 111 def send_event(self, event):
112 112 if event.__class__ not in self.valid_events:
113 113 log.debug('event not valid: %r' % event)
114 114 return
115 115
116 116 if event.name not in self.settings['events']:
117 117 log.debug('event ignored: %r' % event)
118 118 return
119 119
120 120 data = event.as_dict()
121 121
122 122 # defaults
123 123 title = '*%s* caused a *%s* event' % (
124 124 data['actor']['username'], event.name)
125 125 text = '*%s* caused a *%s* event' % (
126 126 data['actor']['username'], event.name)
127 127 fields = None
128 128 overrides = None
129 129
130 130 log.debug('handling slack event for %s' % event.name)
131 131
132 132 if isinstance(event, events.PullRequestCommentEvent):
133 133 (title, text, fields, overrides) \
134 134 = self.format_pull_request_comment_event(event, data)
135 135 elif isinstance(event, events.PullRequestReviewEvent):
136 136 title, text = self.format_pull_request_review_event(event, data)
137 137 elif isinstance(event, events.PullRequestEvent):
138 138 title, text = self.format_pull_request_event(event, data)
139 139 elif isinstance(event, events.RepoPushEvent):
140 140 title, text = self.format_repo_push_event(data)
141 141 elif isinstance(event, events.RepoCreateEvent):
142 142 title, text = self.format_repo_create_event(data)
143 143 else:
144 144 log.error('unhandled event type: %r' % event)
145 145
146 146 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
147 147
148 148 def settings_schema(self):
149 149 schema = SlackSettingsSchema()
150 150 schema.add(colander.SchemaNode(
151 151 colander.Set(),
152 152 widget=deform.widget.CheckboxChoiceWidget(
153 153 values=sorted(
154 154 [(e.name, e.display_name) for e in self.valid_events]
155 155 )
156 156 ),
157 157 description="Events activated for this integration",
158 158 name='events'
159 159 ))
160 160
161 161 return schema
162 162
163 163 def format_pull_request_comment_event(self, event, data):
164 164 comment_text = data['comment']['text']
165 165 if len(comment_text) > 200:
166 166 comment_text = '<{comment_url}|{comment_text}...>'.format(
167 167 comment_text=comment_text[:200],
168 168 comment_url=data['comment']['url'],
169 169 )
170 170
171 171 fields = None
172 172 overrides = None
173 173 status_text = None
174 174
175 175 if data['comment']['status']:
176 176 status_color = {
177 177 'approved': '#0ac878',
178 178 'rejected': '#e85e4d'}.get(data['comment']['status'])
179 179
180 180 if status_color:
181 181 overrides = {"color": status_color}
182 182
183 183 status_text = data['comment']['status']
184 184
185 185 if data['comment']['file']:
186 186 fields = [
187 187 {
188 188 "title": "file",
189 189 "value": data['comment']['file']
190 190 },
191 191 {
192 192 "title": "line",
193 193 "value": data['comment']['line']
194 194 }
195 195 ]
196 196
197 title = Template(textwrap.dedent(r'''
197 template = Template(textwrap.dedent(r'''
198 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 '''))
200 title = render_with_traceback(
201 template, data=data, comment=event.comment)
200 202
201 text = Template(textwrap.dedent(r'''
203 template = Template(textwrap.dedent(r'''
202 204 *pull request title*: ${pr_title}
203 205 % if status_text:
204 206 *submitted status*: `${status_text}`
205 207 % endif
206 208 >>> ${comment_text}
207 ''')).render(comment_text=comment_text,
209 '''))
210 text = render_with_traceback(
211 template,
212 comment_text=comment_text,
208 213 pr_title=data['pullrequest']['title'],
209 214 status_text=status_text)
210 215
211 216 return title, text, fields, overrides
212 217
213 218 def format_pull_request_review_event(self, event, data):
214 title = Template(textwrap.dedent(r'''
219 template = Template(textwrap.dedent(r'''
215 220 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
216 ''')).render(data=data)
221 '''))
222 title = render_with_traceback(template, data=data)
217 223
218 text = Template(textwrap.dedent(r'''
224 template = Template(textwrap.dedent(r'''
219 225 *pull request title*: ${pr_title}
220 ''')).render(
221 pr_title=data['pullrequest']['title'],
222 )
226 '''))
227 text = render_with_traceback(
228 template,
229 pr_title=data['pullrequest']['title'])
223 230
224 231 return title, text
225 232
226 233 def format_pull_request_event(self, event, data):
227 234 action = {
228 235 events.PullRequestCloseEvent: 'closed',
229 236 events.PullRequestMergeEvent: 'merged',
230 237 events.PullRequestUpdateEvent: 'updated',
231 238 events.PullRequestCreateEvent: 'created',
232 239 }.get(event.__class__, str(event.__class__))
233 240
234 title = Template(textwrap.dedent(r'''
241 template = Template(textwrap.dedent(r'''
235 242 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
236 ''')).render(data=data, action=action)
243 '''))
244 title = render_with_traceback(template, data=data, action=action)
237 245
238 text = Template(textwrap.dedent(r'''
246 template = Template(textwrap.dedent(r'''
239 247 *pull request title*: ${pr_title}
240 248 %if data['pullrequest']['commits']:
241 249 *commits*: ${len(data['pullrequest']['commits'])}
242 250 %endif
243 ''')).render(
251 '''))
252 text = render_with_traceback(
253 template,
244 254 pr_title=data['pullrequest']['title'],
245 data=data
246 )
255 data=data)
247 256
248 257 return title, text
249 258
250 259 def format_repo_push_event(self, data):
251 260
252 261 branches_commits = self.aggregate_branch_data(
253 262 data['push']['branches'], data['push']['commits'])
254 263
255 title = Template(r'''
264 template = Template(r'''
256 265 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
257 ''').render(data=data)
266 ''')
267 title = render_with_traceback(template, data=data)
258 268
259 269 repo_push_template = Template(textwrap.dedent(r'''
260 270 %for branch, branch_commits in branches_commits.items():
261 271 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
262 272 %for commit in branch_commits['commits']:
263 273 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
264 274 %endfor
265 275 %endfor
266 276 '''))
267 277
268 text = repo_push_template.render(
278 text = render_with_traceback(
279 repo_push_template,
269 280 data=data,
270 281 branches_commits=branches_commits,
271 282 html_to_slack_links=html_to_slack_links,
272 283 )
273 284
274 285 return title, text
275 286
276 287 def format_repo_create_event(self, data):
277 title = Template(r'''
288 template = Template(r'''
278 289 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
279 ''').render(data=data)
290 ''')
291 title = render_with_traceback(template, data=data)
280 292
281 text = Template(textwrap.dedent(r'''
293 template = Template(textwrap.dedent(r'''
282 294 repo_url: ${data['repo']['url']}
283 295 repo_type: ${data['repo']['repo_type']}
284 ''')).render(data=data)
296 '''))
297 text = render_with_traceback(template, data=data)
285 298
286 299 return title, text
287 300
288 301
289 302 def html_to_slack_links(message):
290 303 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
291 304 r'<\1|\2>', message)
292 305
293 306
294 307 @async_task(ignore_result=True, base=RequestContextTask)
295 308 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
296 309 log.debug('sending %s (%s) to slack %s' % (
297 310 title, text, settings['service']))
298 311
299 312 fields = fields or []
300 313 overrides = overrides or {}
301 314
302 315 message_data = {
303 316 "fallback": text,
304 317 "color": "#427cc9",
305 318 "pretext": title,
306 319 #"author_name": "Bobby Tables",
307 320 #"author_link": "http://flickr.com/bobby/",
308 321 #"author_icon": "http://flickr.com/icons/bobby.jpg",
309 322 #"title": "Slack API Documentation",
310 323 #"title_link": "https://api.slack.com/",
311 324 "text": text,
312 325 "fields": fields,
313 326 #"image_url": "http://my-website.com/path/to/image.jpg",
314 327 #"thumb_url": "http://example.com/path/to/thumb.png",
315 328 "footer": "RhodeCode",
316 329 #"footer_icon": "",
317 330 "ts": time.time(),
318 331 "mrkdwn_in": ["pretext", "text"]
319 332 }
320 333 message_data.update(overrides)
321 334 json_message = {
322 335 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
323 336 "channel": settings.get('channel', ''),
324 337 "username": settings.get('username', 'Rhodecode'),
325 338 "attachments": [message_data]
326 339 }
327 340
328 341 resp = requests.post(settings['service'], json=json_message)
329 342 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now