Initial commit
[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 import gitlab
6
7 from Secret import admin_token
8 git_host = "gitlab.services.zz" #TODO: move this to some sort of plugin config
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 'https://webookserver/margeboot/<rooms>' to the projects you want tracked.
18 <rooms> should be a comma-separated list of rooms you want notified.
19 In errbot:
20 Add <roomname>@domain to the CHATROOM_PRESENCE list in config.py for rooms margebot should join
21 """
22
23 CRONTAB = [
24 '0 11,17 * * * .crontab_hook' # 7:00AM and 1:00PM EST warnings
25 ]
26
27 # def __init__(self, bot):
28 # self.gitlab = None
29
30 def configure(self, configuration):
31 ## TODO: set up a connection to the gitlab API
32
33 self.gitlab = gitlab.Gitlab(git_host,admin_token,verify_ssl=False)
34
35 # notify_re = re.compile('notify_(.*)')
36
37 # def get_mr_rooms(self, project_id):
38 # """
39 # Return a list of errbot room '<roomname>@domain' names which have a 'notify_<roomname>' label on the project
40 # Log an error if you found a 'notify_<roomname>' but margebot isn't in a <roomname> room...
41 # """
42 # retval = []
43 # labels = this.gitlab.getlabels(project_id)
44 # for a_label in labels:
45 # notify_match = self.notify_re.search(a_label['name'])
46 # if notify_match:
47 # roomname = notify_match.group(1)
48 #
49 # b_room_found = False
50 # marge_rooms = xmpp.rooms()
51 # for a_room in marge_rooms:
52 # if a_room.node() == roomname:
53 # retval.append(a_room.person()) # yeah rooms are people: person = node@domain
54 # b_room_found = True
55 # if not b_room_found:
56 # self.log.error("Label of {} found, but margebot isn't tracking that room".format(roomname))
57 # else:
58 # retval.append(roomname)
59 # return retval
60
61 @webhook('/margebot/<rooms>/'
62 def gitlab_hook(self, request):
63 """
64 Webhook that listens on http://<server>:<port>/gitlab
65 """
66
67 # TODO: Will errbot return a json struct or not ?
68
69 # verify it's a merge request
70 if requests['object_kind'] != 'merge_request':
71 self.log.error('expecting object_kind of merge_request but got {}'.format(requests['object_kind']))
72 elif request['object_attributes']['state'] == 'opened':
73 if request['object_attributes']['work_in_progress']:
74 wip = "WIP"
75 else:
76 wip = ""
77 url = request['object_attributes']['url']
78 state = request['object_attributes']['state']
79 title = request['object_attributes']['title']
80
81 author_id = request['object_attributes']['author_id'] # map this to user name ...
82 author = self.gitlab.getuser(author_id)
83 author_name = author['username']
84
85 target_project_id = request['object_attributes']['target_project_id']
86 iid = request['object_attributes']['iid']
87
88 user_name = request['user']['username'] # will this always be Administrator ?
89
90 message = "Reviews: {} has opened a new {} MR: {}\n{}".format(author_name, wip, title, url)
91
92 # TODO: Maybe also check the notify_<room> labels assigned to the MR as well ?
93 #mr_rooms = self.get_mr_rooms(target_project_id)
94 for a_room in rooms.split(','):
95 self.send( self.build_identifier(a_room), message)
96
97 with self.mutable('OPEN_MRS') as open_mrs:
98 open_mrs[(target_project_id,iid,rooms)] = True
99
100 return "OK"
101
102 # def get_open_mrs(self, roomname=None, log_warnings=False):
103 # mrs = []
104 # for a_project in self.gitlab.getprojects():
105 # for a_mr in self.gitlab.getmergerequests(a_project['id'], state='opened'):
106 # rooms = self.get_mr_rooms(a_mr['target_project_id'])
107 # if len(rooms) == 0 and log_warnings:
108 # self.log.warning('No notify room with MRs in project: {}'.format(a_mr['target_project_id']))
109 # elif not roomname:
110 # mrs.append(a_mr)
111 # else:
112 # for a_room in rooms:
113 # if a_room.startswith(roomname):
114 # mrs.append(a_mr)
115 # return mrs
116
117 def crontab_hook(self):
118 """
119 Send a scheduled message to the rooms margebot is watching about open MRs the room cares about.
120 """
121
122 reminder_msg = {} # Map of reminder_msg['roomname@domain'] = msg
123
124 # initialize the reminders
125 rooms = xmpp.rooms()
126 for a_room in rooms:
127 reminder_msg[a_room.node()] = ''
128
129 still_open_mrs = {}
130
131 # Let's walk through the MRs we've seen already:
132 for (project,iid,notify_rooms) in self['OPEM_MRS']:
133
134 # Lookup the MR from the project/iid
135 a_mr = self.gitlab.getmergerequest(project, iid)
136
137 # If the MR is no longer open, skip to the next MR,
138 # and don't include this MR in the next check
139 if a_mr['state'] != 'open':
140 continue
141 else:
142 still_open_mrs[(project, iid, notify_rooms) = True
143
144 # TODO: Warn if an open MR has has conflicts (merge_status == ??)
145 # TODO: Include the count of opened MR notes (does the API show resolved state ??)
146
147 approvals = self.gitlab.getapprovals(a_mr['id'])
148 also_approved = ""
149 for approved in approvals['approved_by']:
150 also_approved += "," + approved['user']['name']
151
152 upvotes = a_mr['upvotes']
153 if upvotes >= 2:
154 msg = "\n{}: Has 2+ upvotes and could be merged in now.".format(a_mr['web_url'])
155 elif upvotes == 1:
156 msg = "\n{}: {} already approved and is waiting for another upvote.".format(a_mr['web_url'], also_approved[1:])
157 else:
158 msg = '\n{}: Has no upvotes.'.format(a_mr['web_url'])
159
160 for a_room in notify_rooms.split(','):
161 reminder_msg[a_room] += msg
162
163 # Remind each of the rooms about open MRs
164 for a_room, room_msg in reminder_msg.iteritems():
165 if room_msg != "":
166 self.send(self.build_identifier(a_room), "Heads up these MRs need some luv:{}\n You can get a list of open reviews I know about by sending me a 'Marge, reviews' command.".format(room_msg))
167
168 self['OPEN_MRS'] = still_open_mrs
169
170 # def callback_message(self, mess):
171 # """
172 # Look for messages that include an URL that looks like 'https://<gitlab_server>/<group_name>/<project_name>/merge_request/<iid>'
173 # Check if gitlab mergerequest webhook already exists for this group/project, and this it reports to this room ?
174 # Add the project to the OPEN_MRS list
175 # """
176 # TODO: compiled re check against mess.body searching for
177 # https://<gitlabe_server/(group_name)/(project_name)/merge_request/(iid)
178 # project = self.gitlab.getprojects(group_name+'%2F'+project_name)
179 # orig_mrs = self['OPEN_MRS']
180 # orig_mrs[(project['id'],iid,mess.to.node()] = True
181 # self.send(mess.to, "Another MR ! YUM !")
182 # return
183
184 @botcmd # flags a command
185 def reviews(self, msg, args): # a command callable with !mrs
186 """
187 Returns a list of MRs that are waiting for some luv.
188 Also returns a list of MRs that have had enough luv but aren't merged in yet.
189 """
190 sender = msg.frm
191
192 send_gitlab_id = None
193 for user in self.gitlab.getusers():
194 if user['username'] == sender.node:
195 sender_gitlab_id = user['id']
196 break
197
198 if not send_gitlab_id:
199 self.log.error('problem mapping {} to a gitlab user'.format(sender.node))
200 self.send(self.build_identifier(msg.frm), "Sorry I couldn't find your gitlab ID")
201 return
202
203 # TODO: how to get the room the message was sent from ? I'm assuming this will either be in msg.frm or msg.to
204 # TODO: weed out MRs the sender opened or otherwise indicate they've opened or have already +1'd
205
206 roomname = msg.to.domain #???
207
208 # Let's walk through the MRs we've seen already:
209 msg = ""
210 still_open_mrs = {}
211 for (project,iid,notify_rooms) in self['OPEM_MRS']:
212
213 # Lookup the MR from the project/iid
214 a_mr = self.gitlab.getmergerequest(project, iid)
215
216 # If the MR is no longer open, skip to the next MR,
217 # and don't include this MR in the next check
218 if a_mr['state'] != 'open':
219 continue
220 else:
221 still_open_mrs[(project, iid, notify_rooms) = True
222
223 authored = (a_mr['author_id'] == sender_gitlab_id)
224 already_approved = False
225
226 approvals = self.gitlab.getapprovals(a_mr['id'])
227 also_approved = ""
228 for approved in approvals['approved_by']:
229 if approved['user']['id'] == sender_gitlab_id:
230 already_approved = True
231 else:
232 also_approved += "," + approved['user']['name']
233
234 upvotes = a_mr['upvotes']
235 if upvotes >= 2:
236 msg = "\n{}: has 2+ upvotes from {} and could be merged in now.".format(a_mr['web_url'], also_approved[1:])
237 elif upvotes == 1:
238 if not authored:
239 if already_approved:
240 msg = "\n{}: has already been approved by you and is waiting for another upvote.".format(a_mr['web_url'])
241 else:
242 msg = "\n{}: has been approved by {} and is waiting for your upvote.".format(a_mr['web_url'], also_approved[1:])
243 else:
244 msg = "\n{}: Your MR has approved by {} and is waiting for another upvote.".format(a_mr['web_url'], also_approved[1:])
245
246 else:
247 if not authored:
248 msg = '\n{}: Has no upvotes and needs your attention.'.format(a_mr['web_url'])
249 else:
250 msg = '\n{}: Your MR has no upvotes.'.format(a_mr['web_url'])
251
252
253 if msg == "":
254 response = 'I found no open merge requests for you.'
255 else:
256 response = msg
257
258 self.send(self.build_identifier(msg.frm), response)
259
260 self['OPEN_MRS'] = still_open_mrs
261
262 return
263
264 @botcmd()
265 def hello(self,msg, args):
266 return "Hi there"