##// END OF EJS Templates
integrations: organized templates a bit more.
marcink -
r3218:5fbf95d0 default
parent child Browse files
Show More
@@ -1,254 +1,253 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import deform
22 import deform
23 import logging
23 import logging
24 import requests
24 import requests
25 import colander
25 import colander
26 import textwrap
26 import textwrap
27 from mako.template import Template
27 from mako.template import Template
28 from rhodecode import events
28 from rhodecode import events
29 from rhodecode.translation import _
29 from rhodecode.translation import _
30 from rhodecode.lib import helpers as h
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
31 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
32 from rhodecode.lib.colander_utils import strip_whitespace
32 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.integrations.types.base import (
33 from rhodecode.integrations.types.base import (
34 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
34 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
35 requests_retry_call)
35 requests_retry_call)
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39 REPO_PUSH_TEMPLATE = Template('''
40 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
41 <br>
42 <ul>
43 %for branch, branch_commits in branches_commits.items():
44 <li>
45 % if branch:
46 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
47 % else:
48 to trunk
49 % endif
50 <ul>
51 % for commit in branch_commits['commits']:
52 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
53 % endfor
54 </ul>
55 </li>
56 %endfor
57 ''')
58
39
59
40 class HipchatSettingsSchema(colander.Schema):
60 class HipchatSettingsSchema(colander.Schema):
41 color_choices = [
61 color_choices = [
42 ('yellow', _('Yellow')),
62 ('yellow', _('Yellow')),
43 ('red', _('Red')),
63 ('red', _('Red')),
44 ('green', _('Green')),
64 ('green', _('Green')),
45 ('purple', _('Purple')),
65 ('purple', _('Purple')),
46 ('gray', _('Gray')),
66 ('gray', _('Gray')),
47 ]
67 ]
48
68
49 server_url = colander.SchemaNode(
69 server_url = colander.SchemaNode(
50 colander.String(),
70 colander.String(),
51 title=_('Hipchat server URL'),
71 title=_('Hipchat server URL'),
52 description=_('Hipchat integration url.'),
72 description=_('Hipchat integration url.'),
53 default='',
73 default='',
54 preparer=strip_whitespace,
74 preparer=strip_whitespace,
55 validator=colander.url,
75 validator=colander.url,
56 widget=deform.widget.TextInputWidget(
76 widget=deform.widget.TextInputWidget(
57 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
77 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
58 ),
78 ),
59 )
79 )
60 notify = colander.SchemaNode(
80 notify = colander.SchemaNode(
61 colander.Bool(),
81 colander.Bool(),
62 title=_('Notify'),
82 title=_('Notify'),
63 description=_('Make a notification to the users in room.'),
83 description=_('Make a notification to the users in room.'),
64 missing=False,
84 missing=False,
65 default=False,
85 default=False,
66 )
86 )
67 color = colander.SchemaNode(
87 color = colander.SchemaNode(
68 colander.String(),
88 colander.String(),
69 title=_('Color'),
89 title=_('Color'),
70 description=_('Background color of message.'),
90 description=_('Background color of message.'),
71 missing='',
91 missing='',
72 validator=colander.OneOf([x[0] for x in color_choices]),
92 validator=colander.OneOf([x[0] for x in color_choices]),
73 widget=deform.widget.Select2Widget(
93 widget=deform.widget.Select2Widget(
74 values=color_choices,
94 values=color_choices,
75 ),
95 ),
76 )
96 )
77
97
78
98
79 repo_push_template = Template('''
80 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
81 <br>
82 <ul>
83 %for branch, branch_commits in branches_commits.items():
84 <li>
85 % if branch:
86 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
87 % else:
88 to trunk
89 % endif
90 <ul>
91 % for commit in branch_commits['commits']:
92 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
93 % endfor
94 </ul>
95 </li>
96 %endfor
97 ''')
98
99
100 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
99 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
101 key = 'hipchat'
100 key = 'hipchat'
102 display_name = _('Hipchat')
101 display_name = _('Hipchat')
103 description = _('Send events such as repo pushes and pull requests to '
102 description = _('Send events such as repo pushes and pull requests to '
104 'your hipchat channel.')
103 'your hipchat channel.')
105
104
106 @classmethod
105 @classmethod
107 def icon(cls):
106 def icon(cls):
108 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>'''
107 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>'''
109
108
110 valid_events = [
109 valid_events = [
111 events.PullRequestCloseEvent,
110 events.PullRequestCloseEvent,
112 events.PullRequestMergeEvent,
111 events.PullRequestMergeEvent,
113 events.PullRequestUpdateEvent,
112 events.PullRequestUpdateEvent,
114 events.PullRequestCommentEvent,
113 events.PullRequestCommentEvent,
115 events.PullRequestReviewEvent,
114 events.PullRequestReviewEvent,
116 events.PullRequestCreateEvent,
115 events.PullRequestCreateEvent,
117 events.RepoPushEvent,
116 events.RepoPushEvent,
118 events.RepoCreateEvent,
117 events.RepoCreateEvent,
119 ]
118 ]
120
119
121 def send_event(self, event):
120 def send_event(self, event):
122 if event.__class__ not in self.valid_events:
121 if event.__class__ not in self.valid_events:
123 log.debug('event not valid: %r', event)
122 log.debug('event not valid: %r', event)
124 return
123 return
125
124
126 if event.name not in self.settings['events']:
125 if event.name not in self.settings['events']:
127 log.debug('event ignored: %r', event)
126 log.debug('event ignored: %r', event)
128 return
127 return
129
128
130 data = event.as_dict()
129 data = event.as_dict()
131
130
132 text = '<b>%s<b> caused a <b>%s</b> event' % (
131 text = '<b>%s<b> caused a <b>%s</b> event' % (
133 data['actor']['username'], event.name)
132 data['actor']['username'], event.name)
134
133
135 log.debug('handling hipchat event for %s', event.name)
134 log.debug('handling hipchat event for %s', event.name)
136
135
137 if isinstance(event, events.PullRequestCommentEvent):
136 if isinstance(event, events.PullRequestCommentEvent):
138 text = self.format_pull_request_comment_event(event, data)
137 text = self.format_pull_request_comment_event(event, data)
139 elif isinstance(event, events.PullRequestReviewEvent):
138 elif isinstance(event, events.PullRequestReviewEvent):
140 text = self.format_pull_request_review_event(event, data)
139 text = self.format_pull_request_review_event(event, data)
141 elif isinstance(event, events.PullRequestEvent):
140 elif isinstance(event, events.PullRequestEvent):
142 text = self.format_pull_request_event(event, data)
141 text = self.format_pull_request_event(event, data)
143 elif isinstance(event, events.RepoPushEvent):
142 elif isinstance(event, events.RepoPushEvent):
144 text = self.format_repo_push_event(data)
143 text = self.format_repo_push_event(data)
145 elif isinstance(event, events.RepoCreateEvent):
144 elif isinstance(event, events.RepoCreateEvent):
146 text = self.format_repo_create_event(data)
145 text = self.format_repo_create_event(data)
147 else:
146 else:
148 log.error('unhandled event type: %r', event)
147 log.error('unhandled event type: %r', event)
149
148
150 run_task(post_text_to_hipchat, self.settings, text)
149 run_task(post_text_to_hipchat, self.settings, text)
151
150
152 def settings_schema(self):
151 def settings_schema(self):
153 schema = HipchatSettingsSchema()
152 schema = HipchatSettingsSchema()
154 schema.add(colander.SchemaNode(
153 schema.add(colander.SchemaNode(
155 colander.Set(),
154 colander.Set(),
156 widget=deform.widget.CheckboxChoiceWidget(
155 widget=deform.widget.CheckboxChoiceWidget(
157 values=sorted(
156 values=sorted(
158 [(e.name, e.display_name) for e in self.valid_events]
157 [(e.name, e.display_name) for e in self.valid_events]
159 )
158 )
160 ),
159 ),
161 description="Events activated for this integration",
160 description="Events activated for this integration",
162 name='events'
161 name='events'
163 ))
162 ))
164
163
165 return schema
164 return schema
166
165
167 def format_pull_request_comment_event(self, event, data):
166 def format_pull_request_comment_event(self, event, data):
168 comment_text = data['comment']['text']
167 comment_text = data['comment']['text']
169 if len(comment_text) > 200:
168 if len(comment_text) > 200:
170 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
169 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
171 comment_text=h.html_escape(comment_text[:200]),
170 comment_text=h.html_escape(comment_text[:200]),
172 comment_url=data['comment']['url'],
171 comment_url=data['comment']['url'],
173 )
172 )
174
173
175 comment_status = ''
174 comment_status = ''
176 if data['comment']['status']:
175 if data['comment']['status']:
177 comment_status = '[{}]: '.format(data['comment']['status'])
176 comment_status = '[{}]: '.format(data['comment']['status'])
178
177
179 return (textwrap.dedent(
178 return (textwrap.dedent(
180 '''
179 '''
181 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
180 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
182 >>> {comment_status}{comment_text}
181 >>> {comment_status}{comment_text}
183 ''').format(
182 ''').format(
184 comment_status=comment_status,
183 comment_status=comment_status,
185 user=data['actor']['username'],
184 user=data['actor']['username'],
186 number=data['pullrequest']['pull_request_id'],
185 number=data['pullrequest']['pull_request_id'],
187 pr_url=data['pullrequest']['url'],
186 pr_url=data['pullrequest']['url'],
188 pr_status=data['pullrequest']['status'],
187 pr_status=data['pullrequest']['status'],
189 pr_title=h.html_escape(data['pullrequest']['title']),
188 pr_title=h.html_escape(data['pullrequest']['title']),
190 comment_text=h.html_escape(comment_text)
189 comment_text=h.html_escape(comment_text)
191 )
190 )
192 )
191 )
193
192
194 def format_pull_request_review_event(self, event, data):
193 def format_pull_request_review_event(self, event, data):
195 return (textwrap.dedent(
194 return (textwrap.dedent(
196 '''
195 '''
197 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
196 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
198 ''').format(
197 ''').format(
199 user=data['actor']['username'],
198 user=data['actor']['username'],
200 number=data['pullrequest']['pull_request_id'],
199 number=data['pullrequest']['pull_request_id'],
201 pr_url=data['pullrequest']['url'],
200 pr_url=data['pullrequest']['url'],
202 pr_status=data['pullrequest']['status'],
201 pr_status=data['pullrequest']['status'],
203 pr_title=h.html_escape(data['pullrequest']['title']),
202 pr_title=h.html_escape(data['pullrequest']['title']),
204 )
203 )
205 )
204 )
206
205
207 def format_pull_request_event(self, event, data):
206 def format_pull_request_event(self, event, data):
208 action = {
207 action = {
209 events.PullRequestCloseEvent: 'closed',
208 events.PullRequestCloseEvent: 'closed',
210 events.PullRequestMergeEvent: 'merged',
209 events.PullRequestMergeEvent: 'merged',
211 events.PullRequestUpdateEvent: 'updated',
210 events.PullRequestUpdateEvent: 'updated',
212 events.PullRequestCreateEvent: 'created',
211 events.PullRequestCreateEvent: 'created',
213 }.get(event.__class__, str(event.__class__))
212 }.get(event.__class__, str(event.__class__))
214
213
215 return ('Pull request <a href="{url}">#{number}</a> - {title} '
214 return ('Pull request <a href="{url}">#{number}</a> - {title} '
216 '{action} by <b>{user}</b>').format(
215 '{action} by <b>{user}</b>').format(
217 user=data['actor']['username'],
216 user=data['actor']['username'],
218 number=data['pullrequest']['pull_request_id'],
217 number=data['pullrequest']['pull_request_id'],
219 url=data['pullrequest']['url'],
218 url=data['pullrequest']['url'],
220 title=h.html_escape(data['pullrequest']['title']),
219 title=h.html_escape(data['pullrequest']['title']),
221 action=action
220 action=action
222 )
221 )
223
222
224 def format_repo_push_event(self, data):
223 def format_repo_push_event(self, data):
225 branches_commits = self.aggregate_branch_data(
224 branches_commits = self.aggregate_branch_data(
226 data['push']['branches'], data['push']['commits'])
225 data['push']['branches'], data['push']['commits'])
227
226
228 result = render_with_traceback(
227 result = render_with_traceback(
229 repo_push_template,
228 REPO_PUSH_TEMPLATE,
230 data=data,
229 data=data,
231 branches_commits=branches_commits,
230 branches_commits=branches_commits,
232 )
231 )
233 return result
232 return result
234
233
235 def format_repo_create_event(self, data):
234 def format_repo_create_event(self, data):
236 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
235 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
237 data['repo']['url'],
236 data['repo']['url'],
238 h.html_escape(data['repo']['repo_name']),
237 h.html_escape(data['repo']['repo_name']),
239 data['repo']['repo_type'],
238 data['repo']['repo_type'],
240 data['actor']['username'],
239 data['actor']['username'],
241 )
240 )
242
241
243
242
244 @async_task(ignore_result=True, base=RequestContextTask)
243 @async_task(ignore_result=True, base=RequestContextTask)
245 def post_text_to_hipchat(settings, text):
244 def post_text_to_hipchat(settings, text):
246 log.debug('sending %s to hipchat %s', text, settings['server_url'])
245 log.debug('sending %s to hipchat %s', text, settings['server_url'])
247 json_message = {
246 json_message = {
248 "message": text,
247 "message": text,
249 "color": settings.get('color', 'yellow'),
248 "color": settings.get('color', 'yellow'),
250 "notify": settings.get('notify', False),
249 "notify": settings.get('notify', False),
251 }
250 }
252 req_session = requests_retry_call()
251 req_session = requests_retry_call()
253 resp = req_session.post(settings['server_url'], json=json_message, timeout=60)
252 resp = req_session.post(settings['server_url'], json=json_message, timeout=60)
254 resp.raise_for_status() # raise exception on a failed request
253 resp.raise_for_status() # raise exception on a failed request
@@ -1,350 +1,351 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from __future__ import unicode_literals
21 from __future__ import unicode_literals
22 import re
22 import re
23 import time
23 import time
24 import textwrap
24 import textwrap
25 import logging
25 import logging
26
26
27 import deform
27 import deform
28 import requests
28 import requests
29 import colander
29 import colander
30 from mako.template import Template
30 from mako.template import Template
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
35 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.integrations.types.base import (
37 from rhodecode.integrations.types.base import (
38 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
38 IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback,
39 requests_retry_call)
39 requests_retry_call)
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 def html_to_slack_links(message):
45 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
46 r'<\1|\2>', message)
47
48
49 REPO_PUSH_TEMPLATE = Template('''
50 <%
51 def branch_text(branch):
52 if branch:
53 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
54 else:
55 ## case for SVN no branch push...
56 return 'to trunk'
57 %> \
58
59 % for branch, branch_commits in branches_commits.items():
60 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
61 % for commit in branch_commits['commits']:
62 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
63 % endfor
64 % endfor
65 ''')
66
67
44 class SlackSettingsSchema(colander.Schema):
68 class SlackSettingsSchema(colander.Schema):
45 service = colander.SchemaNode(
69 service = colander.SchemaNode(
46 colander.String(),
70 colander.String(),
47 title=_('Slack service URL'),
71 title=_('Slack service URL'),
48 description=h.literal(_(
72 description=h.literal(_(
49 'This can be setup at the '
73 'This can be setup at the '
50 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
74 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
51 'slack app manager</a>')),
75 'slack app manager</a>')),
52 default='',
76 default='',
53 preparer=strip_whitespace,
77 preparer=strip_whitespace,
54 validator=colander.url,
78 validator=colander.url,
55 widget=deform.widget.TextInputWidget(
79 widget=deform.widget.TextInputWidget(
56 placeholder='https://hooks.slack.com/services/...',
80 placeholder='https://hooks.slack.com/services/...',
57 ),
81 ),
58 )
82 )
59 username = colander.SchemaNode(
83 username = colander.SchemaNode(
60 colander.String(),
84 colander.String(),
61 title=_('Username'),
85 title=_('Username'),
62 description=_('Username to show notifications coming from.'),
86 description=_('Username to show notifications coming from.'),
63 missing='Rhodecode',
87 missing='Rhodecode',
64 preparer=strip_whitespace,
88 preparer=strip_whitespace,
65 widget=deform.widget.TextInputWidget(
89 widget=deform.widget.TextInputWidget(
66 placeholder='Rhodecode'
90 placeholder='Rhodecode'
67 ),
91 ),
68 )
92 )
69 channel = colander.SchemaNode(
93 channel = colander.SchemaNode(
70 colander.String(),
94 colander.String(),
71 title=_('Channel'),
95 title=_('Channel'),
72 description=_('Channel to send notifications to.'),
96 description=_('Channel to send notifications to.'),
73 missing='',
97 missing='',
74 preparer=strip_whitespace,
98 preparer=strip_whitespace,
75 widget=deform.widget.TextInputWidget(
99 widget=deform.widget.TextInputWidget(
76 placeholder='#general'
100 placeholder='#general'
77 ),
101 ),
78 )
102 )
79 icon_emoji = colander.SchemaNode(
103 icon_emoji = colander.SchemaNode(
80 colander.String(),
104 colander.String(),
81 title=_('Emoji'),
105 title=_('Emoji'),
82 description=_('Emoji to use eg. :studio_microphone:'),
106 description=_('Emoji to use eg. :studio_microphone:'),
83 missing='',
107 missing='',
84 preparer=strip_whitespace,
108 preparer=strip_whitespace,
85 widget=deform.widget.TextInputWidget(
109 widget=deform.widget.TextInputWidget(
86 placeholder=':studio_microphone:'
110 placeholder=':studio_microphone:'
87 ),
111 ),
88 )
112 )
89
113
90
114
91 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
115 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
92 key = 'slack'
116 key = 'slack'
93 display_name = _('Slack')
117 display_name = _('Slack')
94 description = _('Send events such as repo pushes and pull requests to '
118 description = _('Send events such as repo pushes and pull requests to '
95 'your slack channel.')
119 'your slack channel.')
96
120
97 @classmethod
121 @classmethod
98 def icon(cls):
122 def icon(cls):
99 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>'''
123 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>'''
100
124
101 valid_events = [
125 valid_events = [
102 events.PullRequestCloseEvent,
126 events.PullRequestCloseEvent,
103 events.PullRequestMergeEvent,
127 events.PullRequestMergeEvent,
104 events.PullRequestUpdateEvent,
128 events.PullRequestUpdateEvent,
105 events.PullRequestCommentEvent,
129 events.PullRequestCommentEvent,
106 events.PullRequestReviewEvent,
130 events.PullRequestReviewEvent,
107 events.PullRequestCreateEvent,
131 events.PullRequestCreateEvent,
108 events.RepoPushEvent,
132 events.RepoPushEvent,
109 events.RepoCreateEvent,
133 events.RepoCreateEvent,
110 ]
134 ]
111
135
112 def send_event(self, event):
136 def send_event(self, event):
113 if event.__class__ not in self.valid_events:
137 if event.__class__ not in self.valid_events:
114 log.debug('event not valid: %r', event)
138 log.debug('event not valid: %r', event)
115 return
139 return
116
140
117 if event.name not in self.settings['events']:
141 if event.name not in self.settings['events']:
118 log.debug('event ignored: %r', event)
142 log.debug('event ignored: %r', event)
119 return
143 return
120
144
121 data = event.as_dict()
145 data = event.as_dict()
122
146
123 # defaults
147 # defaults
124 title = '*%s* caused a *%s* event' % (
148 title = '*%s* caused a *%s* event' % (
125 data['actor']['username'], event.name)
149 data['actor']['username'], event.name)
126 text = '*%s* caused a *%s* event' % (
150 text = '*%s* caused a *%s* event' % (
127 data['actor']['username'], event.name)
151 data['actor']['username'], event.name)
128 fields = None
152 fields = None
129 overrides = None
153 overrides = None
130
154
131 log.debug('handling slack event for %s', event.name)
155 log.debug('handling slack event for %s', event.name)
132
156
133 if isinstance(event, events.PullRequestCommentEvent):
157 if isinstance(event, events.PullRequestCommentEvent):
134 (title, text, fields, overrides) \
158 (title, text, fields, overrides) \
135 = self.format_pull_request_comment_event(event, data)
159 = self.format_pull_request_comment_event(event, data)
136 elif isinstance(event, events.PullRequestReviewEvent):
160 elif isinstance(event, events.PullRequestReviewEvent):
137 title, text = self.format_pull_request_review_event(event, data)
161 title, text = self.format_pull_request_review_event(event, data)
138 elif isinstance(event, events.PullRequestEvent):
162 elif isinstance(event, events.PullRequestEvent):
139 title, text = self.format_pull_request_event(event, data)
163 title, text = self.format_pull_request_event(event, data)
140 elif isinstance(event, events.RepoPushEvent):
164 elif isinstance(event, events.RepoPushEvent):
141 title, text = self.format_repo_push_event(data)
165 title, text = self.format_repo_push_event(data)
142 elif isinstance(event, events.RepoCreateEvent):
166 elif isinstance(event, events.RepoCreateEvent):
143 title, text = self.format_repo_create_event(data)
167 title, text = self.format_repo_create_event(data)
144 else:
168 else:
145 log.error('unhandled event type: %r', event)
169 log.error('unhandled event type: %r', event)
146
170
147 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
171 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
148
172
149 def settings_schema(self):
173 def settings_schema(self):
150 schema = SlackSettingsSchema()
174 schema = SlackSettingsSchema()
151 schema.add(colander.SchemaNode(
175 schema.add(colander.SchemaNode(
152 colander.Set(),
176 colander.Set(),
153 widget=deform.widget.CheckboxChoiceWidget(
177 widget=deform.widget.CheckboxChoiceWidget(
154 values=sorted(
178 values=sorted(
155 [(e.name, e.display_name) for e in self.valid_events]
179 [(e.name, e.display_name) for e in self.valid_events]
156 )
180 )
157 ),
181 ),
158 description="Events activated for this integration",
182 description="Events activated for this integration",
159 name='events'
183 name='events'
160 ))
184 ))
161
185
162 return schema
186 return schema
163
187
164 def format_pull_request_comment_event(self, event, data):
188 def format_pull_request_comment_event(self, event, data):
165 comment_text = data['comment']['text']
189 comment_text = data['comment']['text']
166 if len(comment_text) > 200:
190 if len(comment_text) > 200:
167 comment_text = '<{comment_url}|{comment_text}...>'.format(
191 comment_text = '<{comment_url}|{comment_text}...>'.format(
168 comment_text=comment_text[:200],
192 comment_text=comment_text[:200],
169 comment_url=data['comment']['url'],
193 comment_url=data['comment']['url'],
170 )
194 )
171
195
172 fields = None
196 fields = None
173 overrides = None
197 overrides = None
174 status_text = None
198 status_text = None
175
199
176 if data['comment']['status']:
200 if data['comment']['status']:
177 status_color = {
201 status_color = {
178 'approved': '#0ac878',
202 'approved': '#0ac878',
179 'rejected': '#e85e4d'}.get(data['comment']['status'])
203 'rejected': '#e85e4d'}.get(data['comment']['status'])
180
204
181 if status_color:
205 if status_color:
182 overrides = {"color": status_color}
206 overrides = {"color": status_color}
183
207
184 status_text = data['comment']['status']
208 status_text = data['comment']['status']
185
209
186 if data['comment']['file']:
210 if data['comment']['file']:
187 fields = [
211 fields = [
188 {
212 {
189 "title": "file",
213 "title": "file",
190 "value": data['comment']['file']
214 "value": data['comment']['file']
191 },
215 },
192 {
216 {
193 "title": "line",
217 "title": "line",
194 "value": data['comment']['line']
218 "value": data['comment']['line']
195 }
219 }
196 ]
220 ]
197
221
198 template = Template(textwrap.dedent(r'''
222 template = Template(textwrap.dedent(r'''
199 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
223 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
200 '''))
224 '''))
201 title = render_with_traceback(
225 title = render_with_traceback(
202 template, data=data, comment=event.comment)
226 template, data=data, comment=event.comment)
203
227
204 template = Template(textwrap.dedent(r'''
228 template = Template(textwrap.dedent(r'''
205 *pull request title*: ${pr_title}
229 *pull request title*: ${pr_title}
206 % if status_text:
230 % if status_text:
207 *submitted status*: `${status_text}`
231 *submitted status*: `${status_text}`
208 % endif
232 % endif
209 >>> ${comment_text}
233 >>> ${comment_text}
210 '''))
234 '''))
211 text = render_with_traceback(
235 text = render_with_traceback(
212 template,
236 template,
213 comment_text=comment_text,
237 comment_text=comment_text,
214 pr_title=data['pullrequest']['title'],
238 pr_title=data['pullrequest']['title'],
215 status_text=status_text)
239 status_text=status_text)
216
240
217 return title, text, fields, overrides
241 return title, text, fields, overrides
218
242
219 def format_pull_request_review_event(self, event, data):
243 def format_pull_request_review_event(self, event, data):
220 template = Template(textwrap.dedent(r'''
244 template = Template(textwrap.dedent(r'''
221 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
245 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
222 '''))
246 '''))
223 title = render_with_traceback(template, data=data)
247 title = render_with_traceback(template, data=data)
224
248
225 template = Template(textwrap.dedent(r'''
249 template = Template(textwrap.dedent(r'''
226 *pull request title*: ${pr_title}
250 *pull request title*: ${pr_title}
227 '''))
251 '''))
228 text = render_with_traceback(
252 text = render_with_traceback(
229 template,
253 template,
230 pr_title=data['pullrequest']['title'])
254 pr_title=data['pullrequest']['title'])
231
255
232 return title, text
256 return title, text
233
257
234 def format_pull_request_event(self, event, data):
258 def format_pull_request_event(self, event, data):
235 action = {
259 action = {
236 events.PullRequestCloseEvent: 'closed',
260 events.PullRequestCloseEvent: 'closed',
237 events.PullRequestMergeEvent: 'merged',
261 events.PullRequestMergeEvent: 'merged',
238 events.PullRequestUpdateEvent: 'updated',
262 events.PullRequestUpdateEvent: 'updated',
239 events.PullRequestCreateEvent: 'created',
263 events.PullRequestCreateEvent: 'created',
240 }.get(event.__class__, str(event.__class__))
264 }.get(event.__class__, str(event.__class__))
241
265
242 template = Template(textwrap.dedent(r'''
266 template = Template(textwrap.dedent(r'''
243 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
267 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
244 '''))
268 '''))
245 title = render_with_traceback(template, data=data, action=action)
269 title = render_with_traceback(template, data=data, action=action)
246
270
247 template = Template(textwrap.dedent(r'''
271 template = Template(textwrap.dedent(r'''
248 *pull request title*: ${pr_title}
272 *pull request title*: ${pr_title}
249 %if data['pullrequest']['commits']:
273 %if data['pullrequest']['commits']:
250 *commits*: ${len(data['pullrequest']['commits'])}
274 *commits*: ${len(data['pullrequest']['commits'])}
251 %endif
275 %endif
252 '''))
276 '''))
253 text = render_with_traceback(
277 text = render_with_traceback(
254 template,
278 template,
255 pr_title=data['pullrequest']['title'],
279 pr_title=data['pullrequest']['title'],
256 data=data)
280 data=data)
257
281
258 return title, text
282 return title, text
259
283
260 def format_repo_push_event(self, data):
284 def format_repo_push_event(self, data):
261
262 branches_commits = self.aggregate_branch_data(
285 branches_commits = self.aggregate_branch_data(
263 data['push']['branches'], data['push']['commits'])
286 data['push']['branches'], data['push']['commits'])
264
287
265 template = Template(r'''
288 template = Template(r'''
266 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
289 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
267 ''')
290 ''')
268 title = render_with_traceback(template, data=data)
291 title = render_with_traceback(template, data=data)
269
292
270 repo_push_template = Template(textwrap.dedent(r'''
271 <%
272 def branch_text(branch):
273 if branch:
274 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
275 else:
276 ## case for SVN no branch push...
277 return 'to trunk'
278 %> \
279 % for branch, branch_commits in branches_commits.items():
280 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
281 % for commit in branch_commits['commits']:
282 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
283 % endfor
284 % endfor
285 '''))
286
287 text = render_with_traceback(
293 text = render_with_traceback(
288 repo_push_template,
294 REPO_PUSH_TEMPLATE,
289 data=data,
295 data=data,
290 branches_commits=branches_commits,
296 branches_commits=branches_commits,
291 html_to_slack_links=html_to_slack_links,
297 html_to_slack_links=html_to_slack_links,
292 )
298 )
293
299
294 return title, text
300 return title, text
295
301
296 def format_repo_create_event(self, data):
302 def format_repo_create_event(self, data):
297 template = Template(r'''
303 template = Template(r'''
298 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
304 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
299 ''')
305 ''')
300 title = render_with_traceback(template, data=data)
306 title = render_with_traceback(template, data=data)
301
307
302 template = Template(textwrap.dedent(r'''
308 template = Template(textwrap.dedent(r'''
303 repo_url: ${data['repo']['url']}
309 repo_url: ${data['repo']['url']}
304 repo_type: ${data['repo']['repo_type']}
310 repo_type: ${data['repo']['repo_type']}
305 '''))
311 '''))
306 text = render_with_traceback(template, data=data)
312 text = render_with_traceback(template, data=data)
307
313
308 return title, text
314 return title, text
309
315
310
316
311 def html_to_slack_links(message):
312 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
313 r'<\1|\2>', message)
314
315
316 @async_task(ignore_result=True, base=RequestContextTask)
317 @async_task(ignore_result=True, base=RequestContextTask)
317 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
318 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
318 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
319 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
319
320
320 fields = fields or []
321 fields = fields or []
321 overrides = overrides or {}
322 overrides = overrides or {}
322
323
323 message_data = {
324 message_data = {
324 "fallback": text,
325 "fallback": text,
325 "color": "#427cc9",
326 "color": "#427cc9",
326 "pretext": title,
327 "pretext": title,
327 #"author_name": "Bobby Tables",
328 #"author_name": "Bobby Tables",
328 #"author_link": "http://flickr.com/bobby/",
329 #"author_link": "http://flickr.com/bobby/",
329 #"author_icon": "http://flickr.com/icons/bobby.jpg",
330 #"author_icon": "http://flickr.com/icons/bobby.jpg",
330 #"title": "Slack API Documentation",
331 #"title": "Slack API Documentation",
331 #"title_link": "https://api.slack.com/",
332 #"title_link": "https://api.slack.com/",
332 "text": text,
333 "text": text,
333 "fields": fields,
334 "fields": fields,
334 #"image_url": "http://my-website.com/path/to/image.jpg",
335 #"image_url": "http://my-website.com/path/to/image.jpg",
335 #"thumb_url": "http://example.com/path/to/thumb.png",
336 #"thumb_url": "http://example.com/path/to/thumb.png",
336 "footer": "RhodeCode",
337 "footer": "RhodeCode",
337 #"footer_icon": "",
338 #"footer_icon": "",
338 "ts": time.time(),
339 "ts": time.time(),
339 "mrkdwn_in": ["pretext", "text"]
340 "mrkdwn_in": ["pretext", "text"]
340 }
341 }
341 message_data.update(overrides)
342 message_data.update(overrides)
342 json_message = {
343 json_message = {
343 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
344 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
344 "channel": settings.get('channel', ''),
345 "channel": settings.get('channel', ''),
345 "username": settings.get('username', 'Rhodecode'),
346 "username": settings.get('username', 'Rhodecode'),
346 "attachments": [message_data]
347 "attachments": [message_data]
347 }
348 }
348 req_session = requests_retry_call()
349 req_session = requests_retry_call()
349 resp = req_session.post(settings['service'], json=json_message, timeout=60)
350 resp = req_session.post(settings['service'], json=json_message, timeout=60)
350 resp.raise_for_status() # raise exception on a failed request
351 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now