##// END OF EJS Templates
slack: updated slack integration to use the attachements for nicer formatting.
marcink -
r1467:bfe33223 default
parent child Browse files
Show More
@@ -1,134 +1,137 b''
1 1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 22 from rhodecode.events.repo import (
23 23 RepoEvent, _commits_as_dict, _issues_as_dict)
24 24
25 25 log = logging.getLogger(__name__)
26 26
27 27
28 28 class PullRequestEvent(RepoEvent):
29 29 """
30 30 Base class for pull request events.
31 31
32 32 :param pullrequest: a :class:`PullRequest` instance
33 33 """
34 34
35 35 def __init__(self, pullrequest):
36 36 super(PullRequestEvent, self).__init__(pullrequest.target_repo)
37 37 self.pullrequest = pullrequest
38 38
39 39 def as_dict(self):
40 40 from rhodecode.model.pull_request import PullRequestModel
41 41 data = super(PullRequestEvent, self).as_dict()
42 42
43 43 commits = _commits_as_dict(
44 44 commit_ids=self.pullrequest.revisions,
45 45 repos=[self.pullrequest.source_repo]
46 46 )
47 47 issues = _issues_as_dict(commits)
48 48
49 49 data.update({
50 50 'pullrequest': {
51 51 'title': self.pullrequest.title,
52 52 'issues': issues,
53 53 'pull_request_id': self.pullrequest.pull_request_id,
54 54 'url': PullRequestModel().get_url(self.pullrequest),
55 55 'status': self.pullrequest.calculated_review_status(),
56 56 'commits': commits,
57 57 }
58 58 })
59 59 return data
60 60
61 61
62 62 class PullRequestCreateEvent(PullRequestEvent):
63 63 """
64 64 An instance of this class is emitted as an :term:`event` after a pull
65 65 request is created.
66 66 """
67 67 name = 'pullrequest-create'
68 68 display_name = lazy_ugettext('pullrequest created')
69 69
70 70
71 71 class PullRequestCloseEvent(PullRequestEvent):
72 72 """
73 73 An instance of this class is emitted as an :term:`event` after a pull
74 74 request is closed.
75 75 """
76 76 name = 'pullrequest-close'
77 77 display_name = lazy_ugettext('pullrequest closed')
78 78
79 79
80 80 class PullRequestUpdateEvent(PullRequestEvent):
81 81 """
82 82 An instance of this class is emitted as an :term:`event` after a pull
83 83 request's commits have been updated.
84 84 """
85 85 name = 'pullrequest-update'
86 86 display_name = lazy_ugettext('pullrequest commits updated')
87 87
88 88
89 89 class PullRequestReviewEvent(PullRequestEvent):
90 90 """
91 91 An instance of this class is emitted as an :term:`event` after a pull
92 92 request review has changed.
93 93 """
94 94 name = 'pullrequest-review'
95 95 display_name = lazy_ugettext('pullrequest review changed')
96 96
97 97
98 98 class PullRequestMergeEvent(PullRequestEvent):
99 99 """
100 100 An instance of this class is emitted as an :term:`event` after a pull
101 101 request is merged.
102 102 """
103 103 name = 'pullrequest-merge'
104 104 display_name = lazy_ugettext('pullrequest merged')
105 105
106 106
107 107 class PullRequestCommentEvent(PullRequestEvent):
108 108 """
109 109 An instance of this class is emitted as an :term:`event` after a pull
110 110 request comment is created.
111 111 """
112 112 name = 'pullrequest-comment'
113 113 display_name = lazy_ugettext('pullrequest commented')
114 114
115 115 def __init__(self, pullrequest, comment):
116 116 super(PullRequestCommentEvent, self).__init__(pullrequest)
117 117 self.comment = comment
118 118
119 119 def as_dict(self):
120 120 from rhodecode.model.comment import CommentsModel
121 121 data = super(PullRequestCommentEvent, self).as_dict()
122 122
123 123 status = None
124 124 if self.comment.status_change:
125 125 status = self.comment.status_change[0].status
126 126
127 127 data.update({
128 128 'comment': {
129 129 'status': status,
130 130 'text': self.comment.text,
131 'type': self.comment.comment_type,
132 'file': self.comment.f_path,
133 'line': self.comment.line_no,
131 134 'url': CommentsModel().get_url(self.comment)
132 135 }
133 136 })
134 137 return data
@@ -1,265 +1,265 b''
1 1 # Copyright (C) 2016-2017 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 22 from rhodecode.model.db import User, Repository, Session
23 23 from rhodecode.events.base import RhodecodeEvent
24 24 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 def _commits_as_dict(commit_ids, repos):
30 30 """
31 31 Helper function to serialize commit_ids
32 32
33 33 :param commit_ids: commits to get
34 34 :param repos: list of repos to check
35 35 """
36 36 from rhodecode.lib.utils2 import extract_mentioned_users
37 from rhodecode.model.db import Repository
38 37 from rhodecode.lib import helpers as h
39 from rhodecode.lib.helpers import process_patterns
40 from rhodecode.lib.helpers import urlify_commit_message
38 from rhodecode.lib.helpers import (
39 urlify_commit_message, process_patterns, chop_at_smart)
41 40
42 41 if not repos:
43 42 raise Exception('no repo defined')
44 43
45 44 if not isinstance(repos, (tuple, list)):
46 45 repos = [repos]
47 46
48 47 if not commit_ids:
49 48 return []
50 49
51 50 needed_commits = list(commit_ids)
52 51
53 52 commits = []
54 53 reviewers = []
55 54 for repo in repos:
56 55 if not needed_commits:
57 56 return commits # return early if we have the commits we need
58 57
59 58 vcs_repo = repo.scm_instance(cache=False)
60 59 try:
61 60 # use copy of needed_commits since we modify it while iterating
62 61 for commit_id in list(needed_commits):
63 62 try:
64 63 cs = vcs_repo.get_changeset(commit_id)
65 64 except CommitDoesNotExistError:
66 65 continue # maybe its in next repo
67 66
68 67 cs_data = cs.__json__()
69 68 cs_data['mentions'] = extract_mentioned_users(cs_data['message'])
70 69 cs_data['reviewers'] = reviewers
71 70 cs_data['url'] = h.url('changeset_home',
72 71 repo_name=repo.repo_name,
73 72 revision=cs_data['raw_id'],
74 73 qualified=True
75 74 )
76 75 urlified_message, issues_data = process_patterns(
77 76 cs_data['message'], repo.repo_name)
78 77 cs_data['issues'] = issues_data
79 78 cs_data['message_html'] = urlify_commit_message(cs_data['message'],
80 79 repo.repo_name)
80 cs_data['message_html_title'] = chop_at_smart(cs_data['message'], '\n', suffix_if_chopped='...')
81 81 commits.append(cs_data)
82 82
83 83 needed_commits.remove(commit_id)
84 84
85 85 except Exception as e:
86 86 log.exception(e)
87 # we don't send any commits when crash happens, only full list matters
88 # we short circuit then.
87 # we don't send any commits when crash happens, only full list
88 # matters we short circuit then.
89 89 return []
90 90
91 91 missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits)
92 92 if missing_commits:
93 93 log.error('missing commits: %s' % ', '.join(missing_commits))
94 94
95 95 return commits
96 96
97 97
98 98 def _issues_as_dict(commits):
99 99 """ Helper function to serialize issues from commits """
100 100 issues = {}
101 101 for commit in commits:
102 102 for issue in commit['issues']:
103 103 issues[issue['id']] = issue
104 104 return issues
105 105
106 106
107 107 class RepoEvent(RhodecodeEvent):
108 108 """
109 109 Base class for events acting on a repository.
110 110
111 111 :param repo: a :class:`Repository` instance
112 112 """
113 113
114 114 def __init__(self, repo):
115 115 super(RepoEvent, self).__init__()
116 116 self.repo = repo
117 117
118 118 def as_dict(self):
119 119 from rhodecode.model.repo import RepoModel
120 120 data = super(RepoEvent, self).as_dict()
121 121 data.update({
122 122 'repo': {
123 123 'repo_id': self.repo.repo_id,
124 124 'repo_name': self.repo.repo_name,
125 125 'repo_type': self.repo.repo_type,
126 126 'url': RepoModel().get_url(self.repo)
127 127 }
128 128 })
129 129 return data
130 130
131 131
132 132 class RepoPreCreateEvent(RepoEvent):
133 133 """
134 134 An instance of this class is emitted as an :term:`event` before a repo is
135 135 created.
136 136 """
137 137 name = 'repo-pre-create'
138 138 display_name = lazy_ugettext('repository pre create')
139 139
140 140
141 141 class RepoCreateEvent(RepoEvent):
142 142 """
143 143 An instance of this class is emitted as an :term:`event` whenever a repo is
144 144 created.
145 145 """
146 146 name = 'repo-create'
147 147 display_name = lazy_ugettext('repository created')
148 148
149 149
150 150 class RepoPreDeleteEvent(RepoEvent):
151 151 """
152 152 An instance of this class is emitted as an :term:`event` whenever a repo is
153 153 created.
154 154 """
155 155 name = 'repo-pre-delete'
156 156 display_name = lazy_ugettext('repository pre delete')
157 157
158 158
159 159 class RepoDeleteEvent(RepoEvent):
160 160 """
161 161 An instance of this class is emitted as an :term:`event` whenever a repo is
162 162 created.
163 163 """
164 164 name = 'repo-delete'
165 165 display_name = lazy_ugettext('repository deleted')
166 166
167 167
168 168 class RepoVCSEvent(RepoEvent):
169 169 """
170 170 Base class for events triggered by the VCS
171 171 """
172 172 def __init__(self, repo_name, extras):
173 173 self.repo = Repository.get_by_repo_name(repo_name)
174 174 if not self.repo:
175 175 raise Exception('repo by this name %s does not exist' % repo_name)
176 176 self.extras = extras
177 177 super(RepoVCSEvent, self).__init__(self.repo)
178 178
179 179 @property
180 180 def actor(self):
181 181 if self.extras.get('username'):
182 182 return User.get_by_username(self.extras['username'])
183 183
184 184 @property
185 185 def actor_ip(self):
186 186 if self.extras.get('ip'):
187 187 return self.extras['ip']
188 188
189 189 @property
190 190 def server_url(self):
191 191 if self.extras.get('server_url'):
192 192 return self.extras['server_url']
193 193
194 194
195 195 class RepoPrePullEvent(RepoVCSEvent):
196 196 """
197 197 An instance of this class is emitted as an :term:`event` before commits
198 198 are pulled from a repo.
199 199 """
200 200 name = 'repo-pre-pull'
201 201 display_name = lazy_ugettext('repository pre pull')
202 202
203 203
204 204 class RepoPullEvent(RepoVCSEvent):
205 205 """
206 206 An instance of this class is emitted as an :term:`event` after commits
207 207 are pulled from a repo.
208 208 """
209 209 name = 'repo-pull'
210 210 display_name = lazy_ugettext('repository pull')
211 211
212 212
213 213 class RepoPrePushEvent(RepoVCSEvent):
214 214 """
215 215 An instance of this class is emitted as an :term:`event` before commits
216 216 are pushed to a repo.
217 217 """
218 218 name = 'repo-pre-push'
219 219 display_name = lazy_ugettext('repository pre push')
220 220
221 221
222 222 class RepoPushEvent(RepoVCSEvent):
223 223 """
224 224 An instance of this class is emitted as an :term:`event` after commits
225 225 are pushed to a repo.
226 226
227 227 :param extras: (optional) dict of data from proxied VCS actions
228 228 """
229 229 name = 'repo-push'
230 230 display_name = lazy_ugettext('repository push')
231 231
232 232 def __init__(self, repo_name, pushed_commit_ids, extras):
233 233 super(RepoPushEvent, self).__init__(repo_name, extras)
234 234 self.pushed_commit_ids = pushed_commit_ids
235 235
236 236 def as_dict(self):
237 237 data = super(RepoPushEvent, self).as_dict()
238 238 branch_url = repo_url = data['repo']['url']
239 239
240 240 commits = _commits_as_dict(
241 241 commit_ids=self.pushed_commit_ids, repos=[self.repo])
242 242
243 243 last_branch = None
244 244 for commit in reversed(commits):
245 245 commit['branch'] = commit['branch'] or last_branch
246 246 last_branch = commit['branch']
247 247 issues = _issues_as_dict(commits)
248 248
249 249 branches = set(
250 250 commit['branch'] for commit in commits if commit['branch'])
251 251 branches = [
252 252 {
253 253 'name': branch,
254 254 'url': '{}/changelog?branch={}'.format(
255 255 data['repo']['url'], branch)
256 256 }
257 257 for branch in branches
258 258 ]
259 259
260 260 data['push'] = {
261 261 'commits': commits,
262 262 'issues': issues,
263 263 'branches': branches,
264 264 }
265 265 return data
@@ -1,263 +1,334 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 import re
23 import time
24 import textwrap
25 import logging
26
22 27 import deform
23 import re
24 import logging
25 28 import requests
26 29 import colander
27 import textwrap
28 30 from celery.task import task
29 31 from mako.template import Template
30 32
31 33 from rhodecode import events
32 34 from rhodecode.translation import _
33 35 from rhodecode.lib import helpers as h
34 36 from rhodecode.lib.celerylib import run_task
35 37 from rhodecode.lib.colander_utils import strip_whitespace
36 38 from rhodecode.integrations.types.base import IntegrationTypeBase
37 39
38 40 log = logging.getLogger(__name__)
39 41
40 42
41 43 class SlackSettingsSchema(colander.Schema):
42 44 service = colander.SchemaNode(
43 45 colander.String(),
44 46 title=_('Slack service URL'),
45 47 description=h.literal(_(
46 48 'This can be setup at the '
47 49 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
48 50 'slack app manager</a>')),
49 51 default='',
50 52 preparer=strip_whitespace,
51 53 validator=colander.url,
52 54 widget=deform.widget.TextInputWidget(
53 55 placeholder='https://hooks.slack.com/services/...',
54 56 ),
55 57 )
56 58 username = colander.SchemaNode(
57 59 colander.String(),
58 60 title=_('Username'),
59 61 description=_('Username to show notifications coming from.'),
60 62 missing='Rhodecode',
61 63 preparer=strip_whitespace,
62 64 widget=deform.widget.TextInputWidget(
63 65 placeholder='Rhodecode'
64 66 ),
65 67 )
66 68 channel = colander.SchemaNode(
67 69 colander.String(),
68 70 title=_('Channel'),
69 71 description=_('Channel to send notifications to.'),
70 72 missing='',
71 73 preparer=strip_whitespace,
72 74 widget=deform.widget.TextInputWidget(
73 75 placeholder='#general'
74 76 ),
75 77 )
76 78 icon_emoji = colander.SchemaNode(
77 79 colander.String(),
78 80 title=_('Emoji'),
79 81 description=_('Emoji to use eg. :studio_microphone:'),
80 82 missing='',
81 83 preparer=strip_whitespace,
82 84 widget=deform.widget.TextInputWidget(
83 85 placeholder=':studio_microphone:'
84 86 ),
85 87 )
86 88
87 89
88 repo_push_template = Template(r'''
89 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
90 %for branch, branch_commits in branches_commits.items():
91 branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
92 %for commit in branch_commits['commits']:
93 > <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
94 %endfor
95 %endfor
96 ''')
97
98
99 90 class SlackIntegrationType(IntegrationTypeBase):
100 91 key = 'slack'
101 92 display_name = _('Slack')
102 93 description = _('Send events such as repo pushes and pull requests to '
103 94 'your slack channel.')
104 95 icon = '''<?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>'''
105 96 valid_events = [
106 97 events.PullRequestCloseEvent,
107 98 events.PullRequestMergeEvent,
108 99 events.PullRequestUpdateEvent,
109 100 events.PullRequestCommentEvent,
110 101 events.PullRequestReviewEvent,
111 102 events.PullRequestCreateEvent,
112 103 events.RepoPushEvent,
113 104 events.RepoCreateEvent,
114 105 ]
115 106
116 107 def send_event(self, event):
117 108 if event.__class__ not in self.valid_events:
118 109 log.debug('event not valid: %r' % event)
119 110 return
120 111
121 112 if event.name not in self.settings['events']:
122 113 log.debug('event ignored: %r' % event)
123 114 return
124 115
125 116 data = event.as_dict()
126 117
118 # defaults
119 title = '*%s* caused a *%s* event' % (
120 data['actor']['username'], event.name)
127 121 text = '*%s* caused a *%s* event' % (
128 122 data['actor']['username'], event.name)
123 fields = None
124 overrides = None
129 125
130 126 log.debug('handling slack event for %s' % event.name)
131 127
132 128 if isinstance(event, events.PullRequestCommentEvent):
133 text = self.format_pull_request_comment_event(event, data)
129 (title, text, fields, overrides) \
130 = self.format_pull_request_comment_event(event, data)
134 131 elif isinstance(event, events.PullRequestReviewEvent):
135 text = self.format_pull_request_review_event(event, data)
132 title, text = self.format_pull_request_review_event(event, data)
136 133 elif isinstance(event, events.PullRequestEvent):
137 text = self.format_pull_request_event(event, data)
134 title, text = self.format_pull_request_event(event, data)
138 135 elif isinstance(event, events.RepoPushEvent):
139 text = self.format_repo_push_event(data)
136 title, text = self.format_repo_push_event(data)
140 137 elif isinstance(event, events.RepoCreateEvent):
141 text = self.format_repo_create_event(data)
138 title, text = self.format_repo_create_event(data)
142 139 else:
143 140 log.error('unhandled event type: %r' % event)
144 141
145 run_task(post_text_to_slack, self.settings, text)
142 run_task(post_text_to_slack, self.settings, title, text, fields, overrides)
146 143
147 144 def settings_schema(self):
148 145 schema = SlackSettingsSchema()
149 146 schema.add(colander.SchemaNode(
150 147 colander.Set(),
151 148 widget=deform.widget.CheckboxChoiceWidget(
152 149 values=sorted(
153 150 [(e.name, e.display_name) for e in self.valid_events]
154 151 )
155 152 ),
156 153 description="Events activated for this integration",
157 154 name='events'
158 155 ))
159 156
160 157 return schema
161 158
162 159 def format_pull_request_comment_event(self, event, data):
163 160 comment_text = data['comment']['text']
164 161 if len(comment_text) > 200:
165 162 comment_text = '<{comment_url}|{comment_text}...>'.format(
166 163 comment_text=comment_text[:200],
167 164 comment_url=data['comment']['url'],
168 165 )
169 166
170 comment_status = ''
167 fields = None
168 overrides = None
169 status_text = None
170
171 171 if data['comment']['status']:
172 comment_status = '[{}]: '.format(data['comment']['status'])
172 status_color = {
173 'approved': '#0ac878',
174 'rejected': '#e85e4d'}.get(data['comment']['status'])
175
176 if status_color:
177 overrides = {"color": status_color}
178
179 status_text = data['comment']['status']
180
181 if data['comment']['file']:
182 fields = [
183 {
184 "title": "file",
185 "value": data['comment']['file']
186 },
187 {
188 "title": "line",
189 "value": data['comment']['line']
190 }
191 ]
173 192
174 return (textwrap.dedent(
175 '''
176 *{user}* commented on pull request <{pr_url}|#{number}> - {pr_title}:
177 >>> {comment_status}{comment_text}
178 ''').format(
179 comment_status=comment_status,
180 user=data['actor']['username'],
181 number=data['pullrequest']['pull_request_id'],
182 pr_url=data['pullrequest']['url'],
183 pr_status=data['pullrequest']['status'],
184 pr_title=data['pullrequest']['title'],
185 comment_text=comment_text
186 )
193 title = Template(textwrap.dedent(r'''
194 *${data['actor']['username']}* left ${data['comment']['type']} on pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
195 ''')).render(data=data, comment=event.comment)
196
197 text = Template(textwrap.dedent(r'''
198 *pull request title*: ${pr_title}
199 % if status_text:
200 *submitted status*: `${status_text}`
201 % endif
202 >>> ${comment_text}
203 ''')).render(comment_text=comment_text,
204 pr_title=data['pullrequest']['title'],
205 status_text=status_text)
206
207 return title, text, fields, overrides
208
209 def format_pull_request_review_event(self, event, data):
210 title = Template(textwrap.dedent(r'''
211 *${data['actor']['username']}* changed status of pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']} to `${data['pullrequest']['status']}`>:
212 ''')).render(data=data)
213
214 text = Template(textwrap.dedent(r'''
215 *pull request title*: ${pr_title}
216 ''')).render(
217 pr_title=data['pullrequest']['title'],
187 218 )
188 219
189 def format_pull_request_review_event(self, event, data):
190 return (textwrap.dedent(
191 '''
192 Status changed to {pr_status} for pull request <{pr_url}|#{number}> - {pr_title}
193 ''').format(
194 user=data['actor']['username'],
195 number=data['pullrequest']['pull_request_id'],
196 pr_url=data['pullrequest']['url'],
197 pr_status=data['pullrequest']['status'],
198 pr_title=data['pullrequest']['title'],
199 )
200 )
220 return title, text
201 221
202 222 def format_pull_request_event(self, event, data):
203 223 action = {
204 224 events.PullRequestCloseEvent: 'closed',
205 225 events.PullRequestMergeEvent: 'merged',
206 226 events.PullRequestUpdateEvent: 'updated',
207 227 events.PullRequestCreateEvent: 'created',
208 228 }.get(event.__class__, str(event.__class__))
209 229
210 return ('Pull request <{url}|#{number}> - {title} '
211 '`{action}` by *{user}*').format(
212 user=data['actor']['username'],
213 number=data['pullrequest']['pull_request_id'],
214 url=data['pullrequest']['url'],
215 title=data['pullrequest']['title'],
216 action=action
230 title = Template(textwrap.dedent(r'''
231 *${data['actor']['username']}* `${action}` pull request <${data['pullrequest']['url']}|#${data['pullrequest']['pull_request_id']}>:
232 ''')).render(data=data, action=action)
233
234 text = Template(textwrap.dedent(r'''
235 *pull request title*: ${pr_title}
236 %if data['pullrequest']['commits']:
237 *commits*: ${len(data['pullrequest']['commits'])}
238 %endif
239 ''')).render(
240 pr_title=data['pullrequest']['title'],
241 data=data
217 242 )
218 243
244 return title, text
245
219 246 def format_repo_push_event(self, data):
220 247 branch_data = {branch['name']: branch
221 248 for branch in data['push']['branches']}
222 249
223 250 branches_commits = {}
224 251 for commit in data['push']['commits']:
225 252 if commit['branch'] not in branches_commits:
226 253 branch_commits = {'branch': branch_data[commit['branch']],
227 254 'commits': []}
228 255 branches_commits[commit['branch']] = branch_commits
229 256
230 257 branch_commits = branches_commits[commit['branch']]
231 258 branch_commits['commits'].append(commit)
232 259
233 result = repo_push_template.render(
260 title = Template(r'''
261 *${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
262 ''').render(data=data)
263
264 repo_push_template = Template(textwrap.dedent(r'''
265 %for branch, branch_commits in branches_commits.items():
266 branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
267 %for commit in branch_commits['commits']:
268 `<${commit['url']}|${commit['short_id']}>` - ${commit['message_html_title']|html_to_slack_links}
269 %endfor
270 %endfor
271 '''))
272
273 text = repo_push_template.render(
234 274 data=data,
235 275 branches_commits=branches_commits,
236 276 html_to_slack_links=html_to_slack_links,
237 277 )
238 return result
278
279 return title, text
239 280
240 281 def format_repo_create_event(self, data):
241 return '<{}|{}> ({}) repository created by *{}*'.format(
242 data['repo']['url'],
243 data['repo']['repo_name'],
244 data['repo']['repo_type'],
245 data['actor']['username'],
246 )
282 title = Template(r'''
283 *${data['actor']['username']}* created new repository ${data['repo']['repo_name']}:
284 ''').render(data=data)
285
286 text = Template(textwrap.dedent(r'''
287 repo_url: ${data['repo']['url']}
288 repo_type: ${data['repo']['repo_type']}
289 ''')).render(data=data)
290
291 return title, text
247 292
248 293
249 294 def html_to_slack_links(message):
250 295 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
251 296 r'<\1|\2>', message)
252 297
253 298
254 299 @task(ignore_result=True)
255 def post_text_to_slack(settings, text):
256 log.debug('sending %s to slack %s' % (text, settings['service']))
257 resp = requests.post(settings['service'], json={
300 def post_text_to_slack(settings, title, text, fields=None, overrides=None):
301 log.debug('sending %s (%s) to slack %s' % (
302 title, text, settings['service']))
303
304 fields = fields or []
305 overrides = overrides or {}
306
307 message_data = {
308 "fallback": text,
309 "color": "#427cc9",
310 "pretext": title,
311 #"author_name": "Bobby Tables",
312 #"author_link": "http://flickr.com/bobby/",
313 #"author_icon": "http://flickr.com/icons/bobby.jpg",
314 #"title": "Slack API Documentation",
315 #"title_link": "https://api.slack.com/",
316 "text": text,
317 "fields": fields,
318 #"image_url": "http://my-website.com/path/to/image.jpg",
319 #"thumb_url": "http://example.com/path/to/thumb.png",
320 "footer": "RhodeCode",
321 #"footer_icon": "",
322 "ts": time.time(),
323 "mrkdwn_in": ["pretext", "text"]
324 }
325 message_data.update(overrides)
326 json_message = {
327 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:'),
258 328 "channel": settings.get('channel', ''),
259 329 "username": settings.get('username', 'Rhodecode'),
260 "text": text,
261 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
262 })
330 "attachments": [message_data]
331 }
332
333 resp = requests.post(settings['service'], json=json_message)
263 334 resp.raise_for_status() # raise exception on a failed request
General Comments 0
You need to be logged in to leave comments. Login now