Initial commit
authorJude N <juden@pwan.org>
Wed, 28 Jun 2017 11:41:49 +0000 (07:41 -0400)
committerJude N <juden@pwan.org>
Wed, 28 Jun 2017 11:41:49 +0000 (07:41 -0400)
README.md [new file with mode: 0644]
Secret.py [new file with mode: 0755]
marge.plug [new file with mode: 0644]
marge.py [new file with mode: 0755]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..4acd309
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+_margebot_ -- I remind you about outstanding Gitlab merge requests
+
+
+
+
diff --git a/Secret.py b/Secret.py
new file mode 100755 (executable)
index 0000000..18d15e5
--- /dev/null
+++ b/Secret.py
@@ -0,0 +1,9 @@
+# go away
+
+# https://docs.gitlab.com/ee/api/README.html#personal-access-tokens
+
+# token for a normal, non-admin user
+my_token="get-this-from-gitlab" 
+
+# token for actions that require admin access
+admin_token="get-this-from-gitlab" 
diff --git a/marge.plug b/marge.plug
new file mode 100644 (file)
index 0000000..6138f43
--- /dev/null
@@ -0,0 +1,11 @@
+[Core]
+Name = Marge
+Module = marge
+
+[Documentation]
+Description = Marge helps with Gitlab merge requests
+
+[Python]
+Version = 3
+
+
diff --git a/marge.py b/marge.py
new file mode 100755 (executable)
index 0000000..f00b3fc
--- /dev/null
+++ b/marge.py
@@ -0,0 +1,266 @@
+import re
+from errbot import BotPlugin, botcmd, webhook
+from errbot.backends import xmpp
+from errcron.bot import CrontabMixin
+import gitlab
+
+from Secret import admin_token
+git_host = "gitlab.services.zz" #TODO: move this to some sort of plugin config
+
+
+class Marge(BotPlugin, CrontabMixin):
+    """
+    I remind you about merge requests
+
+    Use: 
+       In gitlab: 
+          Add a merge request webook of the form 'https://webookserver/margeboot/<rooms>' to the projects you want tracked.  
+          <rooms> should be a comma-separated list of rooms you want notified.
+       In errbot:
+          Add <roomname>@domain to the CHATROOM_PRESENCE list in config.py for rooms margebot should join
+    """
+    
+    CRONTAB = [
+       '0 11,17 * * * .crontab_hook'    # 7:00AM and 1:00PM EST warnings
+    ]
+
+#    def __init__(self, bot):
+#        self.gitlab = None
+
+    def configure(self, configuration):
+        ## TODO: set up a connection to the gitlab API
+
+        self.gitlab = gitlab.Gitlab(git_host,admin_token,verify_ssl=False)
+
+#        notify_re = re.compile('notify_(.*)')
+
+#    def get_mr_rooms(self, project_id):
+#        """
+#        Return a list of errbot room '<roomname>@domain' names which have a 'notify_<roomname>' label on the project
+#        Log an error if you found a 'notify_<roomname>' but margebot isn't in a <roomname> room...    
+#        """
+#        retval = []
+#        labels = this.gitlab.getlabels(project_id)
+#        for a_label in labels:
+#            notify_match =  self.notify_re.search(a_label['name'])
+#            if notify_match:
+#                roomname = notify_match.group(1)
+#
+#                b_room_found = False
+#                marge_rooms = xmpp.rooms()
+#                for a_room in marge_rooms:
+#                    if a_room.node() == roomname:
+#                        retval.append(a_room.person())  # yeah rooms are people: person = node@domain
+#                        b_room_found = True
+#                if not b_room_found:
+#                    self.log.error("Label of {} found, but margebot isn't tracking that room".format(roomname))
+#                else:
+#                    retval.append(roomname)
+#        return retval
+
+    @webhook('/margebot/<rooms>/'
+    def gitlab_hook(self, request):
+        """
+        Webhook that listens on http://<server>:<port>/gitlab
+        """
+
+        # TODO: Will errbot return a json struct or not ?
+
+        # verify it's a merge request
+        if requests['object_kind'] != 'merge_request':
+           self.log.error('expecting object_kind of merge_request but got {}'.format(requests['object_kind']))
+        elif request['object_attributes']['state'] == 'opened':
+           if request['object_attributes']['work_in_progress']:
+               wip = "WIP"
+           else:
+               wip = ""
+           url = request['object_attributes']['url']
+           state = request['object_attributes']['state']
+           title = request['object_attributes']['title']
+           
+           author_id = request['object_attributes']['author_id'] # map this to user name ...
+           author = self.gitlab.getuser(author_id)
+           author_name = author['username']
+
+           target_project_id = request['object_attributes']['target_project_id']
+           iid = request['object_attributes']['iid']
+
+           user_name = request['user']['username'] # will this always be Administrator ?
+
+           message = "Reviews: {} has opened a new {} MR: {}\n{}".format(author_name, wip, title, url)
+
+           # TODO: Maybe also check the notify_<room> labels assigned to the MR as well ?
+           #mr_rooms = self.get_mr_rooms(target_project_id)
+           for a_room in rooms.split(','):
+               self.send( self.build_identifier(a_room), message)
+
+           with self.mutable('OPEN_MRS') as open_mrs:
+               open_mrs[(target_project_id,iid,rooms)] = True
+        
+        return "OK"
+
+#    def get_open_mrs(self, roomname=None, log_warnings=False):
+#        mrs = []
+#        for a_project in self.gitlab.getprojects():
+#            for a_mr in self.gitlab.getmergerequests(a_project['id'], state='opened'):
+#               rooms = self.get_mr_rooms(a_mr['target_project_id'])
+#               if len(rooms) == 0 and log_warnings:
+#                    self.log.warning('No notify room with MRs in project: {}'.format(a_mr['target_project_id']))
+#               elif not roomname:
+#                    mrs.append(a_mr)
+#               else:
+#                    for a_room in rooms:
+#                        if a_room.startswith(roomname):
+#                            mrs.append(a_mr)
+#        return mrs
+
+    def crontab_hook(self):
+        """
+        Send a scheduled message to the rooms margebot is watching about open MRs the room cares about.
+        """
+
+        reminder_msg = {}  # Map of reminder_msg['roomname@domain'] = msg
+        # initialize the reminders
+        rooms = xmpp.rooms()
+        for a_room in rooms:
+            reminder_msg[a_room.node()] = ''
+
+        still_open_mrs = {}
+
+        # Let's walk through the MRs we've seen already:
+        for (project,iid,notify_rooms) in self['OPEM_MRS']:
+
+            # Lookup the MR from the project/iid
+            a_mr = self.gitlab.getmergerequest(project, iid)
+
+            # If the MR is no longer open, skip to the next MR,
+            # and don't include this MR in the next check
+            if a_mr['state'] != 'open':
+                continue
+            else:
+                still_open_mrs[(project, iid, notify_rooms) = True
+
+            # TODO: Warn if an open MR has has conflicts (merge_status == ??)
+            # TODO: Include the count of opened MR notes (does the API show resolved state ??)
+
+            approvals = self.gitlab.getapprovals(a_mr['id'])
+            also_approved = ""
+            for approved in approvals['approved_by']:
+                also_approved += "," + approved['user']['name']
+
+            upvotes = a_mr['upvotes']
+            if upvotes >= 2:
+                msg = "\n{}: Has 2+ upvotes and could be merged in now.".format(a_mr['web_url'])
+            elif upvotes == 1:
+                msg = "\n{}: {} already approved and is waiting for another upvote.".format(a_mr['web_url'], also_approved[1:])
+            else:
+                msg = '\n{}: Has no upvotes.'.format(a_mr['web_url'])
+               
+            for a_room in notify_rooms.split(','):
+                reminder_msg[a_room] += msg
+
+        # Remind each of the rooms about open MRs
+        for a_room, room_msg in reminder_msg.iteritems():
+            if room_msg != "":
+                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))
+
+        self['OPEN_MRS'] = still_open_mrs
+
+#    def callback_message(self, mess):
+#        """
+#        Look for messages that include an URL that looks like 'https://<gitlab_server>/<group_name>/<project_name>/merge_request/<iid>'
+#        Check if gitlab mergerequest webhook already exists for this group/project, and this it reports to this room ?
+#        Add the project to the OPEN_MRS list
+#        """
+#        TODO: compiled re check against mess.body searching for 
+#              https://<gitlabe_server/(group_name)/(project_name)/merge_request/(iid)
+#         project = self.gitlab.getprojects(group_name+'%2F'+project_name)
+#         orig_mrs = self['OPEN_MRS']
+#         orig_mrs[(project['id'],iid,mess.to.node()] = True
+#         self.send(mess.to, "Another MR ! YUM !")
+#        return
+
+    @botcmd  # flags a command
+    def reviews(self, msg, args):  # a command callable with !mrs
+        """
+        Returns a list of MRs that are waiting for some luv.
+        Also returns a list of MRs that have had enough luv but aren't merged in yet.
+        """
+        sender = msg.frm
+
+        send_gitlab_id = None
+        for user in self.gitlab.getusers():
+            if user['username'] == sender.node:
+                sender_gitlab_id = user['id']
+                break
+
+        if not send_gitlab_id:
+            self.log.error('problem mapping {} to a gitlab user'.format(sender.node))
+            self.send(self.build_identifier(msg.frm), "Sorry I couldn't find your gitlab ID")
+            return
+
+        # TODO:  how to get the room the message was sent from ?  I'm assuming this will either be in msg.frm or msg.to
+        # TODO:  weed out MRs the sender opened or otherwise indicate they've opened or have already +1'd
+
+        roomname = msg.to.domain #???
+
+        # Let's walk through the MRs we've seen already:
+        msg = ""
+        still_open_mrs = {}
+        for (project,iid,notify_rooms) in self['OPEM_MRS']:
+
+            # Lookup the MR from the project/iid
+            a_mr = self.gitlab.getmergerequest(project, iid)
+
+            # If the MR is no longer open, skip to the next MR,
+            # and don't include this MR in the next check
+            if a_mr['state'] != 'open':
+                continue
+            else:
+                still_open_mrs[(project, iid, notify_rooms) = True
+
+            authored = (a_mr['author_id'] == sender_gitlab_id)
+            already_approved = False
+
+            approvals = self.gitlab.getapprovals(a_mr['id'])
+            also_approved = ""
+            for approved in approvals['approved_by']:
+               if approved['user']['id'] == sender_gitlab_id:
+                  already_approved = True
+               else:
+                   also_approved += "," + approved['user']['name']
+
+            upvotes = a_mr['upvotes']
+            if upvotes >= 2:
+                msg = "\n{}: has 2+ upvotes from {} and could be merged in now.".format(a_mr['web_url'], also_approved[1:])
+            elif upvotes == 1:
+                if not authored:
+                    if already_approved:
+                        msg = "\n{}: has already been approved by you and is waiting for another upvote.".format(a_mr['web_url'])
+                    else:
+                        msg = "\n{}: has been approved by {} and is waiting for your upvote.".format(a_mr['web_url'], also_approved[1:])
+                else:
+                    msg = "\n{}: Your MR has approved by {} and is waiting for another upvote.".format(a_mr['web_url'], also_approved[1:])
+
+            else:
+                if not authored:
+                    msg = '\n{}: Has no upvotes and needs your attention.'.format(a_mr['web_url'])
+                else:   
+                    msg = '\n{}: Your MR has no upvotes.'.format(a_mr['web_url'])
+            
+
+        if msg == "":
+            response = 'I found no open merge requests for you.'
+        else:
+            response = msg
+            
+        self.send(self.build_identifier(msg.frm), response)
+
+        self['OPEN_MRS'] = still_open_mrs
+
+        return
+
+    @botcmd()
+    def hello(self,msg, args):
+        return "Hi there"