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