hipchat.py
251 lines
| 10.8 KiB
| text/x-python
|
PythonLexer
r5088 | # Copyright (C) 2012-2023 RhodeCode GmbH | |||
r550 | # | |||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
r4912 | ||||
r550 | import deform | |||
import logging | ||||
import requests | ||||
import colander | ||||
import textwrap | ||||
from mako.template import Template | ||||
from rhodecode import events | ||||
r4314 | from rhodecode.model.validation_schema.widgets import CheckboxChoiceWidgetDesc | |||
r731 | from rhodecode.translation import _ | |||
r550 | from rhodecode.lib import helpers as h | |||
r2359 | from rhodecode.lib.celerylib import run_task, async_task, RequestContextTask | |||
r550 | from rhodecode.lib.colander_utils import strip_whitespace | |||
r2644 | from rhodecode.integrations.types.base import ( | |||
r3110 | IntegrationTypeBase, CommitParsingDataHandler, render_with_traceback, | |||
requests_retry_call) | ||||
r550 | ||||
r647 | log = logging.getLogger(__name__) | |||
r550 | ||||
r3218 | REPO_PUSH_TEMPLATE = Template(''' | |||
<b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>: | ||||
<br> | ||||
<ul> | ||||
%for branch, branch_commits in branches_commits.items(): | ||||
<li> | ||||
% if branch: | ||||
<a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a> | ||||
% else: | ||||
to trunk | ||||
% endif | ||||
<ul> | ||||
% for commit in branch_commits['commits']: | ||||
<li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li> | ||||
% endfor | ||||
</ul> | ||||
</li> | ||||
%endfor | ||||
''') | ||||
r550 | ||||
r731 | class HipchatSettingsSchema(colander.Schema): | |||
r550 | color_choices = [ | |||
r731 | ('yellow', _('Yellow')), | |||
('red', _('Red')), | ||||
('green', _('Green')), | ||||
('purple', _('Purple')), | ||||
('gray', _('Gray')), | ||||
r550 | ] | |||
server_url = colander.SchemaNode( | ||||
colander.String(), | ||||
r731 | title=_('Hipchat server URL'), | |||
description=_('Hipchat integration url.'), | ||||
r550 | default='', | |||
preparer=strip_whitespace, | ||||
validator=colander.url, | ||||
widget=deform.widget.TextInputWidget( | ||||
placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?', | ||||
), | ||||
) | ||||
notify = colander.SchemaNode( | ||||
colander.Bool(), | ||||
r731 | title=_('Notify'), | |||
description=_('Make a notification to the users in room.'), | ||||
r550 | missing=False, | |||
default=False, | ||||
) | ||||
color = colander.SchemaNode( | ||||
colander.String(), | ||||
r731 | title=_('Color'), | |||
description=_('Background color of message.'), | ||||
r550 | missing='', | |||
validator=colander.OneOf([x[0] for x in color_choices]), | ||||
widget=deform.widget.Select2Widget( | ||||
values=color_choices, | ||||
), | ||||
) | ||||
r2644 | class HipchatIntegrationType(IntegrationTypeBase, CommitParsingDataHandler): | |||
r550 | key = 'hipchat' | |||
r731 | display_name = _('Hipchat') | |||
description = _('Send events such as repo pushes and pull requests to ' | ||||
'your hipchat channel.') | ||||
r2576 | ||||
@classmethod | ||||
def icon(cls): | ||||
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>''' | ||||
r550 | valid_events = [ | |||
events.PullRequestCloseEvent, | ||||
events.PullRequestMergeEvent, | ||||
events.PullRequestUpdateEvent, | ||||
events.PullRequestCommentEvent, | ||||
events.PullRequestReviewEvent, | ||||
events.PullRequestCreateEvent, | ||||
events.RepoPushEvent, | ||||
events.RepoCreateEvent, | ||||
] | ||||
def send_event(self, event): | ||||
if event.__class__ not in self.valid_events: | ||||
r4314 | log.debug('event %r not present in valid event list (%s)', event, self.valid_events) | |||
r550 | return | |||
r4314 | if not self.event_enabled(event): | |||
r550 | return | |||
data = event.as_dict() | ||||
r5095 | text = '<b>{}<b> caused a <b>{}</b> event'.format( | |||
r550 | data['actor']['username'], event.name) | |||
if isinstance(event, events.PullRequestCommentEvent): | ||||
text = self.format_pull_request_comment_event(event, data) | ||||
r4444 | elif isinstance(event, events.PullRequestCommentEditEvent): | |||
text = self.format_pull_request_comment_event(event, data) | ||||
r550 | elif isinstance(event, events.PullRequestReviewEvent): | |||
text = self.format_pull_request_review_event(event, data) | ||||
elif isinstance(event, events.PullRequestEvent): | ||||
text = self.format_pull_request_event(event, data) | ||||
elif isinstance(event, events.RepoPushEvent): | ||||
text = self.format_repo_push_event(data) | ||||
elif isinstance(event, events.RepoCreateEvent): | ||||
text = self.format_repo_create_event(data) | ||||
else: | ||||
r3061 | log.error('unhandled event type: %r', event) | |||
r550 | ||||
run_task(post_text_to_hipchat, self.settings, text) | ||||
def settings_schema(self): | ||||
schema = HipchatSettingsSchema() | ||||
schema.add(colander.SchemaNode( | ||||
colander.Set(), | ||||
r4314 | widget=CheckboxChoiceWidgetDesc( | |||
r550 | values=sorted( | |||
r4314 | [(e.name, e.display_name, e.description) for e in self.valid_events] | |||
), | ||||
r550 | ), | |||
r4314 | description="List of events activated for this integration", | |||
r550 | name='events' | |||
)) | ||||
return schema | ||||
def format_pull_request_comment_event(self, event, data): | ||||
comment_text = data['comment']['text'] | ||||
if len(comment_text) > 200: | ||||
comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format( | ||||
r934 | comment_text=h.html_escape(comment_text[:200]), | |||
r550 | comment_url=data['comment']['url'], | |||
) | ||||
comment_status = '' | ||||
if data['comment']['status']: | ||||
comment_status = '[{}]: '.format(data['comment']['status']) | ||||
return (textwrap.dedent( | ||||
''' | ||||
{user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}: | ||||
>>> {comment_status}{comment_text} | ||||
''').format( | ||||
comment_status=comment_status, | ||||
user=data['actor']['username'], | ||||
number=data['pullrequest']['pull_request_id'], | ||||
pr_url=data['pullrequest']['url'], | ||||
pr_status=data['pullrequest']['status'], | ||||
r934 | pr_title=h.html_escape(data['pullrequest']['title']), | |||
comment_text=h.html_escape(comment_text) | ||||
r550 | ) | |||
) | ||||
def format_pull_request_review_event(self, event, data): | ||||
return (textwrap.dedent( | ||||
''' | ||||
Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title} | ||||
''').format( | ||||
user=data['actor']['username'], | ||||
number=data['pullrequest']['pull_request_id'], | ||||
pr_url=data['pullrequest']['url'], | ||||
pr_status=data['pullrequest']['status'], | ||||
r934 | pr_title=h.html_escape(data['pullrequest']['title']), | |||
r550 | ) | |||
) | ||||
def format_pull_request_event(self, event, data): | ||||
action = { | ||||
events.PullRequestCloseEvent: 'closed', | ||||
events.PullRequestMergeEvent: 'merged', | ||||
events.PullRequestUpdateEvent: 'updated', | ||||
events.PullRequestCreateEvent: 'created', | ||||
}.get(event.__class__, str(event.__class__)) | ||||
return ('Pull request <a href="{url}">#{number}</a> - {title} ' | ||||
r937 | '{action} by <b>{user}</b>').format( | |||
r550 | user=data['actor']['username'], | |||
number=data['pullrequest']['pull_request_id'], | ||||
url=data['pullrequest']['url'], | ||||
r934 | title=h.html_escape(data['pullrequest']['title']), | |||
r550 | action=action | |||
) | ||||
def format_repo_push_event(self, data): | ||||
r2644 | branches_commits = self.aggregate_branch_data( | |||
data['push']['branches'], data['push']['commits']) | ||||
r776 | ||||
r2646 | result = render_with_traceback( | |||
r3218 | REPO_PUSH_TEMPLATE, | |||
r550 | data=data, | |||
r776 | branches_commits=branches_commits, | |||
r550 | ) | |||
return result | ||||
def format_repo_create_event(self, data): | ||||
return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format( | ||||
data['repo']['url'], | ||||
r934 | h.html_escape(data['repo']['repo_name']), | |||
r550 | data['repo']['repo_type'], | |||
data['actor']['username'], | ||||
) | ||||
r2359 | @async_task(ignore_result=True, base=RequestContextTask) | |||
r550 | def post_text_to_hipchat(settings, text): | |||
r3061 | log.debug('sending %s to hipchat %s', text, settings['server_url']) | |||
r2950 | json_message = { | |||
r550 | "message": text, | |||
"color": settings.get('color', 'yellow'), | ||||
"notify": settings.get('notify', False), | ||||
r2950 | } | |||
r3110 | req_session = requests_retry_call() | |||
resp = req_session.post(settings['server_url'], json=json_message, timeout=60) | ||||
r550 | resp.raise_for_status() # raise exception on a failed request | |||