Checkpoint / crontab_hook working now
[margebot.git] / marge.py
1 import re
2 from errbot import BotPlugin, botcmd, webhook
3 from errbot.backends import xmpp
4 from errcron.bot import CrontabMixin
5 from time import sleep
6 import gitlab
7
8 # TODO: Add certificate verification to the gitlab API calls.
9
10
11 class Marge(BotPlugin, CrontabMixin):
12 """
13 I remind you about merge requests
14
15 Use:
16 In gitlab:
17 Add a merge request webook of the form
18 'https://webookserver/margeboot/<rooms>'
19 to the projects you want tracked. <rooms> should be a
20 comma-separated list of short room names (anything before the '@')
21 that you want notified.
22 In errbot:
23 Add <roomname>@domain to the CHATROOM_PRESENCE list in config.py for
24 rooms margebot should join
25 """
26
27 CRONTAB = [
28 # Set in config now: '0 11,17 * * * .crontab_hook' # 7:00AM and 1:00PM EST warnings
29 ]
30
31 def __init__(self, *args, **kwargs):
32 self.git_host = None
33 self.chatroom_host = None
34 super().__init__(*args, **kwargs)
35
36 def get_configuration_template(self):
37 """
38 GITLAB_HOST: Host name of your gitlab server
39 GITLAB_ADMIN_TOKEN: PAT from an admin's https://${GIT_HOST}/profile/personal_access_tokens page.
40 CHATROOM_HOST: Chatroom host. Usually 'chatroom' + FQDN of Jabber server
41 CRONTAB: Schedule of automated merge request checks in '%M %H %d %m %w' format
42 VERIFY_SSL : True, False, or path to CA cert to verify cert
43 """
44 return {'GITLAB_HOST': 'gitlab.example.com',
45 'GITLAB_ADMIN_TOKEN' : 'gitlab-admin-user-private-token',
46 'CHATROOM_HOST': 'conference.jabber.example.com',
47 'CRONTAB' : '0 11,17 * * *',
48 'VERIFY_SSL' : True}
49
50 def check_configuration(self, configuration):
51 super().check_configuration(configuration)
52
53 def activate(self):
54 if not self.config:
55 self.log.info('Margebot is not configured. Forbid activation')
56 return
57 self.git_host = self.config['GITLAB_HOST']
58 self.chatroom_host = self.config['CHATROOM_HOST']
59 Marge.CRONTAB = ['{} .crontab_hook'.format(self.config['CRONTAB']) ]
60 self.gitlab = gitlab.Gitlab(self.git_host, self.config['GITLAB_ADMIN_TOKEN'], verify_ssl=self.config['VERIFY_SSL'])
61 self.activate_crontab()
62
63 super().activate()
64
65 def deactivate(self):
66 # TODO: Anything special for closing gitlab ?
67 super().deactivate()
68
69 @webhook('/margebot/<rooms>/')
70 def gitlab_hook(self, request, rooms):
71 """
72 Webhook that listens on http://<server>:<port>/gitlab
73 """
74
75 self.log.info('margebot webhook request: {}'.format(request))
76 self.log.info('margebot webhook rooms {}'.format(rooms))
77
78 # TODO: Will errbot return a json struct or not ?
79
80 # verify it's a merge request
81 if request['object_kind'] != 'merge_request':
82 self.log.error('expecting object_kind of merge_request but got {}'.format(request['object_kind']))
83 self.log.error('request: {}'.format(request))
84 elif request['object_attributes']['state'] == 'opened':
85
86 # TODO:
87 # - check for reopened / request['object_attributes']['action'] == 'reopn'
88 # (there's no 'action': 'opened' for MRs are created...
89 # - pop open_mrs when MRs are closed (action == close / state == closed
90
91 if request['object_attributes']['work_in_progress']:
92 wip = "WIP"
93 else:
94 wip = ""
95 url = request['project']['homepage']
96 state = request['object_attributes']['state']
97 title = request['object_attributes']['title']
98
99 author_id = request['object_attributes']['author_id'] # map this to user name ...
100 author = self.gitlab.getuser(author_id)
101 author_name = author['username']
102
103 target_project_id = request['object_attributes']['target_project_id']
104 iid = request['object_attributes']['iid']
105
106 user_name = request['user']['username'] # will this always be Administrator ?
107
108 msg_template = "New Review: {} has opened a new {} MR: \"{}\"\n{}/merge_requests/{}"
109 msg = msg_template.format(author_name, wip, title, url, iid)
110
111 for a_room in rooms.split(','):
112 if self.config:
113 self.send(self.build_identifier(a_room + '@' + self.chatroom_host), msg)
114
115 if 'OPEN_MRS' not in self.keys():
116 empty_dict = {}
117 self['OPEN_MRS'] = empty_dict
118
119 with self.mutable('OPEN_MRS') as open_mrs:
120 open_mrs[(target_project_id, iid, rooms)] = True
121
122 return "OK"
123
124 def crontab_hook(self, polled_time):
125 """
126 Send a scheduled message to the rooms margebot is watching
127 about open MRs the room cares about.
128 """
129
130 self.log.info("crontab_hook triggered at {}".format(polled_time))
131
132 reminder_msg = {} # Map of reminder_msg['roomname@domain'] = msg
133
134 # initialize the reminders
135 rooms = self.rooms()
136 for a_room in rooms:
137 reminder_msg[a_room.node] = ''
138
139 msg = ""
140 still_open_mrs = {}
141
142 # Let's walk through the MRs we've seen already:
143 with self.mutable('OPEN_MRS') as open_mrs:
144 for (project, iid, notify_rooms) in open_mrs:
145
146 # Lookup the MR from the project/iid
147 a_mr = self.gitlab.getmergerequest(project, iid)
148
149 self.log.info("a_mr: {} {} {} {}".format(project, iid, notify_rooms, a_mr['state']))
150
151 # If the MR is no longer open, skip to the next MR,
152 # and don't include this MR in the next check
153 if a_mr['state'] != 'opened':
154 continue
155 else:
156 still_open_mrs[(project, iid, notify_rooms)] = True
157
158 # TODO: Warn if an open MR has has conflicts (merge_status == ??)
159 # TODO: Include the count of opened MR notes (does the API show resolved state ??)
160
161 # getapprovals is only available in GitLab 8.9 EE or greater (not the open source CE version)
162 # approvals = self.gitlab.getapprovals(a_mr['id'])
163 # also_approved = ""
164 # for approved in approvals['approved_by']:
165 # also_approved += "," + approved['user']['name']
166
167 upvotes = a_mr['upvotes']
168 if upvotes >= 2:
169 msg = "\n{}: Has 2+ upvotes / Could be merged in now.".format(a_mr['web_url'])
170 elif upvotes == 1:
171 msg_template = "\n{}: Waiting for another upvote."
172 msg = msg_template.format(a_mr['web_url'])
173 else:
174 msg = '\n{}: Noo upvotes / Please Review.'.format(a_mr['web_url'])
175
176 for a_room in notify_rooms.split(','):
177 reminder_msg[a_room] += msg
178
179 # Remind each of the rooms about open MRs
180 for a_room, room_msg in reminder_msg.items():
181 if room_msg != "":
182 if self.config:
183 msg_template = "Heads up these MRs need some attention:{}\n"
184 msg_template += "You can get an updated list with the '/msg MargeB !reviews' command."
185 msg = msg_template.format(room_msg)
186 self.send(self.build_identifier(a_room + '@' + self.config['CHATROOM_HOST']), msg)
187
188 self['OPEN_MRS'] = still_open_mrs
189
190 @botcmd()
191 def reviews(self, msg, args): # a command callable with !mrs
192 """
193 Returns a list of MRs that are waiting for some luv.
194 Also returns a list of MRs that have had enough luv but aren't merged in yet.
195 """
196 ## Sending directly to Margbot: sender in the form sender@....
197 ## Sending to a chatroom: snder in the form room@rooms/sender
198
199 if msg.frm.domain == self.config['CHATROOM_HOST']:
200 sender = msg.frm.resource
201 else:
202 sender = msg.frm.node
203
204 if 'OPEN_MRS' not in self.keys():
205 return "No MRs to review"
206
207 sender_gitlab_id = None
208 for user in self.gitlab.getusers():
209 if user['username'] == sender:
210 sender_gitlab_id = user['id']
211 break
212
213 if not sender_gitlab_id:
214 self.log.error('problem mapping {} to a gitlab user'.format(sender))
215 return "Sorry, I couldn't find your gitlab account."
216
217 # Walk through the MRs we've seen already:
218 msg = ""
219 still_open_mrs = {}
220 with self.mutable('OPEN_MRS') as open_mrs:
221 for (project, iid, notify_rooms) in open_mrs:
222
223 # Lookup the MR from the project/iid
224 a_mr = self.gitlab.getmergerequest(project, iid)
225
226 # If the MR is no longer open, skip to the next MR,
227 # and don't include this MR in the next check
228 if a_mr['state'] != 'opened':
229 continue
230 else:
231 still_open_mrs[(project, iid, notify_rooms)] = True
232
233 authored = (a_mr['author']['id'] == sender_gitlab_id)
234 already_approved = False
235
236 # getapprovals is currently only available in GitLab >= 8.9 EE (not available in the CE yet)
237 # approvals = self.gitlab.getapprovals(a_mr['id'])
238 # also_approved = ""
239 # for approved in approvals['approved_by']:
240 # if approved['user']['id'] == sender_gitlab_id:
241 # already_approved = True
242 # else:
243 # also_approved += "," + approved['user']['name']
244
245 upvotes = a_mr['upvotes']
246 if upvotes >= 2:
247 msg += "\n{}: has 2+ upvotes and could be merged in now.".format(a_mr['web_url'])
248 elif upvotes == 1:
249 if not authored:
250 msg += "\n{}: is waiting for another upvote.".format(a_mr['web_url'])
251 else:
252 msg += "\n{}: Your MR is waiting for another upvote.".format(a_mr['web_url'])
253
254 else:
255 if not authored:
256 msg += '\n{}: Has no upvotes and needs your attention.'.format(a_mr['web_url'])
257 else:
258 msg += '\n{}: Your MR has no upvotes.'.format(a_mr['web_url'])
259
260 if msg == "":
261 response = 'Hi {}\n{}'.format(sender, 'I found no open MRs for you.')
262 else:
263 response = 'Hi {}\n{}'.format(sender,msg)
264
265 # self.send(self.build_identifier(msg.frm), response)
266
267 with self.mutable('OPEN_MRS') as open_mrs:
268 open_mrs = still_open_mrs
269
270 return response
271
272 @botcmd()
273 def hello(self, msg, args):
274 return "Hi there"
275
276 @botcmd()
277 def xyzzy(self, msg, args):
278 yield "/me whispers \"All open MRs have ben merged into master.\""
279 sleep(5)
280 yield "(just kidding)"