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