##// END OF EJS Templates
events: updated logging for ignored events.
marcink -
r3398:030252b4 default
parent child Browse files
Show More
@@ -1,253 +1,255 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 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('''
39 REPO_PUSH_TEMPLATE = Template('''
40 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
40 <b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
41 <br>
41 <br>
42 <ul>
42 <ul>
43 %for branch, branch_commits in branches_commits.items():
43 %for branch, branch_commits in branches_commits.items():
44 <li>
44 <li>
45 % if branch:
45 % if branch:
46 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
46 <a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
47 % else:
47 % else:
48 to trunk
48 to trunk
49 % endif
49 % endif
50 <ul>
50 <ul>
51 % for commit in branch_commits['commits']:
51 % for commit in branch_commits['commits']:
52 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
52 <li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
53 % endfor
53 % endfor
54 </ul>
54 </ul>
55 </li>
55 </li>
56 %endfor
56 %endfor
57 ''')
57 ''')
58
58
59
59
60 class HipchatSettingsSchema(colander.Schema):
60 class HipchatSettingsSchema(colander.Schema):
61 color_choices = [
61 color_choices = [
62 ('yellow', _('Yellow')),
62 ('yellow', _('Yellow')),
63 ('red', _('Red')),
63 ('red', _('Red')),
64 ('green', _('Green')),
64 ('green', _('Green')),
65 ('purple', _('Purple')),
65 ('purple', _('Purple')),
66 ('gray', _('Gray')),
66 ('gray', _('Gray')),
67 ]
67 ]
68
68
69 server_url = colander.SchemaNode(
69 server_url = colander.SchemaNode(
70 colander.String(),
70 colander.String(),
71 title=_('Hipchat server URL'),
71 title=_('Hipchat server URL'),
72 description=_('Hipchat integration url.'),
72 description=_('Hipchat integration url.'),
73 default='',
73 default='',
74 preparer=strip_whitespace,
74 preparer=strip_whitespace,
75 validator=colander.url,
75 validator=colander.url,
76 widget=deform.widget.TextInputWidget(
76 widget=deform.widget.TextInputWidget(
77 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
77 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
78 ),
78 ),
79 )
79 )
80 notify = colander.SchemaNode(
80 notify = colander.SchemaNode(
81 colander.Bool(),
81 colander.Bool(),
82 title=_('Notify'),
82 title=_('Notify'),
83 description=_('Make a notification to the users in room.'),
83 description=_('Make a notification to the users in room.'),
84 missing=False,
84 missing=False,
85 default=False,
85 default=False,
86 )
86 )
87 color = colander.SchemaNode(
87 color = colander.SchemaNode(
88 colander.String(),
88 colander.String(),
89 title=_('Color'),
89 title=_('Color'),
90 description=_('Background color of message.'),
90 description=_('Background color of message.'),
91 missing='',
91 missing='',
92 validator=colander.OneOf([x[0] for x in color_choices]),
92 validator=colander.OneOf([x[0] for x in color_choices]),
93 widget=deform.widget.Select2Widget(
93 widget=deform.widget.Select2Widget(
94 values=color_choices,
94 values=color_choices,
95 ),
95 ),
96 )
96 )
97
97
98
98
99 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
99 class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
100 key = 'hipchat'
100 key = 'hipchat'
101 display_name = _('Hipchat')
101 display_name = _('Hipchat')
102 description = _('Send events such as repo pushes and pull requests to '
102 description = _('Send events such as repo pushes and pull requests to '
103 'your hipchat channel.')
103 'your hipchat channel.')
104
104
105 @classmethod
105 @classmethod
106 def icon(cls):
106 def icon(cls):
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>'''
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>'''
108
108
109 valid_events = [
109 valid_events = [
110 events.PullRequestCloseEvent,
110 events.PullRequestCloseEvent,
111 events.PullRequestMergeEvent,
111 events.PullRequestMergeEvent,
112 events.PullRequestUpdateEvent,
112 events.PullRequestUpdateEvent,
113 events.PullRequestCommentEvent,
113 events.PullRequestCommentEvent,
114 events.PullRequestReviewEvent,
114 events.PullRequestReviewEvent,
115 events.PullRequestCreateEvent,
115 events.PullRequestCreateEvent,
116 events.RepoPushEvent,
116 events.RepoPushEvent,
117 events.RepoCreateEvent,
117 events.RepoCreateEvent,
118 ]
118 ]
119
119
120 def send_event(self, event):
120 def send_event(self, event):
121 if event.__class__ not in self.valid_events:
121 if event.__class__ not in self.valid_events:
122 log.debug('event not valid: %r', event)
122 log.debug('event not valid: %r', event)
123 return
123 return
124
124
125 if event.name not in self.settings['events']:
125 allowed_events = self.settings['events']
126 log.debug('event ignored: %r', event)
126 if event.name not in allowed_events:
127 log.debug('event ignored: %r event %s not in allowed events %s',
128 event, event.name, allowed_events)
127 return
129 return
128
130
129 data = event.as_dict()
131 data = event.as_dict()
130
132
131 text = '<b>%s<b> caused a <b>%s</b> event' % (
133 text = '<b>%s<b> caused a <b>%s</b> event' % (
132 data['actor']['username'], event.name)
134 data['actor']['username'], event.name)
133
135
134 log.debug('handling hipchat event for %s', event.name)
136 log.debug('handling hipchat event for %s', event.name)
135
137
136 if isinstance(event, events.PullRequestCommentEvent):
138 if isinstance(event, events.PullRequestCommentEvent):
137 text = self.format_pull_request_comment_event(event, data)
139 text = self.format_pull_request_comment_event(event, data)
138 elif isinstance(event, events.PullRequestReviewEvent):
140 elif isinstance(event, events.PullRequestReviewEvent):
139 text = self.format_pull_request_review_event(event, data)
141 text = self.format_pull_request_review_event(event, data)
140 elif isinstance(event, events.PullRequestEvent):
142 elif isinstance(event, events.PullRequestEvent):
141 text = self.format_pull_request_event(event, data)
143 text = self.format_pull_request_event(event, data)
142 elif isinstance(event, events.RepoPushEvent):
144 elif isinstance(event, events.RepoPushEvent):
143 text = self.format_repo_push_event(data)
145 text = self.format_repo_push_event(data)
144 elif isinstance(event, events.RepoCreateEvent):
146 elif isinstance(event, events.RepoCreateEvent):
145 text = self.format_repo_create_event(data)
147 text = self.format_repo_create_event(data)
146 else:
148 else:
147 log.error('unhandled event type: %r', event)
149 log.error('unhandled event type: %r', event)
148
150
149 run_task(post_text_to_hipchat, self.settings, text)
151 run_task(post_text_to_hipchat, self.settings, text)
150
152
151 def settings_schema(self):
153 def settings_schema(self):
152 schema = HipchatSettingsSchema()
154 schema = HipchatSettingsSchema()
153 schema.add(colander.SchemaNode(
155 schema.add(colander.SchemaNode(
154 colander.Set(),
156 colander.Set(),
155 widget=deform.widget.CheckboxChoiceWidget(
157 widget=deform.widget.CheckboxChoiceWidget(
156 values=sorted(
158 values=sorted(
157 [(e.name, e.display_name) for e in self.valid_events]
159 [(e.name, e.display_name) for e in self.valid_events]
158 )
160 )
159 ),
161 ),
160 description="Events activated for this integration",
162 description="Events activated for this integration",
161 name='events'
163 name='events'
162 ))
164 ))
163
165
164 return schema
166 return schema
165
167
166 def format_pull_request_comment_event(self, event, data):
168 def format_pull_request_comment_event(self, event, data):
167 comment_text = data['comment']['text']
169 comment_text = data['comment']['text']
168 if len(comment_text) > 200:
170 if len(comment_text) > 200:
169 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
171 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
170 comment_text=h.html_escape(comment_text[:200]),
172 comment_text=h.html_escape(comment_text[:200]),
171 comment_url=data['comment']['url'],
173 comment_url=data['comment']['url'],
172 )
174 )
173
175
174 comment_status = ''
176 comment_status = ''
175 if data['comment']['status']:
177 if data['comment']['status']:
176 comment_status = '[{}]: '.format(data['comment']['status'])
178 comment_status = '[{}]: '.format(data['comment']['status'])
177
179
178 return (textwrap.dedent(
180 return (textwrap.dedent(
179 '''
181 '''
180 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
182 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
181 >>> {comment_status}{comment_text}
183 >>> {comment_status}{comment_text}
182 ''').format(
184 ''').format(
183 comment_status=comment_status,
185 comment_status=comment_status,
184 user=data['actor']['username'],
186 user=data['actor']['username'],
185 number=data['pullrequest']['pull_request_id'],
187 number=data['pullrequest']['pull_request_id'],
186 pr_url=data['pullrequest']['url'],
188 pr_url=data['pullrequest']['url'],
187 pr_status=data['pullrequest']['status'],
189 pr_status=data['pullrequest']['status'],
188 pr_title=h.html_escape(data['pullrequest']['title']),
190 pr_title=h.html_escape(data['pullrequest']['title']),
189 comment_text=h.html_escape(comment_text)
191 comment_text=h.html_escape(comment_text)
190 )
192 )
191 )
193 )
192
194
193 def format_pull_request_review_event(self, event, data):
195 def format_pull_request_review_event(self, event, data):
194 return (textwrap.dedent(
196 return (textwrap.dedent(
195 '''
197 '''
196 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
198 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
197 ''').format(
199 ''').format(
198 user=data['actor']['username'],
200 user=data['actor']['username'],
199 number=data['pullrequest']['pull_request_id'],
201 number=data['pullrequest']['pull_request_id'],
200 pr_url=data['pullrequest']['url'],
202 pr_url=data['pullrequest']['url'],
201 pr_status=data['pullrequest']['status'],
203 pr_status=data['pullrequest']['status'],
202 pr_title=h.html_escape(data['pullrequest']['title']),
204 pr_title=h.html_escape(data['pullrequest']['title']),
203 )
205 )
204 )
206 )
205
207
206 def format_pull_request_event(self, event, data):
208 def format_pull_request_event(self, event, data):
207 action = {
209 action = {
208 events.PullRequestCloseEvent: 'closed',
210 events.PullRequestCloseEvent: 'closed',
209 events.PullRequestMergeEvent: 'merged',
211 events.PullRequestMergeEvent: 'merged',
210 events.PullRequestUpdateEvent: 'updated',
212 events.PullRequestUpdateEvent: 'updated',
211 events.PullRequestCreateEvent: 'created',
213 events.PullRequestCreateEvent: 'created',
212 }.get(event.__class__, str(event.__class__))
214 }.get(event.__class__, str(event.__class__))
213
215
214 return ('Pull request <a href="{url}">#{number}</a> - {title} '
216 return ('Pull request <a href="{url}">#{number}</a> - {title} '
215 '{action} by <b>{user}</b>').format(
217 '{action} by <b>{user}</b>').format(
216 user=data['actor']['username'],
218 user=data['actor']['username'],
217 number=data['pullrequest']['pull_request_id'],
219 number=data['pullrequest']['pull_request_id'],
218 url=data['pullrequest']['url'],
220 url=data['pullrequest']['url'],
219 title=h.html_escape(data['pullrequest']['title']),
221 title=h.html_escape(data['pullrequest']['title']),
220 action=action
222 action=action
221 )
223 )
222
224
223 def format_repo_push_event(self, data):
225 def format_repo_push_event(self, data):
224 branches_commits = self.aggregate_branch_data(
226 branches_commits = self.aggregate_branch_data(
225 data['push']['branches'], data['push']['commits'])
227 data['push']['branches'], data['push']['commits'])
226
228
227 result = render_with_traceback(
229 result = render_with_traceback(
228 REPO_PUSH_TEMPLATE,
230 REPO_PUSH_TEMPLATE,
229 data=data,
231 data=data,
230 branches_commits=branches_commits,
232 branches_commits=branches_commits,
231 )
233 )
232 return result
234 return result
233
235
234 def format_repo_create_event(self, data):
236 def format_repo_create_event(self, data):
235 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
237 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
236 data['repo']['url'],
238 data['repo']['url'],
237 h.html_escape(data['repo']['repo_name']),
239 h.html_escape(data['repo']['repo_name']),
238 data['repo']['repo_type'],
240 data['repo']['repo_type'],
239 data['actor']['username'],
241 data['actor']['username'],
240 )
242 )
241
243
242
244
243 @async_task(ignore_result=True, base=RequestContextTask)
245 @async_task(ignore_result=True, base=RequestContextTask)
244 def post_text_to_hipchat(settings, text):
246 def post_text_to_hipchat(settings, text):
245 log.debug('sending %s to hipchat %s', text, settings['server_url'])
247 log.debug('sending %s to hipchat %s', text, settings['server_url'])
246 json_message = {
248 json_message = {
247 "message": text,
249 "message": text,
248 "color": settings.get('color', 'yellow'),
250 "color": settings.get('color', 'yellow'),
249 "notify": settings.get('notify', False),
251 "notify": settings.get('notify', False),
250 }
252 }
251 req_session = requests_retry_call()
253 req_session = requests_retry_call()
252 resp = req_session.post(settings['server_url'], json=json_message, timeout=60)
254 resp = req_session.post(settings['server_url'], json=json_message, timeout=60)
253 resp.raise_for_status() # raise exception on a failed request
255 resp.raise_for_status() # raise exception on a failed request
@@ -1,351 +1,353 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 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):
44 def html_to_slack_links(message):
45 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
45 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
46 r'<\1|\2>', message)
46 r'<\1|\2>', message)
47
47
48
48
49 REPO_PUSH_TEMPLATE = Template('''
49 REPO_PUSH_TEMPLATE = Template('''
50 <%
50 <%
51 def branch_text(branch):
51 def branch_text(branch):
52 if branch:
52 if branch:
53 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
53 return 'on branch: <{}|{}>'.format(branch_commits['branch']['url'], branch_commits['branch']['name'])
54 else:
54 else:
55 ## case for SVN no branch push...
55 ## case for SVN no branch push...
56 return 'to trunk'
56 return 'to trunk'
57 %> \
57 %> \
58
58
59 % for branch, branch_commits in branches_commits.items():
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)}
60 ${len(branch_commits['commits'])} ${'commit' if len(branch_commits['commits']) == 1 else 'commits'} ${branch_text(branch)}
61 % for commit in branch_commits['commits']:
61 % for commit in branch_commits['commits']:
62 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
62 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html']|html_to_slack_links}
63 % endfor
63 % endfor
64 % endfor
64 % endfor
65 ''')
65 ''')
66
66
67
67
68 class SlackSettingsSchema(colander.Schema):
68 class SlackSettingsSchema(colander.Schema):
69 service = colander.SchemaNode(
69 service = colander.SchemaNode(
70 colander.String(),
70 colander.String(),
71 title=_('Slack service URL'),
71 title=_('Slack service URL'),
72 description=h.literal(_(
72 description=h.literal(_(
73 'This can be setup at the '
73 'This can be setup at the '
74 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
74 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
75 'slack app manager</a>')),
75 'slack app manager</a>')),
76 default='',
76 default='',
77 preparer=strip_whitespace,
77 preparer=strip_whitespace,
78 validator=colander.url,
78 validator=colander.url,
79 widget=deform.widget.TextInputWidget(
79 widget=deform.widget.TextInputWidget(
80 placeholder='https://hooks.slack.com/services/...',
80 placeholder='https://hooks.slack.com/services/...',
81 ),
81 ),
82 )
82 )
83 username = colander.SchemaNode(
83 username = colander.SchemaNode(
84 colander.String(),
84 colander.String(),
85 title=_('Username'),
85 title=_('Username'),
86 description=_('Username to show notifications coming from.'),
86 description=_('Username to show notifications coming from.'),
87 missing='Rhodecode',
87 missing='Rhodecode',
88 preparer=strip_whitespace,
88 preparer=strip_whitespace,
89 widget=deform.widget.TextInputWidget(
89 widget=deform.widget.TextInputWidget(
90 placeholder='Rhodecode'
90 placeholder='Rhodecode'
91 ),
91 ),
92 )
92 )
93 channel = colander.SchemaNode(
93 channel = colander.SchemaNode(
94 colander.String(),
94 colander.String(),
95 title=_('Channel'),
95 title=_('Channel'),
96 description=_('Channel to send notifications to.'),
96 description=_('Channel to send notifications to.'),
97 missing='',
97 missing='',
98 preparer=strip_whitespace,
98 preparer=strip_whitespace,
99 widget=deform.widget.TextInputWidget(
99 widget=deform.widget.TextInputWidget(
100 placeholder='#general'
100 placeholder='#general'
101 ),
101 ),
102 )
102 )
103 icon_emoji = colander.SchemaNode(
103 icon_emoji = colander.SchemaNode(
104 colander.String(),
104 colander.String(),
105 title=_('Emoji'),
105 title=_('Emoji'),
106 description=_('Emoji to use eg. :studio_microphone:'),
106 description=_('Emoji to use eg. :studio_microphone:'),
107 missing='',
107 missing='',
108 preparer=strip_whitespace,
108 preparer=strip_whitespace,
109 widget=deform.widget.TextInputWidget(
109 widget=deform.widget.TextInputWidget(
110 placeholder=':studio_microphone:'
110 placeholder=':studio_microphone:'
111 ),
111 ),
112 )
112 )
113
113
114
114
115 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
115 class SlackIntegrationType(IntegrationTypeBase, CommitParsingDataHandler):
116 key = 'slack'
116 key = 'slack'
117 display_name = _('Slack')
117 display_name = _('Slack')
118 description = _('Send events such as repo pushes and pull requests to '
118 description = _('Send events such as repo pushes and pull requests to '
119 'your slack channel.')
119 'your slack channel.')
120
120
121 @classmethod
121 @classmethod
122 def icon(cls):
122 def icon(cls):
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>'''
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>'''
124
124
125 valid_events = [
125 valid_events = [
126 events.PullRequestCloseEvent,
126 events.PullRequestCloseEvent,
127 events.PullRequestMergeEvent,
127 events.PullRequestMergeEvent,
128 events.PullRequestUpdateEvent,
128 events.PullRequestUpdateEvent,
129 events.PullRequestCommentEvent,
129 events.PullRequestCommentEvent,
130 events.PullRequestReviewEvent,
130 events.PullRequestReviewEvent,
131 events.PullRequestCreateEvent,
131 events.PullRequestCreateEvent,
132 events.RepoPushEvent,
132 events.RepoPushEvent,
133 events.RepoCreateEvent,
133 events.RepoCreateEvent,
134 ]
134 ]
135
135
136 def send_event(self, event):
136 def send_event(self, event):
137 if event.__class__ not in self.valid_events:
137 if event.__class__ not in self.valid_events:
138 log.debug('event not valid: %r', event)
138 log.debug('event not valid: %r', event)
139 return
139 return
140
140
141 if event.name not in self.settings['events']:
141 allowed_events = self.settings['events']
142 log.debug('event ignored: %r', event)
142 if event.name not in allowed_events:
143 log.debug('event ignored: %r event %s not in allowed events %s',
144 event, event.name, allowed_events)
143 return
145 return
144
146
145 data = event.as_dict()
147 data = event.as_dict()
146
148
147 # defaults
149 # defaults
148 title = '*%s* caused a *%s* event' % (
150 title = '*%s* caused a *%s* event' % (
149 data['actor']['username'], event.name)
151 data['actor']['username'], event.name)
150 text = '*%s* caused a *%s* event' % (
152 text = '*%s* caused a *%s* event' % (
151 data['actor']['username'], event.name)
153 data['actor']['username'], event.name)
152 fields = None
154 fields = None
153 overrides = None
155 overrides = None
154
156
155 log.debug('handling slack event for %s', event.name)
157 log.debug('handling slack event for %s', event.name)
156
158
157 if isinstance(event, events.PullRequestCommentEvent):
159 if isinstance(event, events.PullRequestCommentEvent):
158 (title, text, fields, overrides) \
160 (title, text, fields, overrides) \
159 = self.format_pull_request_comment_event(event, data)
161 = self.format_pull_request_comment_event(event, data)
160 elif isinstance(event, events.PullRequestReviewEvent):
162 elif isinstance(event, events.PullRequestReviewEvent):
161 title, text = self.format_pull_request_review_event(event, data)
163 title, text = self.format_pull_request_review_event(event, data)
162 elif isinstance(event, events.PullRequestEvent):
164 elif isinstance(event, events.PullRequestEvent):
163 title, text = self.format_pull_request_event(event, data)
165 title, text = self.format_pull_request_event(event, data)
164 elif isinstance(event, events.RepoPushEvent):
166 elif isinstance(event, events.RepoPushEvent):
165 title, text = self.format_repo_push_event(data)
167 title, text = self.format_repo_push_event(data)
166 elif isinstance(event, events.RepoCreateEvent):
168 elif isinstance(event, events.RepoCreateEvent):
167 title, text = self.format_repo_create_event(data)
169 title, text = self.format_repo_create_event(data)
168 else:
170 else:
169 log.error('unhandled event type: %r', event)
171 log.error('unhandled event type: %r', event)
170
172
171 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
173 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
172
174
173 def settings_schema(self):
175 def settings_schema(self):
174 schema = SlackSettingsSchema()
176 schema = SlackSettingsSchema()
175 schema.add(colander.SchemaNode(
177 schema.add(colander.SchemaNode(
176 colander.Set(),
178 colander.Set(),
177 widget=deform.widget.CheckboxChoiceWidget(
179 widget=deform.widget.CheckboxChoiceWidget(
178 values=sorted(
180 values=sorted(
179 [(e.name, e.display_name) for e in self.valid_events]
181 [(e.name, e.display_name) for e in self.valid_events]
180 )
182 )
181 ),
183 ),
182 description="Events activated for this integration",
184 description="Events activated for this integration",
183 name='events'
185 name='events'
184 ))
186 ))
185
187
186 return schema
188 return schema
187
189
188 def format_pull_request_comment_event(self, event, data):
190 def format_pull_request_comment_event(self, event, data):
189 comment_text = data['comment']['text']
191 comment_text = data['comment']['text']
190 if len(comment_text) > 200:
192 if len(comment_text) > 200:
191 comment_text = '<{comment_url}|{comment_text}...>'.format(
193 comment_text = '<{comment_url}|{comment_text}...>'.format(
192 comment_text=comment_text[:200],
194 comment_text=comment_text[:200],
193 comment_url=data['comment']['url'],
195 comment_url=data['comment']['url'],
194 )
196 )
195
197
196 fields = None
198 fields = None
197 overrides = None
199 overrides = None
198 status_text = None
200 status_text = None
199
201
200 if data['comment']['status']:
202 if data['comment']['status']:
201 status_color = {
203 status_color = {
202 'approved': '#0ac878',
204 'approved': '#0ac878',
203 'rejected': '#e85e4d'}.get(data['comment']['status'])
205 'rejected': '#e85e4d'}.get(data['comment']['status'])
204
206
205 if status_color:
207 if status_color:
206 overrides = {"color": status_color}
208 overrides = {"color": status_color}
207
209
208 status_text = data['comment']['status']
210 status_text = data['comment']['status']
209
211
210 if data['comment']['file']:
212 if data['comment']['file']:
211 fields = [
213 fields = [
212 {
214 {
213 "title": "file",
215 "title": "file",
214 "value": data['comment']['file']
216 "value": data['comment']['file']
215 },
217 },
216 {
218 {
217 "title": "line",
219 "title": "line",
218 "value": data['comment']['line']
220 "value": data['comment']['line']
219 }
221 }
220 ]
222 ]
221
223
222 template = Template(textwrap.dedent(r'''
224 template = Template(textwrap.dedent(r'''
223 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
225 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
224 '''))
226 '''))
225 title = render_with_traceback(
227 title = render_with_traceback(
226 template, data=data, comment=event.comment)
228 template, data=data, comment=event.comment)
227
229
228 template = Template(textwrap.dedent(r'''
230 template = Template(textwrap.dedent(r'''
229 *pull request title*: ${pr_title}
231 *pull request title*: ${pr_title}
230 % if status_text:
232 % if status_text:
231 *submitted status*: `${status_text}`
233 *submitted status*: `${status_text}`
232 % endif
234 % endif
233 >>> ${comment_text}
235 >>> ${comment_text}
234 '''))
236 '''))
235 text = render_with_traceback(
237 text = render_with_traceback(
236 template,
238 template,
237 comment_text=comment_text,
239 comment_text=comment_text,
238 pr_title=data['pullrequest']['title'],
240 pr_title=data['pullrequest']['title'],
239 status_text=status_text)
241 status_text=status_text)
240
242
241 return title, text, fields, overrides
243 return title, text, fields, overrides
242
244
243 def format_pull_request_review_event(self, event, data):
245 def format_pull_request_review_event(self, event, data):
244 template = Template(textwrap.dedent(r'''
246 template = Template(textwrap.dedent(r'''
245 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
247 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
246 '''))
248 '''))
247 title = render_with_traceback(template, data=data)
249 title = render_with_traceback(template, data=data)
248
250
249 template = Template(textwrap.dedent(r'''
251 template = Template(textwrap.dedent(r'''
250 *pull request title*: ${pr_title}
252 *pull request title*: ${pr_title}
251 '''))
253 '''))
252 text = render_with_traceback(
254 text = render_with_traceback(
253 template,
255 template,
254 pr_title=data['pullrequest']['title'])
256 pr_title=data['pullrequest']['title'])
255
257
256 return title, text
258 return title, text
257
259
258 def format_pull_request_event(self, event, data):
260 def format_pull_request_event(self, event, data):
259 action = {
261 action = {
260 events.PullRequestCloseEvent: 'closed',
262 events.PullRequestCloseEvent: 'closed',
261 events.PullRequestMergeEvent: 'merged',
263 events.PullRequestMergeEvent: 'merged',
262 events.PullRequestUpdateEvent: 'updated',
264 events.PullRequestUpdateEvent: 'updated',
263 events.PullRequestCreateEvent: 'created',
265 events.PullRequestCreateEvent: 'created',
264 }.get(event.__class__, str(event.__class__))
266 }.get(event.__class__, str(event.__class__))
265
267
266 template = Template(textwrap.dedent(r'''
268 template = Template(textwrap.dedent(r'''
267 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
269 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
268 '''))
270 '''))
269 title = render_with_traceback(template, data=data, action=action)
271 title = render_with_traceback(template, data=data, action=action)
270
272
271 template = Template(textwrap.dedent(r'''
273 template = Template(textwrap.dedent(r'''
272 *pull request title*: ${pr_title}
274 *pull request title*: ${pr_title}
273 %if data['pullrequest']['commits']:
275 %if data['pullrequest']['commits']:
274 *commits*: ${len(data['pullrequest']['commits'])}
276 *commits*: ${len(data['pullrequest']['commits'])}
275 %endif
277 %endif
276 '''))
278 '''))
277 text = render_with_traceback(
279 text = render_with_traceback(
278 template,
280 template,
279 pr_title=data['pullrequest']['title'],
281 pr_title=data['pullrequest']['title'],
280 data=data)
282 data=data)
281
283
282 return title, text
284 return title, text
283
285
284 def format_repo_push_event(self, data):
286 def format_repo_push_event(self, data):
285 branches_commits = self.aggregate_branch_data(
287 branches_commits = self.aggregate_branch_data(
286 data['push']['branches'], data['push']['commits'])
288 data['push']['branches'], data['push']['commits'])
287
289
288 template = Template(r'''
290 template = Template(r'''
289 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
291 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
290 ''')
292 ''')
291 title = render_with_traceback(template, data=data)
293 title = render_with_traceback(template, data=data)
292
294
293 text = render_with_traceback(
295 text = render_with_traceback(
294 REPO_PUSH_TEMPLATE,
296 REPO_PUSH_TEMPLATE,
295 data=data,
297 data=data,
296 branches_commits=branches_commits,
298 branches_commits=branches_commits,
297 html_to_slack_links=html_to_slack_links,
299 html_to_slack_links=html_to_slack_links,
298 )
300 )
299
301
300 return title, text
302 return title, text
301
303
302 def format_repo_create_event(self, data):
304 def format_repo_create_event(self, data):
303 template = Template(r'''
305 template = Template(r'''
304 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
306 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
305 ''')
307 ''')
306 title = render_with_traceback(template, data=data)
308 title = render_with_traceback(template, data=data)
307
309
308 template = Template(textwrap.dedent(r'''
310 template = Template(textwrap.dedent(r'''
309 repo_url: ${data['repo']['url']}
311 repo_url: ${data['repo']['url']}
310 repo_type: ${data['repo']['repo_type']}
312 repo_type: ${data['repo']['repo_type']}
311 '''))
313 '''))
312 text = render_with_traceback(template, data=data)
314 text = render_with_traceback(template, data=data)
313
315
314 return title, text
316 return title, text
315
317
316
318
317 @async_task(ignore_result=True, base=RequestContextTask)
319 @async_task(ignore_result=True, base=RequestContextTask)
318 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
320 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
319 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
321 log.debug('sending %s (%s) to slack %s', title, text, settings['service'])
320
322
321 fields = fields or []
323 fields = fields or []
322 overrides = overrides or {}
324 overrides = overrides or {}
323
325
324 message_data = {
326 message_data = {
325 "fallback": text,
327 "fallback": text,
326 "color": "#427cc9",
328 "color": "#427cc9",
327 "pretext": title,
329 "pretext": title,
328 #"author_name": "Bobby Tables",
330 #"author_name": "Bobby Tables",
329 #"author_link": "http://flickr.com/bobby/",
331 #"author_link": "http://flickr.com/bobby/",
330 #"author_icon": "http://flickr.com/icons/bobby.jpg",
332 #"author_icon": "http://flickr.com/icons/bobby.jpg",
331 #"title": "Slack API Documentation",
333 #"title": "Slack API Documentation",
332 #"title_link": "https://api.slack.com/",
334 #"title_link": "https://api.slack.com/",
333 "text": text,
335 "text": text,
334 "fields": fields,
336 "fields": fields,
335 #"image_url": "http://my-website.com/path/to/image.jpg",
337 #"image_url": "http://my-website.com/path/to/image.jpg",
336 #"thumb_url": "http://example.com/path/to/thumb.png",
338 #"thumb_url": "http://example.com/path/to/thumb.png",
337 "footer": "RhodeCode",
339 "footer": "RhodeCode",
338 #"footer_icon": "",
340 #"footer_icon": "",
339 "ts": time.time(),
341 "ts": time.time(),
340 "mrkdwn_in": ["pretext", "text"]
342 "mrkdwn_in": ["pretext", "text"]
341 }
343 }
342 message_data.update(overrides)
344 message_data.update(overrides)
343 json_message = {
345 json_message = {
344 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
346 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
345 "channel": settings.get('channel', ''),
347 "channel": settings.get('channel', ''),
346 "username": settings.get('username', 'Rhodecode'),
348 "username": settings.get('username', 'Rhodecode'),
347 "attachments": [message_data]
349 "attachments": [message_data]
348 }
350 }
349 req_session = requests_retry_call()
351 req_session = requests_retry_call()
350 resp = req_session.post(settings['service'], json=json_message, timeout=60)
352 resp = req_session.post(settings['service'], json=json_message, timeout=60)
351 resp.raise_for_status() # raise exception on a failed request
353 resp.raise_for_status() # raise exception on a failed request
@@ -1,263 +1,265 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 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
22
23 import deform
23 import deform
24 import deform.widget
24 import deform.widget
25 import logging
25 import logging
26 import colander
26 import colander
27
27
28 import rhodecode
28 import rhodecode
29 from rhodecode import events
29 from rhodecode import events
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.integrations.types.base import (
31 from rhodecode.integrations.types.base import (
32 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
32 IntegrationTypeBase, get_auth, get_web_token, get_url_vars,
33 WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
33 WebhookDataHandler, WEBHOOK_URL_VARS, requests_retry_call)
34 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
34 from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask
35 from rhodecode.model.validation_schema import widgets
35 from rhodecode.model.validation_schema import widgets
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 # updating this required to update the `common_vars` passed in url calling func
40 # updating this required to update the `common_vars` passed in url calling func
41
41
42 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
42 URL_VARS = get_url_vars(WEBHOOK_URL_VARS)
43
43
44
44
45 class WebhookSettingsSchema(colander.Schema):
45 class WebhookSettingsSchema(colander.Schema):
46 url = colander.SchemaNode(
46 url = colander.SchemaNode(
47 colander.String(),
47 colander.String(),
48 title=_('Webhook URL'),
48 title=_('Webhook URL'),
49 description=
49 description=
50 _('URL to which Webhook should submit data. If used some of the '
50 _('URL to which Webhook should submit data. If used some of the '
51 'variables would trigger multiple calls, like ${branch} or '
51 'variables would trigger multiple calls, like ${branch} or '
52 '${commit_id}. Webhook will be called as many times as unique '
52 '${commit_id}. Webhook will be called as many times as unique '
53 'objects in data in such cases.'),
53 'objects in data in such cases.'),
54 missing=colander.required,
54 missing=colander.required,
55 required=True,
55 required=True,
56 validator=colander.url,
56 validator=colander.url,
57 widget=widgets.CodeMirrorWidget(
57 widget=widgets.CodeMirrorWidget(
58 help_block_collapsable_name='Show url variables',
58 help_block_collapsable_name='Show url variables',
59 help_block_collapsable=(
59 help_block_collapsable=(
60 'E.g http://my-serv/trigger_job/${{event_name}}'
60 'E.g http://my-serv/trigger_job/${{event_name}}'
61 '?PR_ID=${{pull_request_id}}'
61 '?PR_ID=${{pull_request_id}}'
62 '\nFull list of vars:\n{}'.format(URL_VARS)),
62 '\nFull list of vars:\n{}'.format(URL_VARS)),
63 codemirror_mode='text',
63 codemirror_mode='text',
64 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
64 codemirror_options='{"lineNumbers": false, "lineWrapping": true}'),
65 )
65 )
66 secret_token = colander.SchemaNode(
66 secret_token = colander.SchemaNode(
67 colander.String(),
67 colander.String(),
68 title=_('Secret Token'),
68 title=_('Secret Token'),
69 description=_('Optional string used to validate received payloads. '
69 description=_('Optional string used to validate received payloads. '
70 'It will be sent together with event data in JSON'),
70 'It will be sent together with event data in JSON'),
71 default='',
71 default='',
72 missing='',
72 missing='',
73 widget=deform.widget.TextInputWidget(
73 widget=deform.widget.TextInputWidget(
74 placeholder='e.g. secret_token'
74 placeholder='e.g. secret_token'
75 ),
75 ),
76 )
76 )
77 username = colander.SchemaNode(
77 username = colander.SchemaNode(
78 colander.String(),
78 colander.String(),
79 title=_('Username'),
79 title=_('Username'),
80 description=_('Optional username to authenticate the call.'),
80 description=_('Optional username to authenticate the call.'),
81 default='',
81 default='',
82 missing='',
82 missing='',
83 widget=deform.widget.TextInputWidget(
83 widget=deform.widget.TextInputWidget(
84 placeholder='e.g. admin'
84 placeholder='e.g. admin'
85 ),
85 ),
86 )
86 )
87 password = colander.SchemaNode(
87 password = colander.SchemaNode(
88 colander.String(),
88 colander.String(),
89 title=_('Password'),
89 title=_('Password'),
90 description=_('Optional password to authenticate the call.'),
90 description=_('Optional password to authenticate the call.'),
91 default='',
91 default='',
92 missing='',
92 missing='',
93 widget=deform.widget.PasswordWidget(
93 widget=deform.widget.PasswordWidget(
94 placeholder='e.g. secret.',
94 placeholder='e.g. secret.',
95 redisplay=True,
95 redisplay=True,
96 ),
96 ),
97 )
97 )
98 custom_header_key = colander.SchemaNode(
98 custom_header_key = colander.SchemaNode(
99 colander.String(),
99 colander.String(),
100 title=_('Custom Header Key'),
100 title=_('Custom Header Key'),
101 description=_('Custom Header name to be set when calling endpoint.'),
101 description=_('Custom Header name to be set when calling endpoint.'),
102 default='',
102 default='',
103 missing='',
103 missing='',
104 widget=deform.widget.TextInputWidget(
104 widget=deform.widget.TextInputWidget(
105 placeholder='e.g: Authorization'
105 placeholder='e.g: Authorization'
106 ),
106 ),
107 )
107 )
108 custom_header_val = colander.SchemaNode(
108 custom_header_val = colander.SchemaNode(
109 colander.String(),
109 colander.String(),
110 title=_('Custom Header Value'),
110 title=_('Custom Header Value'),
111 description=_('Custom Header value to be set when calling endpoint.'),
111 description=_('Custom Header value to be set when calling endpoint.'),
112 default='',
112 default='',
113 missing='',
113 missing='',
114 widget=deform.widget.TextInputWidget(
114 widget=deform.widget.TextInputWidget(
115 placeholder='e.g. Basic XxXxXx'
115 placeholder='e.g. Basic XxXxXx'
116 ),
116 ),
117 )
117 )
118 method_type = colander.SchemaNode(
118 method_type = colander.SchemaNode(
119 colander.String(),
119 colander.String(),
120 title=_('Call Method'),
120 title=_('Call Method'),
121 description=_('Select a HTTP method to use when calling the Webhook.'),
121 description=_('Select a HTTP method to use when calling the Webhook.'),
122 default='post',
122 default='post',
123 missing='',
123 missing='',
124 widget=deform.widget.RadioChoiceWidget(
124 widget=deform.widget.RadioChoiceWidget(
125 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
125 values=[('get', 'GET'), ('post', 'POST'), ('put', 'PUT')],
126 inline=True
126 inline=True
127 ),
127 ),
128 )
128 )
129
129
130
130
131 class WebhookIntegrationType(IntegrationTypeBase):
131 class WebhookIntegrationType(IntegrationTypeBase):
132 key = 'webhook'
132 key = 'webhook'
133 display_name = _('Webhook')
133 display_name = _('Webhook')
134 description = _('send JSON data to a url endpoint')
134 description = _('send JSON data to a url endpoint')
135
135
136 @classmethod
136 @classmethod
137 def icon(cls):
137 def icon(cls):
138 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
138 return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
139
139
140 valid_events = [
140 valid_events = [
141 events.PullRequestCloseEvent,
141 events.PullRequestCloseEvent,
142 events.PullRequestMergeEvent,
142 events.PullRequestMergeEvent,
143 events.PullRequestUpdateEvent,
143 events.PullRequestUpdateEvent,
144 events.PullRequestCommentEvent,
144 events.PullRequestCommentEvent,
145 events.PullRequestReviewEvent,
145 events.PullRequestReviewEvent,
146 events.PullRequestCreateEvent,
146 events.PullRequestCreateEvent,
147 events.RepoPushEvent,
147 events.RepoPushEvent,
148 events.RepoCreateEvent,
148 events.RepoCreateEvent,
149 ]
149 ]
150
150
151 def settings_schema(self):
151 def settings_schema(self):
152 schema = WebhookSettingsSchema()
152 schema = WebhookSettingsSchema()
153 schema.add(colander.SchemaNode(
153 schema.add(colander.SchemaNode(
154 colander.Set(),
154 colander.Set(),
155 widget=deform.widget.CheckboxChoiceWidget(
155 widget=deform.widget.CheckboxChoiceWidget(
156 values=sorted(
156 values=sorted(
157 [(e.name, e.display_name) for e in self.valid_events]
157 [(e.name, e.display_name) for e in self.valid_events]
158 )
158 )
159 ),
159 ),
160 description="Events activated for this integration",
160 description="Events activated for this integration",
161 name='events'
161 name='events'
162 ))
162 ))
163 return schema
163 return schema
164
164
165 def send_event(self, event):
165 def send_event(self, event):
166 log.debug(
166 log.debug(
167 'handling event %s with Webhook integration %s', event.name, self)
167 'handling event %s with Webhook integration %s', event.name, self)
168
168
169 if event.__class__ not in self.valid_events:
169 if event.__class__ not in self.valid_events:
170 log.debug('event not valid: %r', event)
170 log.debug('event not valid: %r', event)
171 return
171 return
172
172
173 if event.name not in self.settings['events']:
173 allowed_events = self.settings['events']
174 log.debug('event ignored: %r', event)
174 if event.name not in allowed_events:
175 log.debug('event ignored: %r event %s not in allowed events %s',
176 event, event.name, allowed_events)
175 return
177 return
176
178
177 data = event.as_dict()
179 data = event.as_dict()
178 template_url = self.settings['url']
180 template_url = self.settings['url']
179
181
180 headers = {}
182 headers = {}
181 head_key = self.settings.get('custom_header_key')
183 head_key = self.settings.get('custom_header_key')
182 head_val = self.settings.get('custom_header_val')
184 head_val = self.settings.get('custom_header_val')
183 if head_key and head_val:
185 if head_key and head_val:
184 headers = {head_key: head_val}
186 headers = {head_key: head_val}
185
187
186 handler = WebhookDataHandler(template_url, headers)
188 handler = WebhookDataHandler(template_url, headers)
187
189
188 url_calls = handler(event, data)
190 url_calls = handler(event, data)
189 log.debug('webhook: calling following urls: %s', [x[0] for x in url_calls])
191 log.debug('webhook: calling following urls: %s', [x[0] for x in url_calls])
190
192
191 run_task(post_to_webhook, url_calls, self.settings)
193 run_task(post_to_webhook, url_calls, self.settings)
192
194
193
195
194 @async_task(ignore_result=True, base=RequestContextTask)
196 @async_task(ignore_result=True, base=RequestContextTask)
195 def post_to_webhook(url_calls, settings):
197 def post_to_webhook(url_calls, settings):
196 """
198 """
197 Example data::
199 Example data::
198
200
199 {'actor': {'user_id': 2, 'username': u'admin'},
201 {'actor': {'user_id': 2, 'username': u'admin'},
200 'actor_ip': u'192.168.157.1',
202 'actor_ip': u'192.168.157.1',
201 'name': 'repo-push',
203 'name': 'repo-push',
202 'push': {'branches': [{'name': u'default',
204 'push': {'branches': [{'name': u'default',
203 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
205 'url': 'http://rc.local:8080/hg-repo/changelog?branch=default'}],
204 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
206 'commits': [{'author': u'Marcin Kuzminski <marcin@rhodecode.com>',
205 'branch': u'default',
207 'branch': u'default',
206 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
208 'date': datetime.datetime(2017, 11, 30, 12, 59, 48),
207 'issues': [],
209 'issues': [],
208 'mentions': [],
210 'mentions': [],
209 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
211 'message': u'commit Thu 30 Nov 2017 13:59:48 CET',
210 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
212 'message_html': u'commit Thu 30 Nov 2017 13:59:48 CET',
211 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
213 'message_html_title': u'commit Thu 30 Nov 2017 13:59:48 CET',
212 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
214 'parents': [{'raw_id': '431b772a5353dad9974b810dd3707d79e3a7f6e0'}],
213 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
215 'permalink_url': u'http://rc.local:8080/_7/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
214 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
216 'raw_id': 'a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf',
215 'refs': {'bookmarks': [],
217 'refs': {'bookmarks': [],
216 'branches': [u'default'],
218 'branches': [u'default'],
217 'tags': [u'tip']},
219 'tags': [u'tip']},
218 'reviewers': [],
220 'reviewers': [],
219 'revision': 9L,
221 'revision': 9L,
220 'short_id': 'a815cc738b96',
222 'short_id': 'a815cc738b96',
221 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
223 'url': u'http://rc.local:8080/hg-repo/changeset/a815cc738b9651eb5ffbcfb1ce6ccd7c701a5ddf'}],
222 'issues': {}},
224 'issues': {}},
223 'repo': {'extra_fields': '',
225 'repo': {'extra_fields': '',
224 'permalink_url': u'http://rc.local:8080/_7',
226 'permalink_url': u'http://rc.local:8080/_7',
225 'repo_id': 7,
227 'repo_id': 7,
226 'repo_name': u'hg-repo',
228 'repo_name': u'hg-repo',
227 'repo_type': u'hg',
229 'repo_type': u'hg',
228 'url': u'http://rc.local:8080/hg-repo'},
230 'url': u'http://rc.local:8080/hg-repo'},
229 'server_url': u'http://rc.local:8080',
231 'server_url': u'http://rc.local:8080',
230 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
232 'utc_timestamp': datetime.datetime(2017, 11, 30, 13, 0, 1, 569276)
231 }
233 }
232 """
234 """
233
235
234 call_headers = {
236 call_headers = {
235 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
237 'User-Agent': 'RhodeCode-webhook-caller/{}'.format(rhodecode.__version__)
236 } # updated below with custom ones, allows override
238 } # updated below with custom ones, allows override
237
239
238 auth = get_auth(settings)
240 auth = get_auth(settings)
239 token = get_web_token(settings)
241 token = get_web_token(settings)
240
242
241 for url, headers, data in url_calls:
243 for url, headers, data in url_calls:
242 req_session = requests_retry_call()
244 req_session = requests_retry_call()
243
245
244 method = settings.get('method_type') or 'post'
246 method = settings.get('method_type') or 'post'
245 call_method = getattr(req_session, method)
247 call_method = getattr(req_session, method)
246
248
247 headers = headers or {}
249 headers = headers or {}
248 call_headers.update(headers)
250 call_headers.update(headers)
249
251
250 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
252 log.debug('calling Webhook with method: %s, and auth:%s', call_method, auth)
251 if settings.get('log_data'):
253 if settings.get('log_data'):
252 log.debug('calling webhook with data: %s', data)
254 log.debug('calling webhook with data: %s', data)
253 resp = call_method(url, json={
255 resp = call_method(url, json={
254 'token': token,
256 'token': token,
255 'event': data
257 'event': data
256 }, headers=call_headers, auth=auth, timeout=60)
258 }, headers=call_headers, auth=auth, timeout=60)
257 log.debug('Got Webhook response: %s', resp)
259 log.debug('Got Webhook response: %s', resp)
258
260
259 try:
261 try:
260 resp.raise_for_status() # raise exception on a failed request
262 resp.raise_for_status() # raise exception on a failed request
261 except Exception:
263 except Exception:
262 log.error(resp.text)
264 log.error(resp.text)
263 raise
265 raise
General Comments 0
You need to be logged in to leave comments. Login now