##// END OF EJS Templates
slack: expose the FULL message instead of title....
marcink -
r2336:f43c9783 default
parent child Browse files
Show More
@@ -1,334 +1,334 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 celery.task import task
31 31 from mako.template import Template
32 32
33 33 from rhodecode import events
34 34 from rhodecode.translation import _
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.celerylib import run_task
37 37 from rhodecode.lib.colander_utils import strip_whitespace
38 38 from rhodecode.integrations.types.base import IntegrationTypeBase
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):
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 icon = '''<?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>'''
96 96 valid_events = [
97 97 events.PullRequestCloseEvent,
98 98 events.PullRequestMergeEvent,
99 99 events.PullRequestUpdateEvent,
100 100 events.PullRequestCommentEvent,
101 101 events.PullRequestReviewEvent,
102 102 events.PullRequestCreateEvent,
103 103 events.RepoPushEvent,
104 104 events.RepoCreateEvent,
105 105 ]
106 106
107 107 def send_event(self, event):
108 108 if event.__class__ not in self.valid_events:
109 109 log.debug('event not valid: %r' % event)
110 110 return
111 111
112 112 if event.name not in self.settings['events']:
113 113 log.debug('event ignored: %r' % event)
114 114 return
115 115
116 116 data = event.as_dict()
117 117
118 118 # defaults
119 119 title = '*%s* caused a *%s* event' % (
120 120 data['actor']['username'], event.name)
121 121 text = '*%s* caused a *%s* event' % (
122 122 data['actor']['username'], event.name)
123 123 fields = None
124 124 overrides = None
125 125
126 126 log.debug('handling slack event for %s' % event.name)
127 127
128 128 if isinstance(event, events.PullRequestCommentEvent):
129 129 (title, text, fields, overrides) \
130 130 = self.format_pull_request_comment_event(event, data)
131 131 elif isinstance(event, events.PullRequestReviewEvent):
132 132 title, text = self.format_pull_request_review_event(event, data)
133 133 elif isinstance(event, events.PullRequestEvent):
134 134 title, text = self.format_pull_request_event(event, data)
135 135 elif isinstance(event, events.RepoPushEvent):
136 136 title, text = self.format_repo_push_event(data)
137 137 elif isinstance(event, events.RepoCreateEvent):
138 138 title, text = self.format_repo_create_event(data)
139 139 else:
140 140 log.error('unhandled event type: %r' % event)
141 141
142 142 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
143 143
144 144 def settings_schema(self):
145 145 schema = SlackSettingsSchema()
146 146 schema.add(colander.SchemaNode(
147 147 colander.Set(),
148 148 widget=deform.widget.CheckboxChoiceWidget(
149 149 values=sorted(
150 150 [(e.name, e.display_name) for e in self.valid_events]
151 151 )
152 152 ),
153 153 description="Events activated for this integration",
154 154 name='events'
155 155 ))
156 156
157 157 return schema
158 158
159 159 def format_pull_request_comment_event(self, event, data):
160 160 comment_text = data['comment']['text']
161 161 if len(comment_text) > 200:
162 162 comment_text = '<{comment_url}|{comment_text}...>'.format(
163 163 comment_text=comment_text[:200],
164 164 comment_url=data['comment']['url'],
165 165 )
166 166
167 167 fields = None
168 168 overrides = None
169 169 status_text = None
170 170
171 171 if data['comment']['status']:
172 172 status_color = {
173 173 'approved': '#0ac878',
174 174 'rejected': '#e85e4d'}.get(data['comment']['status'])
175 175
176 176 if status_color:
177 177 overrides = {"color": status_color}
178 178
179 179 status_text = data['comment']['status']
180 180
181 181 if data['comment']['file']:
182 182 fields = [
183 183 {
184 184 "title": "file",
185 185 "value": data['comment']['file']
186 186 },
187 187 {
188 188 "title": "line",
189 189 "value": data['comment']['line']
190 190 }
191 191 ]
192 192
193 193 title = Template(textwrap.dedent(r'''
194 194 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
195 195 ''')).render(data=data, comment=event.comment)
196 196
197 197 text = Template(textwrap.dedent(r'''
198 198 *pull request title*: ${pr_title}
199 199 % if status_text:
200 200 *submitted status*: `${status_text}`
201 201 % endif
202 202 >>> ${comment_text}
203 203 ''')).render(comment_text=comment_text,
204 204 pr_title=data['pullrequest']['title'],
205 205 status_text=status_text)
206 206
207 207 return title, text, fields, overrides
208 208
209 209 def format_pull_request_review_event(self, event, data):
210 210 title = Template(textwrap.dedent(r'''
211 211 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
212 212 ''')).render(data=data)
213 213
214 214 text = Template(textwrap.dedent(r'''
215 215 *pull request title*: ${pr_title}
216 216 ''')).render(
217 217 pr_title=data['pullrequest']['title'],
218 218 )
219 219
220 220 return title, text
221 221
222 222 def format_pull_request_event(self, event, data):
223 223 action = {
224 224 events.PullRequestCloseEvent: 'closed',
225 225 events.PullRequestMergeEvent: 'merged',
226 226 events.PullRequestUpdateEvent: 'updated',
227 227 events.PullRequestCreateEvent: 'created',
228 228 }.get(event.__class__, str(event.__class__))
229 229
230 230 title = Template(textwrap.dedent(r'''
231 231 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
232 232 ''')).render(data=data, action=action)
233 233
234 234 text = Template(textwrap.dedent(r'''
235 235 *pull request title*: ${pr_title}
236 236 %if data['pullrequest']['commits']:
237 237 *commits*: ${len(data['pullrequest']['commits'])}
238 238 %endif
239 239 ''')).render(
240 240 pr_title=data['pullrequest']['title'],
241 241 data=data
242 242 )
243 243
244 244 return title, text
245 245
246 246 def format_repo_push_event(self, data):
247 247 branch_data = {branch['name']: branch
248 248 for branch in data['push']['branches']}
249 249
250 250 branches_commits = {}
251 251 for commit in data['push']['commits']:
252 252 if commit['branch'] not in branches_commits:
253 253 branch_commits = {'branch': branch_data[commit['branch']],
254 254 'commits': []}
255 255 branches_commits[commit['branch']] = branch_commits
256 256
257 257 branch_commits = branches_commits[commit['branch']]
258 258 branch_commits['commits'].append(commit)
259 259
260 260 title = Template(r'''
261 261 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
262 262 ''').render(data=data)
263 263
264 264 repo_push_template = Template(textwrap.dedent(r'''
265 265 %for branch, branch_commits in branches_commits.items():
266 266 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
267 267 %for commit in branch_commits['commits']:
268 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html_title']|html_to_slack_links}
268 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
269 269 %endfor
270 270 %endfor
271 271 '''))
272 272
273 273 text = repo_push_template.render(
274 274 data=data,
275 275 branches_commits=branches_commits,
276 276 html_to_slack_links=html_to_slack_links,
277 277 )
278 278
279 279 return title, text
280 280
281 281 def format_repo_create_event(self, data):
282 282 title = Template(r'''
283 283 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
284 284 ''').render(data=data)
285 285
286 286 text = Template(textwrap.dedent(r'''
287 287 repo_url: ${data['repo']['url']}
288 288 repo_type: ${data['repo']['repo_type']}
289 289 ''')).render(data=data)
290 290
291 291 return title, text
292 292
293 293
294 294 def html_to_slack_links(message):
295 295 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
296 296 r'<\1|\2>', message)
297 297
298 298
299 299 @task(ignore_result=True)
300 300 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
301 301 log.debug('sending %s (%s) to slack %s' % (
302 302 title, text, settings['service']))
303 303
304 304 fields = fields or []
305 305 overrides = overrides or {}
306 306
307 307 message_data = {
308 308 "fallback": text,
309 309 "color": "#427cc9",
310 310 "pretext": title,
311 311 #"author_name": "Bobby Tables",
312 312 #"author_link": "http://flickr.com/bobby/",
313 313 #"author_icon": "http://flickr.com/icons/bobby.jpg",
314 314 #"title": "Slack API Documentation",
315 315 #"title_link": "https://api.slack.com/",
316 316 "text": text,
317 317 "fields": fields,
318 318 #"image_url": "http://my-website.com/path/to/image.jpg",
319 319 #"thumb_url": "http://example.com/path/to/thumb.png",
320 320 "footer": "RhodeCode",
321 321 #"footer_icon": "",
322 322 "ts": time.time(),
323 323 "mrkdwn_in": ["pretext", "text"]
324 324 }
325 325 message_data.update(overrides)
326 326 json_message = {
327 327 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
328 328 "channel": settings.get('channel', ''),
329 329 "username": settings.get('username', 'Rhodecode'),
330 330 "attachments": [message_data]
331 331 }
332 332
333 333 resp = requests.post(settings['service'], json=json_message)
334 334 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now