##// END OF EJS Templates
integrations: use orderedDict in all integrations for settings.
marcink -
r2420:e50c0448 default
parent child Browse files
Show More
@@ -1,252 +1,252 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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
27 from mako.template import Template
28 from mako.template import Template
28
29 from rhodecode import events
29 from rhodecode import events
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
32 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
33 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.lib.colander_utils import strip_whitespace
34 from rhodecode.integrations.types.base import IntegrationTypeBase
34 from rhodecode.integrations.types.base import IntegrationTypeBase
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):
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 icon = '''<?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>'''
100 icon = '''<?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>'''
101 valid_events = [
101 valid_events = [
102 events.PullRequestCloseEvent,
102 events.PullRequestCloseEvent,
103 events.PullRequestMergeEvent,
103 events.PullRequestMergeEvent,
104 events.PullRequestUpdateEvent,
104 events.PullRequestUpdateEvent,
105 events.PullRequestCommentEvent,
105 events.PullRequestCommentEvent,
106 events.PullRequestReviewEvent,
106 events.PullRequestReviewEvent,
107 events.PullRequestCreateEvent,
107 events.PullRequestCreateEvent,
108 events.RepoPushEvent,
108 events.RepoPushEvent,
109 events.RepoCreateEvent,
109 events.RepoCreateEvent,
110 ]
110 ]
111
111
112 def send_event(self, event):
112 def send_event(self, event):
113 if event.__class__ not in self.valid_events:
113 if event.__class__ not in self.valid_events:
114 log.debug('event not valid: %r' % event)
114 log.debug('event not valid: %r' % event)
115 return
115 return
116
116
117 if event.name not in self.settings['events']:
117 if event.name not in self.settings['events']:
118 log.debug('event ignored: %r' % event)
118 log.debug('event ignored: %r' % event)
119 return
119 return
120
120
121 data = event.as_dict()
121 data = event.as_dict()
122
122
123 text = '<b>%s<b> caused a <b>%s</b> event' % (
123 text = '<b>%s<b> caused a <b>%s</b> event' % (
124 data['actor']['username'], event.name)
124 data['actor']['username'], event.name)
125
125
126 log.debug('handling hipchat event for %s' % event.name)
126 log.debug('handling hipchat event for %s' % event.name)
127
127
128 if isinstance(event, events.PullRequestCommentEvent):
128 if isinstance(event, events.PullRequestCommentEvent):
129 text = self.format_pull_request_comment_event(event, data)
129 text = self.format_pull_request_comment_event(event, data)
130 elif isinstance(event, events.PullRequestReviewEvent):
130 elif isinstance(event, events.PullRequestReviewEvent):
131 text = self.format_pull_request_review_event(event, data)
131 text = self.format_pull_request_review_event(event, data)
132 elif isinstance(event, events.PullRequestEvent):
132 elif isinstance(event, events.PullRequestEvent):
133 text = self.format_pull_request_event(event, data)
133 text = self.format_pull_request_event(event, data)
134 elif isinstance(event, events.RepoPushEvent):
134 elif isinstance(event, events.RepoPushEvent):
135 text = self.format_repo_push_event(data)
135 text = self.format_repo_push_event(data)
136 elif isinstance(event, events.RepoCreateEvent):
136 elif isinstance(event, events.RepoCreateEvent):
137 text = self.format_repo_create_event(data)
137 text = self.format_repo_create_event(data)
138 else:
138 else:
139 log.error('unhandled event type: %r' % event)
139 log.error('unhandled event type: %r' % event)
140
140
141 run_task(post_text_to_hipchat, self.settings, text)
141 run_task(post_text_to_hipchat, self.settings, text)
142
142
143 def settings_schema(self):
143 def settings_schema(self):
144 schema = HipchatSettingsSchema()
144 schema = HipchatSettingsSchema()
145 schema.add(colander.SchemaNode(
145 schema.add(colander.SchemaNode(
146 colander.Set(),
146 colander.Set(),
147 widget=deform.widget.CheckboxChoiceWidget(
147 widget=deform.widget.CheckboxChoiceWidget(
148 values=sorted(
148 values=sorted(
149 [(e.name, e.display_name) for e in self.valid_events]
149 [(e.name, e.display_name) for e in self.valid_events]
150 )
150 )
151 ),
151 ),
152 description="Events activated for this integration",
152 description="Events activated for this integration",
153 name='events'
153 name='events'
154 ))
154 ))
155
155
156 return schema
156 return schema
157
157
158 def format_pull_request_comment_event(self, event, data):
158 def format_pull_request_comment_event(self, event, data):
159 comment_text = data['comment']['text']
159 comment_text = data['comment']['text']
160 if len(comment_text) > 200:
160 if len(comment_text) > 200:
161 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
161 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
162 comment_text=h.html_escape(comment_text[:200]),
162 comment_text=h.html_escape(comment_text[:200]),
163 comment_url=data['comment']['url'],
163 comment_url=data['comment']['url'],
164 )
164 )
165
165
166 comment_status = ''
166 comment_status = ''
167 if data['comment']['status']:
167 if data['comment']['status']:
168 comment_status = '[{}]: '.format(data['comment']['status'])
168 comment_status = '[{}]: '.format(data['comment']['status'])
169
169
170 return (textwrap.dedent(
170 return (textwrap.dedent(
171 '''
171 '''
172 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
172 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
173 >>> {comment_status}{comment_text}
173 >>> {comment_status}{comment_text}
174 ''').format(
174 ''').format(
175 comment_status=comment_status,
175 comment_status=comment_status,
176 user=data['actor']['username'],
176 user=data['actor']['username'],
177 number=data['pullrequest']['pull_request_id'],
177 number=data['pullrequest']['pull_request_id'],
178 pr_url=data['pullrequest']['url'],
178 pr_url=data['pullrequest']['url'],
179 pr_status=data['pullrequest']['status'],
179 pr_status=data['pullrequest']['status'],
180 pr_title=h.html_escape(data['pullrequest']['title']),
180 pr_title=h.html_escape(data['pullrequest']['title']),
181 comment_text=h.html_escape(comment_text)
181 comment_text=h.html_escape(comment_text)
182 )
182 )
183 )
183 )
184
184
185 def format_pull_request_review_event(self, event, data):
185 def format_pull_request_review_event(self, event, data):
186 return (textwrap.dedent(
186 return (textwrap.dedent(
187 '''
187 '''
188 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
188 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
189 ''').format(
189 ''').format(
190 user=data['actor']['username'],
190 user=data['actor']['username'],
191 number=data['pullrequest']['pull_request_id'],
191 number=data['pullrequest']['pull_request_id'],
192 pr_url=data['pullrequest']['url'],
192 pr_url=data['pullrequest']['url'],
193 pr_status=data['pullrequest']['status'],
193 pr_status=data['pullrequest']['status'],
194 pr_title=h.html_escape(data['pullrequest']['title']),
194 pr_title=h.html_escape(data['pullrequest']['title']),
195 )
195 )
196 )
196 )
197
197
198 def format_pull_request_event(self, event, data):
198 def format_pull_request_event(self, event, data):
199 action = {
199 action = {
200 events.PullRequestCloseEvent: 'closed',
200 events.PullRequestCloseEvent: 'closed',
201 events.PullRequestMergeEvent: 'merged',
201 events.PullRequestMergeEvent: 'merged',
202 events.PullRequestUpdateEvent: 'updated',
202 events.PullRequestUpdateEvent: 'updated',
203 events.PullRequestCreateEvent: 'created',
203 events.PullRequestCreateEvent: 'created',
204 }.get(event.__class__, str(event.__class__))
204 }.get(event.__class__, str(event.__class__))
205
205
206 return ('Pull request <a href="{url}">#{number}</a> - {title} '
206 return ('Pull request <a href="{url}">#{number}</a> - {title} '
207 '{action} by <b>{user}</b>').format(
207 '{action} by <b>{user}</b>').format(
208 user=data['actor']['username'],
208 user=data['actor']['username'],
209 number=data['pullrequest']['pull_request_id'],
209 number=data['pullrequest']['pull_request_id'],
210 url=data['pullrequest']['url'],
210 url=data['pullrequest']['url'],
211 title=h.html_escape(data['pullrequest']['title']),
211 title=h.html_escape(data['pullrequest']['title']),
212 action=action
212 action=action
213 )
213 )
214
214
215 def format_repo_push_event(self, data):
215 def format_repo_push_event(self, data):
216 branch_data = {branch['name']: branch
216 branch_data = {branch['name']: branch
217 for branch in data['push']['branches']}
217 for branch in data['push']['branches']}
218
218
219 branches_commits = {}
219 branches_commits = OrderedDict()
220 for commit in data['push']['commits']:
220 for commit in data['push']['commits']:
221 if commit['branch'] not in branches_commits:
221 if commit['branch'] not in branches_commits:
222 branch_commits = {'branch': branch_data[commit['branch']],
222 branch_commits = {'branch': branch_data[commit['branch']],
223 'commits': []}
223 'commits': []}
224 branches_commits[commit['branch']] = branch_commits
224 branches_commits[commit['branch']] = branch_commits
225
225
226 branch_commits = branches_commits[commit['branch']]
226 branch_commits = branches_commits[commit['branch']]
227 branch_commits['commits'].append(commit)
227 branch_commits['commits'].append(commit)
228
228
229 result = repo_push_template.render(
229 result = repo_push_template.render(
230 data=data,
230 data=data,
231 branches_commits=branches_commits,
231 branches_commits=branches_commits,
232 )
232 )
233 return result
233 return result
234
234
235 def format_repo_create_event(self, data):
235 def format_repo_create_event(self, data):
236 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
236 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
237 data['repo']['url'],
237 data['repo']['url'],
238 h.html_escape(data['repo']['repo_name']),
238 h.html_escape(data['repo']['repo_name']),
239 data['repo']['repo_type'],
239 data['repo']['repo_type'],
240 data['actor']['username'],
240 data['actor']['username'],
241 )
241 )
242
242
243
243
244 @async_task(ignore_result=True, base=RequestContextTask)
244 @async_task(ignore_result=True, base=RequestContextTask)
245 def post_text_to_hipchat(settings, text):
245 def post_text_to_hipchat(settings, text):
246 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
246 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
247 resp = requests.post(settings['server_url'], json={
247 resp = requests.post(settings['server_url'], json={
248 "message": text,
248 "message": text,
249 "color": settings.get('color', 'yellow'),
249 "color": settings.get('color', 'yellow'),
250 "notify": settings.get('notify', False),
250 "notify": settings.get('notify', False),
251 })
251 })
252 resp.raise_for_status() # raise exception on a failed request
252 resp.raise_for_status() # raise exception on a failed request
@@ -1,333 +1,334 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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
31
32
32 from rhodecode import events
33 from rhodecode import events
33 from rhodecode.translation import _
34 from rhodecode.translation import _
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.integrations.types.base import IntegrationTypeBase
38 from rhodecode.integrations.types.base import IntegrationTypeBase
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 class SlackSettingsSchema(colander.Schema):
43 class SlackSettingsSchema(colander.Schema):
43 service = colander.SchemaNode(
44 service = colander.SchemaNode(
44 colander.String(),
45 colander.String(),
45 title=_('Slack service URL'),
46 title=_('Slack service URL'),
46 description=h.literal(_(
47 description=h.literal(_(
47 'This can be setup at the '
48 'This can be setup at the '
48 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 'slack app manager</a>')),
50 'slack app manager</a>')),
50 default='',
51 default='',
51 preparer=strip_whitespace,
52 preparer=strip_whitespace,
52 validator=colander.url,
53 validator=colander.url,
53 widget=deform.widget.TextInputWidget(
54 widget=deform.widget.TextInputWidget(
54 placeholder='https://hooks.slack.com/services/...',
55 placeholder='https://hooks.slack.com/services/...',
55 ),
56 ),
56 )
57 )
57 username = colander.SchemaNode(
58 username = colander.SchemaNode(
58 colander.String(),
59 colander.String(),
59 title=_('Username'),
60 title=_('Username'),
60 description=_('Username to show notifications coming from.'),
61 description=_('Username to show notifications coming from.'),
61 missing='Rhodecode',
62 missing='Rhodecode',
62 preparer=strip_whitespace,
63 preparer=strip_whitespace,
63 widget=deform.widget.TextInputWidget(
64 widget=deform.widget.TextInputWidget(
64 placeholder='Rhodecode'
65 placeholder='Rhodecode'
65 ),
66 ),
66 )
67 )
67 channel = colander.SchemaNode(
68 channel = colander.SchemaNode(
68 colander.String(),
69 colander.String(),
69 title=_('Channel'),
70 title=_('Channel'),
70 description=_('Channel to send notifications to.'),
71 description=_('Channel to send notifications to.'),
71 missing='',
72 missing='',
72 preparer=strip_whitespace,
73 preparer=strip_whitespace,
73 widget=deform.widget.TextInputWidget(
74 widget=deform.widget.TextInputWidget(
74 placeholder='#general'
75 placeholder='#general'
75 ),
76 ),
76 )
77 )
77 icon_emoji = colander.SchemaNode(
78 icon_emoji = colander.SchemaNode(
78 colander.String(),
79 colander.String(),
79 title=_('Emoji'),
80 title=_('Emoji'),
80 description=_('Emoji to use eg. :studio_microphone:'),
81 description=_('Emoji to use eg. :studio_microphone:'),
81 missing='',
82 missing='',
82 preparer=strip_whitespace,
83 preparer=strip_whitespace,
83 widget=deform.widget.TextInputWidget(
84 widget=deform.widget.TextInputWidget(
84 placeholder=':studio_microphone:'
85 placeholder=':studio_microphone:'
85 ),
86 ),
86 )
87 )
87
88
88
89
89 class SlackIntegrationType(IntegrationTypeBase):
90 class SlackIntegrationType(IntegrationTypeBase):
90 key = 'slack'
91 key = 'slack'
91 display_name = _('Slack')
92 display_name = _('Slack')
92 description = _('Send events such as repo pushes and pull requests to '
93 description = _('Send events such as repo pushes and pull requests to '
93 'your slack channel.')
94 'your slack channel.')
94 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>'''
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>'''
95 valid_events = [
96 valid_events = [
96 events.PullRequestCloseEvent,
97 events.PullRequestCloseEvent,
97 events.PullRequestMergeEvent,
98 events.PullRequestMergeEvent,
98 events.PullRequestUpdateEvent,
99 events.PullRequestUpdateEvent,
99 events.PullRequestCommentEvent,
100 events.PullRequestCommentEvent,
100 events.PullRequestReviewEvent,
101 events.PullRequestReviewEvent,
101 events.PullRequestCreateEvent,
102 events.PullRequestCreateEvent,
102 events.RepoPushEvent,
103 events.RepoPushEvent,
103 events.RepoCreateEvent,
104 events.RepoCreateEvent,
104 ]
105 ]
105
106
106 def send_event(self, event):
107 def send_event(self, event):
107 if event.__class__ not in self.valid_events:
108 if event.__class__ not in self.valid_events:
108 log.debug('event not valid: %r' % event)
109 log.debug('event not valid: %r' % event)
109 return
110 return
110
111
111 if event.name not in self.settings['events']:
112 if event.name not in self.settings['events']:
112 log.debug('event ignored: %r' % event)
113 log.debug('event ignored: %r' % event)
113 return
114 return
114
115
115 data = event.as_dict()
116 data = event.as_dict()
116
117
117 # defaults
118 # defaults
118 title = '*%s* caused a *%s* event' % (
119 title = '*%s* caused a *%s* event' % (
119 data['actor']['username'], event.name)
120 data['actor']['username'], event.name)
120 text = '*%s* caused a *%s* event' % (
121 text = '*%s* caused a *%s* event' % (
121 data['actor']['username'], event.name)
122 data['actor']['username'], event.name)
122 fields = None
123 fields = None
123 overrides = None
124 overrides = None
124
125
125 log.debug('handling slack event for %s' % event.name)
126 log.debug('handling slack event for %s' % event.name)
126
127
127 if isinstance(event, events.PullRequestCommentEvent):
128 if isinstance(event, events.PullRequestCommentEvent):
128 (title, text, fields, overrides) \
129 (title, text, fields, overrides) \
129 = self.format_pull_request_comment_event(event, data)
130 = self.format_pull_request_comment_event(event, data)
130 elif isinstance(event, events.PullRequestReviewEvent):
131 elif isinstance(event, events.PullRequestReviewEvent):
131 title, text = self.format_pull_request_review_event(event, data)
132 title, text = self.format_pull_request_review_event(event, data)
132 elif isinstance(event, events.PullRequestEvent):
133 elif isinstance(event, events.PullRequestEvent):
133 title, text = self.format_pull_request_event(event, data)
134 title, text = self.format_pull_request_event(event, data)
134 elif isinstance(event, events.RepoPushEvent):
135 elif isinstance(event, events.RepoPushEvent):
135 title, text = self.format_repo_push_event(data)
136 title, text = self.format_repo_push_event(data)
136 elif isinstance(event, events.RepoCreateEvent):
137 elif isinstance(event, events.RepoCreateEvent):
137 title, text = self.format_repo_create_event(data)
138 title, text = self.format_repo_create_event(data)
138 else:
139 else:
139 log.error('unhandled event type: %r' % event)
140 log.error('unhandled event type: %r' % event)
140
141
141 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
142 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
142
143
143 def settings_schema(self):
144 def settings_schema(self):
144 schema = SlackSettingsSchema()
145 schema = SlackSettingsSchema()
145 schema.add(colander.SchemaNode(
146 schema.add(colander.SchemaNode(
146 colander.Set(),
147 colander.Set(),
147 widget=deform.widget.CheckboxChoiceWidget(
148 widget=deform.widget.CheckboxChoiceWidget(
148 values=sorted(
149 values=sorted(
149 [(e.name, e.display_name) for e in self.valid_events]
150 [(e.name, e.display_name) for e in self.valid_events]
150 )
151 )
151 ),
152 ),
152 description="Events activated for this integration",
153 description="Events activated for this integration",
153 name='events'
154 name='events'
154 ))
155 ))
155
156
156 return schema
157 return schema
157
158
158 def format_pull_request_comment_event(self, event, data):
159 def format_pull_request_comment_event(self, event, data):
159 comment_text = data['comment']['text']
160 comment_text = data['comment']['text']
160 if len(comment_text) > 200:
161 if len(comment_text) > 200:
161 comment_text = '<{comment_url}|{comment_text}...>'.format(
162 comment_text = '<{comment_url}|{comment_text}...>'.format(
162 comment_text=comment_text[:200],
163 comment_text=comment_text[:200],
163 comment_url=data['comment']['url'],
164 comment_url=data['comment']['url'],
164 )
165 )
165
166
166 fields = None
167 fields = None
167 overrides = None
168 overrides = None
168 status_text = None
169 status_text = None
169
170
170 if data['comment']['status']:
171 if data['comment']['status']:
171 status_color = {
172 status_color = {
172 'approved': '#0ac878',
173 'approved': '#0ac878',
173 'rejected': '#e85e4d'}.get(data['comment']['status'])
174 'rejected': '#e85e4d'}.get(data['comment']['status'])
174
175
175 if status_color:
176 if status_color:
176 overrides = {"color": status_color}
177 overrides = {"color": status_color}
177
178
178 status_text = data['comment']['status']
179 status_text = data['comment']['status']
179
180
180 if data['comment']['file']:
181 if data['comment']['file']:
181 fields = [
182 fields = [
182 {
183 {
183 "title": "file",
184 "title": "file",
184 "value": data['comment']['file']
185 "value": data['comment']['file']
185 },
186 },
186 {
187 {
187 "title": "line",
188 "title": "line",
188 "value": data['comment']['line']
189 "value": data['comment']['line']
189 }
190 }
190 ]
191 ]
191
192
192 title = Template(textwrap.dedent(r'''
193 title = Template(textwrap.dedent(r'''
193 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
194 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
194 ''')).render(data=data, comment=event.comment)
195 ''')).render(data=data, comment=event.comment)
195
196
196 text = Template(textwrap.dedent(r'''
197 text = Template(textwrap.dedent(r'''
197 *pull request title*: ${pr_title}
198 *pull request title*: ${pr_title}
198 % if status_text:
199 % if status_text:
199 *submitted status*: `${status_text}`
200 *submitted status*: `${status_text}`
200 % endif
201 % endif
201 >>> ${comment_text}
202 >>> ${comment_text}
202 ''')).render(comment_text=comment_text,
203 ''')).render(comment_text=comment_text,
203 pr_title=data['pullrequest']['title'],
204 pr_title=data['pullrequest']['title'],
204 status_text=status_text)
205 status_text=status_text)
205
206
206 return title, text, fields, overrides
207 return title, text, fields, overrides
207
208
208 def format_pull_request_review_event(self, event, data):
209 def format_pull_request_review_event(self, event, data):
209 title = Template(textwrap.dedent(r'''
210 title = Template(textwrap.dedent(r'''
210 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
211 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
211 ''')).render(data=data)
212 ''')).render(data=data)
212
213
213 text = Template(textwrap.dedent(r'''
214 text = Template(textwrap.dedent(r'''
214 *pull request title*: ${pr_title}
215 *pull request title*: ${pr_title}
215 ''')).render(
216 ''')).render(
216 pr_title=data['pullrequest']['title'],
217 pr_title=data['pullrequest']['title'],
217 )
218 )
218
219
219 return title, text
220 return title, text
220
221
221 def format_pull_request_event(self, event, data):
222 def format_pull_request_event(self, event, data):
222 action = {
223 action = {
223 events.PullRequestCloseEvent: 'closed',
224 events.PullRequestCloseEvent: 'closed',
224 events.PullRequestMergeEvent: 'merged',
225 events.PullRequestMergeEvent: 'merged',
225 events.PullRequestUpdateEvent: 'updated',
226 events.PullRequestUpdateEvent: 'updated',
226 events.PullRequestCreateEvent: 'created',
227 events.PullRequestCreateEvent: 'created',
227 }.get(event.__class__, str(event.__class__))
228 }.get(event.__class__, str(event.__class__))
228
229
229 title = Template(textwrap.dedent(r'''
230 title = Template(textwrap.dedent(r'''
230 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
231 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
231 ''')).render(data=data, action=action)
232 ''')).render(data=data, action=action)
232
233
233 text = Template(textwrap.dedent(r'''
234 text = Template(textwrap.dedent(r'''
234 *pull request title*: ${pr_title}
235 *pull request title*: ${pr_title}
235 %if data['pullrequest']['commits']:
236 %if data['pullrequest']['commits']:
236 *commits*: ${len(data['pullrequest']['commits'])}
237 *commits*: ${len(data['pullrequest']['commits'])}
237 %endif
238 %endif
238 ''')).render(
239 ''')).render(
239 pr_title=data['pullrequest']['title'],
240 pr_title=data['pullrequest']['title'],
240 data=data
241 data=data
241 )
242 )
242
243
243 return title, text
244 return title, text
244
245
245 def format_repo_push_event(self, data):
246 def format_repo_push_event(self, data):
246 branch_data = {branch['name']: branch
247 branch_data = {branch['name']: branch
247 for branch in data['push']['branches']}
248 for branch in data['push']['branches']}
248
249
249 branches_commits = {}
250 branches_commits = OrderedDict()
250 for commit in data['push']['commits']:
251 for commit in data['push']['commits']:
251 if commit['branch'] not in branches_commits:
252 if commit['branch'] not in branches_commits:
252 branch_commits = {'branch': branch_data[commit['branch']],
253 branch_commits = {'branch': branch_data[commit['branch']],
253 'commits': []}
254 'commits': []}
254 branches_commits[commit['branch']] = branch_commits
255 branches_commits[commit['branch']] = branch_commits
255
256
256 branch_commits = branches_commits[commit['branch']]
257 branch_commits = branches_commits[commit['branch']]
257 branch_commits['commits'].append(commit)
258 branch_commits['commits'].append(commit)
258
259
259 title = Template(r'''
260 title = Template(r'''
260 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
261 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
261 ''').render(data=data)
262 ''').render(data=data)
262
263
263 repo_push_template = Template(textwrap.dedent(r'''
264 repo_push_template = Template(textwrap.dedent(r'''
264 %for branch, branch_commits in branches_commits.items():
265 %for branch, branch_commits in branches_commits.items():
265 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
266 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} on branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
266 %for commit in branch_commits['commits']:
267 %for commit in branch_commits['commits']:
267 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
268 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
268 %endfor
269 %endfor
269 %endfor
270 %endfor
270 '''))
271 '''))
271
272
272 text = repo_push_template.render(
273 text = repo_push_template.render(
273 data=data,
274 data=data,
274 branches_commits=branches_commits,
275 branches_commits=branches_commits,
275 html_to_slack_links=html_to_slack_links,
276 html_to_slack_links=html_to_slack_links,
276 )
277 )
277
278
278 return title, text
279 return title, text
279
280
280 def format_repo_create_event(self, data):
281 def format_repo_create_event(self, data):
281 title = Template(r'''
282 title = Template(r'''
282 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
283 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
283 ''').render(data=data)
284 ''').render(data=data)
284
285
285 text = Template(textwrap.dedent(r'''
286 text = Template(textwrap.dedent(r'''
286 repo_url: ${data['repo']['url']}
287 repo_url: ${data['repo']['url']}
287 repo_type: ${data['repo']['repo_type']}
288 repo_type: ${data['repo']['repo_type']}
288 ''')).render(data=data)
289 ''')).render(data=data)
289
290
290 return title, text
291 return title, text
291
292
292
293
293 def html_to_slack_links(message):
294 def html_to_slack_links(message):
294 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
295 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
295 r'<\1|\2>', message)
296 r'<\1|\2>', message)
296
297
297
298
298 @async_task(ignore_result=True, base=RequestContextTask)
299 @async_task(ignore_result=True, base=RequestContextTask)
299 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
300 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
300 log.debug('sending %s (%s) to slack %s' % (
301 log.debug('sending %s (%s) to slack %s' % (
301 title, text, settings['service']))
302 title, text, settings['service']))
302
303
303 fields = fields or []
304 fields = fields or []
304 overrides = overrides or {}
305 overrides = overrides or {}
305
306
306 message_data = {
307 message_data = {
307 "fallback": text,
308 "fallback": text,
308 "color": "#427cc9",
309 "color": "#427cc9",
309 "pretext": title,
310 "pretext": title,
310 #"author_name": "Bobby Tables",
311 #"author_name": "Bobby Tables",
311 #"author_link": "http://flickr.com/bobby/",
312 #"author_link": "http://flickr.com/bobby/",
312 #"author_icon": "http://flickr.com/icons/bobby.jpg",
313 #"author_icon": "http://flickr.com/icons/bobby.jpg",
313 #"title": "Slack API Documentation",
314 #"title": "Slack API Documentation",
314 #"title_link": "https://api.slack.com/",
315 #"title_link": "https://api.slack.com/",
315 "text": text,
316 "text": text,
316 "fields": fields,
317 "fields": fields,
317 #"image_url": "http://my-website.com/path/to/image.jpg",
318 #"image_url": "http://my-website.com/path/to/image.jpg",
318 #"thumb_url": "http://example.com/path/to/thumb.png",
319 #"thumb_url": "http://example.com/path/to/thumb.png",
319 "footer": "RhodeCode",
320 "footer": "RhodeCode",
320 #"footer_icon": "",
321 #"footer_icon": "",
321 "ts": time.time(),
322 "ts": time.time(),
322 "mrkdwn_in": ["pretext", "text"]
323 "mrkdwn_in": ["pretext", "text"]
323 }
324 }
324 message_data.update(overrides)
325 message_data.update(overrides)
325 json_message = {
326 json_message = {
326 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
327 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
327 "channel": settings.get('channel', ''),
328 "channel": settings.get('channel', ''),
328 "username": settings.get('username', 'Rhodecode'),
329 "username": settings.get('username', 'Rhodecode'),
329 "attachments": [message_data]
330 "attachments": [message_data]
330 }
331 }
331
332
332 resp = requests.post(settings['service'], json=json_message)
333 resp = requests.post(settings['service'], json=json_message)
333 resp.raise_for_status() # raise exception on a failed request
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