##// END OF EJS Templates
slack: fix wrong named function
dan -
r417:cd14395c default
parent child Browse files
Show More
@@ -1,388 +1,389 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 """
22 22 Pylons middleware initialization
23 23 """
24 24 import logging
25 25
26 26 from paste.registry import RegistryManager
27 27 from paste.gzipper import make_gzip_middleware
28 28 from pylons.wsgiapp import PylonsApp
29 29 from pyramid.authorization import ACLAuthorizationPolicy
30 30 from pyramid.config import Configurator
31 31 from pyramid.static import static_view
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
35 35 import pyramid.httpexceptions as httpexceptions
36 36 from pyramid.renderers import render_to_response, render
37 37 from routes.middleware import RoutesMiddleware
38 38 import routes.util
39 39
40 40 import rhodecode
41 import rhodecode.integrations # do not remove this as it registers celery tasks
41 42 from rhodecode.config import patches
42 43 from rhodecode.config.environment import (
43 44 load_environment, load_pyramid_environment)
44 45 from rhodecode.lib.middleware import csrf
45 46 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
46 47 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
47 48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 49 from rhodecode.lib.middleware.vcs import VCSMiddleware
49 50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 51
51 52
52 53 log = logging.getLogger(__name__)
53 54
54 55
55 56 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
56 57 """Create a Pylons WSGI application and return it
57 58
58 59 ``global_conf``
59 60 The inherited configuration for this application. Normally from
60 61 the [DEFAULT] section of the Paste ini file.
61 62
62 63 ``full_stack``
63 64 Whether or not this application provides a full WSGI stack (by
64 65 default, meaning it handles its own exceptions and errors).
65 66 Disable full_stack when this application is "managed" by
66 67 another WSGI middleware.
67 68
68 69 ``app_conf``
69 70 The application's local configuration. Normally specified in
70 71 the [app:<name>] section of the Paste ini file (where <name>
71 72 defaults to main).
72 73
73 74 """
74 75 # Apply compatibility patches
75 76 patches.kombu_1_5_1_python_2_7_11()
76 77 patches.inspect_getargspec()
77 78
78 79 # Configure the Pylons environment
79 80 config = load_environment(global_conf, app_conf)
80 81
81 82 # The Pylons WSGI app
82 83 app = PylonsApp(config=config)
83 84 if rhodecode.is_test:
84 85 app = csrf.CSRFDetector(app)
85 86
86 87 expected_origin = config.get('expected_origin')
87 88 if expected_origin:
88 89 # The API can be accessed from other Origins.
89 90 app = csrf.OriginChecker(app, expected_origin,
90 91 skip_urls=[routes.util.url_for('api')])
91 92
92 93
93 94 if asbool(full_stack):
94 95
95 96 # Appenlight monitoring and error handler
96 97 app, appenlight_client = wrap_in_appenlight_if_enabled(app, config)
97 98
98 99 # we want our low level middleware to get to the request ASAP. We don't
99 100 # need any pylons stack middleware in them
100 101 app = VCSMiddleware(app, config, appenlight_client)
101 102
102 103 # Establish the Registry for this application
103 104 app = RegistryManager(app)
104 105
105 106 app.config = config
106 107
107 108 return app
108 109
109 110
110 111 def make_pyramid_app(global_config, **settings):
111 112 """
112 113 Constructs the WSGI application based on Pyramid and wraps the Pylons based
113 114 application.
114 115
115 116 Specials:
116 117
117 118 * We migrate from Pylons to Pyramid. While doing this, we keep both
118 119 frameworks functional. This involves moving some WSGI middlewares around
119 120 and providing access to some data internals, so that the old code is
120 121 still functional.
121 122
122 123 * The application can also be integrated like a plugin via the call to
123 124 `includeme`. This is accompanied with the other utility functions which
124 125 are called. Changing this should be done with great care to not break
125 126 cases when these fragments are assembled from another place.
126 127
127 128 """
128 129 # The edition string should be available in pylons too, so we add it here
129 130 # before copying the settings.
130 131 settings.setdefault('rhodecode.edition', 'Community Edition')
131 132
132 133 # As long as our Pylons application does expect "unprepared" settings, make
133 134 # sure that we keep an unmodified copy. This avoids unintentional change of
134 135 # behavior in the old application.
135 136 settings_pylons = settings.copy()
136 137
137 138 sanitize_settings_and_apply_defaults(settings)
138 139 config = Configurator(settings=settings)
139 140 add_pylons_compat_data(config.registry, global_config, settings_pylons)
140 141
141 142 load_pyramid_environment(global_config, settings)
142 143
143 144 includeme(config)
144 145 includeme_last(config)
145 146 pyramid_app = config.make_wsgi_app()
146 147 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
147 148 return pyramid_app
148 149
149 150
150 151 def add_pylons_compat_data(registry, global_config, settings):
151 152 """
152 153 Attach data to the registry to support the Pylons integration.
153 154 """
154 155 registry._pylons_compat_global_config = global_config
155 156 registry._pylons_compat_settings = settings
156 157
157 158
158 159 def webob_to_pyramid_http_response(webob_response):
159 160 ResponseClass = httpexceptions.status_map[webob_response.status_int]
160 161 pyramid_response = ResponseClass(webob_response.status)
161 162 pyramid_response.status = webob_response.status
162 163 pyramid_response.headers.update(webob_response.headers)
163 164 if pyramid_response.headers['content-type'] == 'text/html':
164 165 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
165 166 return pyramid_response
166 167
167 168
168 169 def error_handler(exception, request):
169 170 # TODO: dan: replace the old pylons error controller with this
170 171 from rhodecode.model.settings import SettingsModel
171 172 from rhodecode.lib.utils2 import AttributeDict
172 173
173 174 try:
174 175 rc_config = SettingsModel().get_all_settings()
175 176 except Exception:
176 177 log.exception('failed to fetch settings')
177 178 rc_config = {}
178 179
179 180 base_response = HTTPInternalServerError()
180 181 # prefer original exception for the response since it may have headers set
181 182 if isinstance(exception, HTTPError):
182 183 base_response = exception
183 184
184 185 c = AttributeDict()
185 186 c.error_message = base_response.status
186 187 c.error_explanation = base_response.explanation or str(base_response)
187 188 c.visual = AttributeDict()
188 189
189 190 c.visual.rhodecode_support_url = (
190 191 request.registry.settings.get('rhodecode_support_url') or
191 192 request.route_url('rhodecode_support')
192 193 )
193 194 c.redirect_time = 0
194 195 c.rhodecode_name = rc_config.get('rhodecode_title', '')
195 196 if not c.rhodecode_name:
196 197 c.rhodecode_name = 'Rhodecode'
197 198
198 199 response = render_to_response(
199 200 '/errors/error_document.html', {'c': c}, request=request,
200 201 response=base_response)
201 202
202 203 return response
203 204
204 205
205 206 def includeme(config):
206 207 settings = config.registry.settings
207 208
208 209 if asbool(settings.get('appenlight', 'false')):
209 210 config.include('appenlight_client.ext.pyramid_tween')
210 211
211 212 # Includes which are required. The application would fail without them.
212 213 config.include('pyramid_mako')
213 214 config.include('pyramid_beaker')
214 215 config.include('rhodecode.admin')
215 216 config.include('rhodecode.authentication')
216 217 config.include('rhodecode.integrations')
217 218 config.include('rhodecode.login')
218 219 config.include('rhodecode.tweens')
219 220 config.include('rhodecode.api')
220 221 config.add_route(
221 222 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
222 223
223 224 # Set the authorization policy.
224 225 authz_policy = ACLAuthorizationPolicy()
225 226 config.set_authorization_policy(authz_policy)
226 227
227 228 # Set the default renderer for HTML templates to mako.
228 229 config.add_mako_renderer('.html')
229 230
230 231 # plugin information
231 232 config.registry.rhodecode_plugins = {}
232 233
233 234 config.add_directive(
234 235 'register_rhodecode_plugin', register_rhodecode_plugin)
235 236 # include RhodeCode plugins
236 237 includes = aslist(settings.get('rhodecode.includes', []))
237 238 for inc in includes:
238 239 config.include(inc)
239 240
240 241 pylons_app = make_app(
241 242 config.registry._pylons_compat_global_config,
242 243 **config.registry._pylons_compat_settings)
243 244 config.registry._pylons_compat_config = pylons_app.config
244 245
245 246 pylons_app_as_view = wsgiapp(pylons_app)
246 247
247 248 # Protect from VCS Server error related pages when server is not available
248 249 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
249 250 if not vcs_server_enabled:
250 251 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
251 252
252 253
253 254 def pylons_app_with_error_handler(context, request):
254 255 """
255 256 Handle exceptions from rc pylons app:
256 257
257 258 - old webob type exceptions get converted to pyramid exceptions
258 259 - pyramid exceptions are passed to the error handler view
259 260 """
260 261 try:
261 262 response = pylons_app_as_view(context, request)
262 263 if 400 <= response.status_int <= 599: # webob type error responses
263 264 return error_handler(
264 265 webob_to_pyramid_http_response(response), request)
265 266 except HTTPError as e: # pyramid type exceptions
266 267 return error_handler(e, request)
267 268 except Exception:
268 269 if settings.get('debugtoolbar.enabled', False):
269 270 raise
270 271 return error_handler(HTTPInternalServerError(), request)
271 272 return response
272 273
273 274 # This is the glue which allows us to migrate in chunks. By registering the
274 275 # pylons based application as the "Not Found" view in Pyramid, we will
275 276 # fallback to the old application each time the new one does not yet know
276 277 # how to handle a request.
277 278 config.add_notfound_view(pylons_app_with_error_handler)
278 279
279 280 if settings.get('debugtoolbar.enabled', False):
280 281 # if toolbar, then only http type exceptions get caught and rendered
281 282 ExcClass = HTTPError
282 283 else:
283 284 # if no toolbar, then any exception gets caught and rendered
284 285 ExcClass = Exception
285 286 config.add_view(error_handler, context=ExcClass)
286 287
287 288
288 289 def includeme_last(config):
289 290 """
290 291 The static file catchall needs to be last in the view configuration.
291 292 """
292 293 settings = config.registry.settings
293 294
294 295 # Note: johbo: I would prefer to register a prefix for static files at some
295 296 # point, e.g. move them under '_static/'. This would fully avoid that we
296 297 # can have name clashes with a repository name. Imaging someone calling his
297 298 # repo "css" ;-) Also having an external web server to serve out the static
298 299 # files seems to be easier to set up if they have a common prefix.
299 300 #
300 301 # Example: config.add_static_view('_static', path='rhodecode:public')
301 302 #
302 303 # It might be an option to register both paths for a while and then migrate
303 304 # over to the new location.
304 305
305 306 # Serving static files with a catchall.
306 307 if settings['static_files']:
307 308 config.add_route('catchall_static', '/*subpath')
308 309 config.add_view(
309 310 static_view('rhodecode:public'), route_name='catchall_static')
310 311
311 312
312 313 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
313 314 """
314 315 Apply outer WSGI middlewares around the application.
315 316
316 317 Part of this has been moved up from the Pylons layer, so that the
317 318 data is also available if old Pylons code is hit through an already ported
318 319 view.
319 320 """
320 321 settings = config.registry.settings
321 322
322 323 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
323 324 pyramid_app = HttpsFixup(pyramid_app, settings)
324 325
325 326 # Add RoutesMiddleware to support the pylons compatibility tween during
326 327
327 328 # migration to pyramid.
328 329 pyramid_app = RoutesMiddleware(
329 330 pyramid_app, config.registry._pylons_compat_config['routes.map'])
330 331
331 332 if asbool(settings.get('appenlight', 'false')):
332 333 pyramid_app, _ = wrap_in_appenlight_if_enabled(
333 334 pyramid_app, config.registry._pylons_compat_config)
334 335
335 336 # TODO: johbo: Don't really see why we enable the gzip middleware when
336 337 # serving static files, might be something that should have its own setting
337 338 # as well?
338 339 if settings['static_files']:
339 340 pyramid_app = make_gzip_middleware(
340 341 pyramid_app, settings, compress_level=1)
341 342
342 343 return pyramid_app
343 344
344 345
345 346 def sanitize_settings_and_apply_defaults(settings):
346 347 """
347 348 Applies settings defaults and does all type conversion.
348 349
349 350 We would move all settings parsing and preparation into this place, so that
350 351 we have only one place left which deals with this part. The remaining parts
351 352 of the application would start to rely fully on well prepared settings.
352 353
353 354 This piece would later be split up per topic to avoid a big fat monster
354 355 function.
355 356 """
356 357
357 358 # Pyramid's mako renderer has to search in the templates folder so that the
358 359 # old templates still work. Ported and new templates are expected to use
359 360 # real asset specifications for the includes.
360 361 mako_directories = settings.setdefault('mako.directories', [
361 362 # Base templates of the original Pylons application
362 363 'rhodecode:templates',
363 364 ])
364 365 log.debug(
365 366 "Using the following Mako template directories: %s",
366 367 mako_directories)
367 368
368 369 # Default includes, possible to change as a user
369 370 pyramid_includes = settings.setdefault('pyramid.includes', [
370 371 'rhodecode.lib.middleware.request_wrapper',
371 372 ])
372 373 log.debug(
373 374 "Using the following pyramid.includes: %s",
374 375 pyramid_includes)
375 376
376 377 # TODO: johbo: Re-think this, usually the call to config.include
377 378 # should allow to pass in a prefix.
378 379 settings.setdefault('rhodecode.api.url', '/_admin/api')
379 380
380 381 _bool_setting(settings, 'vcs.server.enable', 'true')
381 382 _bool_setting(settings, 'static_files', 'true')
382 383 _bool_setting(settings, 'is_test', 'false')
383 384
384 385 return settings
385 386
386 387
387 388 def _bool_setting(settings, name, default):
388 389 settings[name] = asbool(settings.get(name, default))
@@ -1,202 +1,202 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 re
24 24 import logging
25 25 import requests
26 26 import colander
27 27 from celery.task import task
28 28 from mako.template import Template
29 29
30 30 from rhodecode import events
31 31 from rhodecode.translation import lazy_ugettext
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib.celerylib import run_task
34 34 from rhodecode.lib.colander_utils import strip_whitespace
35 35 from rhodecode.integrations.types.base import IntegrationTypeBase
36 36 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
37 37
38 38 log = logging.getLogger()
39 39
40 40
41 41 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
42 42 service = colander.SchemaNode(
43 43 colander.String(),
44 44 title=lazy_ugettext('Slack service URL'),
45 45 description=h.literal(lazy_ugettext(
46 46 'This can be setup at the '
47 47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
48 48 'slack app manager</a>')),
49 49 default='',
50 50 placeholder='https://hooks.slack.com/services/...',
51 51 preparer=strip_whitespace,
52 52 validator=colander.url,
53 53 widget='string'
54 54 )
55 55 username = colander.SchemaNode(
56 56 colander.String(),
57 57 title=lazy_ugettext('Username'),
58 58 description=lazy_ugettext('Username to show notifications coming from.'),
59 59 missing='Rhodecode',
60 60 preparer=strip_whitespace,
61 61 widget='string',
62 62 placeholder='Rhodecode'
63 63 )
64 64 channel = colander.SchemaNode(
65 65 colander.String(),
66 66 title=lazy_ugettext('Channel'),
67 67 description=lazy_ugettext('Channel to send notifications to.'),
68 68 missing='',
69 69 preparer=strip_whitespace,
70 70 widget='string',
71 71 placeholder='#general'
72 72 )
73 73 icon_emoji = colander.SchemaNode(
74 74 colander.String(),
75 75 title=lazy_ugettext('Emoji'),
76 76 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
77 77 missing='',
78 78 preparer=strip_whitespace,
79 79 widget='string',
80 80 placeholder=':studio_microphone:'
81 81 )
82 82
83 83
84 84 repo_push_template = Template(r'''
85 85 *${data['actor']['username']}* pushed to \
86 86 %if data['push']['branches']:
87 87 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
88 88 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
89 89 %else:
90 90 unknown branch \
91 91 %endif
92 92 in <${data['repo']['url']}|${data['repo']['repo_name']}>
93 93 >>>
94 94 %for commit in data['push']['commits']:
95 95 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
96 96 %endfor
97 97 ''')
98 98
99 99
100 100 class SlackIntegrationType(IntegrationTypeBase):
101 101 key = 'slack'
102 102 display_name = lazy_ugettext('Slack')
103 103 SettingsSchema = SlackSettingsSchema
104 104 valid_events = [
105 105 events.PullRequestCloseEvent,
106 106 events.PullRequestMergeEvent,
107 107 events.PullRequestUpdateEvent,
108 108 events.PullRequestReviewEvent,
109 109 events.PullRequestCreateEvent,
110 110 events.RepoPushEvent,
111 111 events.RepoCreateEvent,
112 112 ]
113 113
114 114 def send_event(self, event):
115 115 if event.__class__ not in self.valid_events:
116 116 log.debug('event not valid: %r' % event)
117 117 return
118 118
119 119 if event.name not in self.settings['events']:
120 120 log.debug('event ignored: %r' % event)
121 121 return
122 122
123 123 data = event.as_dict()
124 124
125 125 text = '*%s* caused a *%s* event' % (
126 126 data['actor']['username'], event.name)
127 127
128 128 log.debug('handling slack event for %s' % event.name)
129 129
130 130 if isinstance(event, events.PullRequestEvent):
131 131 text = self.format_pull_request_event(event, data)
132 132 elif isinstance(event, events.RepoPushEvent):
133 133 text = self.format_repo_push_event(data)
134 134 elif isinstance(event, events.RepoCreateEvent):
135 135 text = self.format_repo_create_event(data)
136 136 else:
137 137 log.error('unhandled event type: %r' % event)
138 138
139 139 run_task(post_text_to_slack, self.settings, text)
140 140
141 141 @classmethod
142 142 def settings_schema(cls):
143 143 schema = SlackSettingsSchema()
144 144 schema.add(colander.SchemaNode(
145 145 colander.Set(),
146 146 widget='checkbox_list',
147 147 choices=sorted([e.name for e in cls.valid_events]),
148 148 description="Events activated for this integration",
149 149 default=[e.name for e in cls.valid_events],
150 150 name='events'
151 151 ))
152 152 return schema
153 153
154 154 def format_pull_request_event(self, event, data):
155 155 action = {
156 156 events.PullRequestCloseEvent: 'closed',
157 157 events.PullRequestMergeEvent: 'merged',
158 158 events.PullRequestUpdateEvent: 'updated',
159 159 events.PullRequestReviewEvent: 'reviewed',
160 160 events.PullRequestCreateEvent: 'created',
161 161 }.get(event.__class__, '<unknown action>')
162 162
163 163 return ('Pull request <{url}|#{number}> ({title}) '
164 164 '{action} by {user}').format(
165 165 user=data['actor']['username'],
166 166 number=data['pullrequest']['pull_request_id'],
167 167 url=data['pullrequest']['url'],
168 168 title=data['pullrequest']['title'],
169 169 action=action
170 170 )
171 171
172 172 def format_repo_push_event(self, data):
173 173 result = repo_push_template.render(
174 174 data=data,
175 175 html_to_slack_links=html_to_slack_links,
176 176 )
177 177 return result
178 178
179 def format_repo_create_msg(self, data):
179 def format_repo_create_event(self, data):
180 180 return '<{}|{}> ({}) repository created by *{}*'.format(
181 181 data['repo']['url'],
182 182 data['repo']['repo_name'],
183 183 data['repo']['repo_type'],
184 184 data['actor']['username'],
185 185 )
186 186
187 187
188 188 def html_to_slack_links(message):
189 189 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
190 190 r'<\1|\2>', message)
191 191
192 192
193 193 @task(ignore_result=True)
194 194 def post_text_to_slack(settings, text):
195 195 log.debug('sending %s to slack %s' % (text, settings['service']))
196 196 resp = requests.post(settings['service'], json={
197 197 "channel": settings.get('channel', ''),
198 198 "username": settings.get('username', 'Rhodecode'),
199 199 "text": text,
200 200 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
201 201 })
202 202 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now