##// END OF EJS Templates
slack: added a case for SVN commits to trunk
marcink -
r2647:b00c6b3e default
parent child Browse files
Show More
@@ -1,342 +1,350 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 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 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 199 '''))
200 200 title = render_with_traceback(
201 201 template, data=data, comment=event.comment)
202 202
203 203 template = Template(textwrap.dedent(r'''
204 204 *pull request title*: ${pr_title}
205 205 % if status_text:
206 206 *submitted status*: `${status_text}`
207 207 % endif
208 208 >>> ${comment_text}
209 209 '''))
210 210 text = render_with_traceback(
211 211 template,
212 212 comment_text=comment_text,
213 213 pr_title=data['pullrequest']['title'],
214 214 status_text=status_text)
215 215
216 216 return title, text, fields, overrides
217 217
218 218 def format_pull_request_review_event(self, event, data):
219 219 template = Template(textwrap.dedent(r'''
220 220 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
221 221 '''))
222 222 title = render_with_traceback(template, data=data)
223 223
224 224 template = Template(textwrap.dedent(r'''
225 225 *pull request title*: ${pr_title}
226 226 '''))
227 227 text = render_with_traceback(
228 228 template,
229 229 pr_title=data['pullrequest']['title'])
230 230
231 231 return title, text
232 232
233 233 def format_pull_request_event(self, event, data):
234 234 action = {
235 235 events.PullRequestCloseEvent: 'closed',
236 236 events.PullRequestMergeEvent: 'merged',
237 237 events.PullRequestUpdateEvent: 'updated',
238 238 events.PullRequestCreateEvent: 'created',
239 239 }.get(event.__class__, str(event.__class__))
240 240
241 241 template = Template(textwrap.dedent(r'''
242 242 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
243 243 '''))
244 244 title = render_with_traceback(template, data=data, action=action)
245 245
246 246 template = Template(textwrap.dedent(r'''
247 247 *pull request title*: ${pr_title}
248 248 %if data['pullrequest']['commits']:
249 249 *commits*: ${len(data['pullrequest']['commits'])}
250 250 %endif
251 251 '''))
252 252 text = render_with_traceback(
253 253 template,
254 254 pr_title=data['pullrequest']['title'],
255 255 data=data)
256 256
257 257 return title, text
258 258
259 259 def format_repo_push_event(self, data):
260 260
261 261 branches_commits = self.aggregate_branch_data(
262 262 data['push']['branches'], data['push']['commits'])
263 263
264 264 template = Template(r'''
265 265 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
266 266 ''')
267 267 title = render_with_traceback(template, data=data)
268 268
269 269 repo_push_template = Template(textwrap.dedent(r'''
270 %for branch, branch_commits in branches_commits.items():
271 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
272 %for commit in branch_commits['commits']:
270 <%
271 def branch_text(branch):
272 if branch:
273 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
274 else:
275 ## case for SVN no branch push...
276 return 'to trunk'
277 %> \
278 % for branch, branch_commits in branches_commits.items():
279 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
280 % for commit in branch_commits['commits']:
273 281 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
274 %endfor
275 %endfor
282 % endfor
283 % endfor
276 284 '''))
277 285
278 286 text = render_with_traceback(
279 287 repo_push_template,
280 288 data=data,
281 289 branches_commits=branches_commits,
282 290 html_to_slack_links=html_to_slack_links,
283 291 )
284 292
285 293 return title, text
286 294
287 295 def format_repo_create_event(self, data):
288 296 template = Template(r'''
289 297 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
290 298 ''')
291 299 title = render_with_traceback(template, data=data)
292 300
293 301 template = Template(textwrap.dedent(r'''
294 302 repo_url: ${data['repo']['url']}
295 303 repo_type: ${data['repo']['repo_type']}
296 304 '''))
297 305 text = render_with_traceback(template, data=data)
298 306
299 307 return title, text
300 308
301 309
302 310 def html_to_slack_links(message):
303 311 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
304 312 r'<\1|\2>', message)
305 313
306 314
307 315 @async_task(ignore_result=True, base=RequestContextTask)
308 316 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
309 317 log.debug('sending %s (%s) to slack %s' % (
310 318 title, text, settings['service']))
311 319
312 320 fields = fields or []
313 321 overrides = overrides or {}
314 322
315 323 message_data = {
316 324 "fallback": text,
317 325 "color": "#427cc9",
318 326 "pretext": title,
319 327 #"author_name": "Bobby Tables",
320 328 #"author_link": "http://flickr.com/bobby/",
321 329 #"author_icon": "http://flickr.com/icons/bobby.jpg",
322 330 #"title": "Slack API Documentation",
323 331 #"title_link": "https://api.slack.com/",
324 332 "text": text,
325 333 "fields": fields,
326 334 #"image_url": "http://my-website.com/path/to/image.jpg",
327 335 #"thumb_url": "http://example.com/path/to/thumb.png",
328 336 "footer": "RhodeCode",
329 337 #"footer_icon": "",
330 338 "ts": time.time(),
331 339 "mrkdwn_in": ["pretext", "text"]
332 340 }
333 341 message_data.update(overrides)
334 342 json_message = {
335 343 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
336 344 "channel": settings.get('channel', ''),
337 345 "username": settings.get('username', 'Rhodecode'),
338 346 "attachments": [message_data]
339 347 }
340 348
341 349 resp = requests.post(settings['service'], json=json_message)
342 350 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now