Adding (un-unit-tested) watchrepo support.
authorJude N <juden@pwan.org>
Mon, 12 Feb 2018 05:36:17 +0000 (00:36 -0500)
committerJude N <juden@pwan.org>
Mon, 12 Feb 2018 05:36:17 +0000 (00:36 -0500)
Stubbed out some setup.py commands since they don't make sense for Errbot plugins

MANIFEST.in
README.md
plugins/LICENCE [moved from LICENCE with 100% similarity]
plugins/README.md [new file with mode: 0644]
plugins/marge.py
setup.py

index 9a92a1a..2bb1bfb 100644 (file)
@@ -1,6 +1 @@
 include plugins/*
 include plugins/*
-include setup.cfg
-include setup.py
-include templates/*
-include LICENCE
-include README.md
index fa3a9d9..70720da 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,9 +1 @@
-Marge: I remind you about outstanding Gitlab merge requests
-
-Use: 
-
-* In Gitlab:
-    * For each repo you want Margebot to watch, add a Merge Request triggered webhook in the form https://your.webhook.host/margebot/room1,room2 for haing Mrbebot send reminders to rooms room1 and room2.
-* In Errbot:
-    * Update CHATROOM\_PRESENCE list in config.py so Margebot joins all the rooms you want it to join.
-
+Marge: I remind you about outstanding Gitlab merge requests
\ No newline at end of file
similarity index 100%
rename from LICENCE
rename to plugins/LICENCE
diff --git a/plugins/README.md b/plugins/README.md
new file mode 100644 (file)
index 0000000..fa3a9d9
--- /dev/null
@@ -0,0 +1,9 @@
+Marge: I remind you about outstanding Gitlab merge requests
+
+Use: 
+
+* In Gitlab:
+    * For each repo you want Margebot to watch, add a Merge Request triggered webhook in the form https://your.webhook.host/margebot/room1,room2 for haing Mrbebot send reminders to rooms room1 and room2.
+* In Errbot:
+    * Update CHATROOM\_PRESENCE list in config.py so Margebot joins all the rooms you want it to join.
+
index 7f66423..9343bb9 100755 (executable)
@@ -1,15 +1,60 @@
 """
 Margebot: A Errbot Plugin for Gitlab MR reminders
 """
 """
 Margebot: A Errbot Plugin for Gitlab MR reminders
 """
+import re
 from datetime import datetime, timezone
 from time import sleep
 from dateutil import parser
 from dateutil.tz import tzutc
 from dateutil.relativedelta import relativedelta
 from datetime import datetime, timezone
 from time import sleep
 from dateutil import parser
 from dateutil.tz import tzutc
 from dateutil.relativedelta import relativedelta
-from errbot import BotPlugin, botcmd, re_botcmd, webhook
+from errbot import BotPlugin, botcmd, arg_botcmd, re_botcmd, webhook
 from errbot.templating import tenv
 from errcron.bot import CrontabMixin
 import gitlab
 from errbot.templating import tenv
 from errcron.bot import CrontabMixin
 import gitlab
+import requests
+
+class MargeGitlab(gitlab.Gitlab):
+    """
+    Subclass gitlab.Gitlab so extra_data args can be added
+    to the addprojecthook() and editprojecthook() methods
+    """
+
+    def __init__(self, host, token="", oauth_token="", verify_ssl=True):
+        super().__init__(host, token, oauth_token, verify_ssl)
+
+    def addprojecthook_extra(self, project_id, url, push=False, issues=False, merge_requests=False, tag_push=False, extra_data=None):
+        """
+        A copy parent addprojecthook with an extra_data field
+        """
+        data = {"id": project_id, "url": url}
+        if extra_data:
+            for ed_key, ed_value in extra_data.items():
+                data[ed_key] = ed_value
+        data['push_events'] = int(bool(push))
+        data['issues_events'] = int(bool(issues))
+        data['merge_requests_events'] = int(bool(merge_requests))
+        data['tag_push_events'] = int(bool(tag_push))
+        request = requests.post("{0}/{1}/hooks".format(self.projects_url, project_id),
+                                headers=self.headers, data=data, verify=self.verify_ssl)
+        if request.status_code == 201:
+            return request.json()
+        return False
+
+    def editprojecthook_extra(self, project_id, hook_id, url, push=False, issues=False, merge_requests=False, tag_push=False, extra_data=None):
+        """
+        A copy of the parent editprojecthook with an extra_data field
+        """
+        data = {"id": project_id, "hook_id": hook_id, "url": url}
+        if extra_data:
+            for ed_key, ed_value in extra_data.items():
+                data[ed_key] = ed_value
+        data['push_events'] = int(bool(push))
+        data['issues_events'] = int(bool(issues))
+        data['merge_requests_events'] = int(bool(merge_requests))
+        data['tag_push_events'] = int(bool(tag_push))
+        request = requests.put("{0}/{1}/hooks/{2}".format(self.projects_url, project_id, hook_id),
+                               headers=self.headers, data=data, verify=self.verify_ssl)
+        return request.status_code == 200
 
 
 def deltastr(any_delta):
 
 
 def deltastr(any_delta):
@@ -69,13 +114,15 @@ class Marge(BotPlugin, CrontabMixin):
         CRONTAB: Schedule of automated merge request checks in '%M %H %d %m %w' format
         VERIFY_SSL : True, False, or path to CA cert to verify cert
         CRONTAB_SOAK_HOURS : Don't send out reminders about MRs opened less than this many hours
         CRONTAB: Schedule of automated merge request checks in '%M %H %d %m %w' format
         VERIFY_SSL : True, False, or path to CA cert to verify cert
         CRONTAB_SOAK_HOURS : Don't send out reminders about MRs opened less than this many hours
+        WEBHOOK_URL :  URL to use for defining MR integration in gitlab
         """
         return {'GITLAB_HOST': 'gitlab.example.com',
                 'GITLAB_ADMIN_TOKEN': 'gitlab-admin-user-private-token',
                 'CHATROOM_HOST': 'conference.jabber.example.com',
                 'CRONTAB': '0 11,17 * * *',
                 'VERIFY_SSL': True,
         """
         return {'GITLAB_HOST': 'gitlab.example.com',
                 'GITLAB_ADMIN_TOKEN': 'gitlab-admin-user-private-token',
                 'CHATROOM_HOST': 'conference.jabber.example.com',
                 'CRONTAB': '0 11,17 * * *',
                 'VERIFY_SSL': True,
-                'CRONTAB_SOAK_HOURS': 1}
+                'CRONTAB_SOAK_HOURS': 1,
+                'WEBHOOK_URL': 'https://webhooks.example.com:3142/margebot/'}
 
     def check_configuration(self, configuration):
         """
 
     def check_configuration(self, configuration):
         """
@@ -95,10 +142,13 @@ class Marge(BotPlugin, CrontabMixin):
         Marge.CRONTAB = ['{} .crontab_hook'.format(self.config['CRONTAB'])]
         gitlab_auth_token = self.config['GITLAB_ADMIN_TOKEN']
         verify_ssl = self.config['VERIFY_SSL']
         Marge.CRONTAB = ['{} .crontab_hook'.format(self.config['CRONTAB'])]
         gitlab_auth_token = self.config['GITLAB_ADMIN_TOKEN']
         verify_ssl = self.config['VERIFY_SSL']
-        self.gitlab = gitlab.Gitlab(self.git_host, gitlab_auth_token, verify_ssl=verify_ssl)
+        self.gitlab = MargeGitlab(self.git_host, gitlab_auth_token, verify_ssl=verify_ssl)
         self.activate_crontab()
 
         self.soak_delta = relativedelta(hours=self.config['CRONTAB_SOAK_HOURS'])
         self.activate_crontab()
 
         self.soak_delta = relativedelta(hours=self.config['CRONTAB_SOAK_HOURS'])
+        self.webhook_url = self.config['WEBHOOK_URL']
+        if self.webhook_url[-1] != '/':
+            self.webhook_url += '/'
         super().activate()
 
     def deactivate(self):
         super().activate()
 
     def deactivate(self):
@@ -114,7 +164,7 @@ class Marge(BotPlugin, CrontabMixin):
         """
 
         self.log.info("webhook: request: {}, rooms: {}".format(request, rooms))
         """
 
         self.log.info("webhook: request: {}, rooms: {}".format(request, rooms))
-        self.log.info("state: {}".format(request['object_attributes']['state']))
+        self.log.info("state: {}".format(request['object_attributes']['state']))
 
         # verify it's a merge request
         if request['object_kind'] != 'merge_request':
 
         # verify it's a merge request
         if request['object_kind'] != 'merge_request':
@@ -360,13 +410,109 @@ class Marge(BotPlugin, CrontabMixin):
 
         return {'sender': sender, 'msg_list': msg_list}
 
 
         return {'sender': sender, 'msg_list': msg_list}
 
-    # pragma pylint: disable=unused-argument
-    @botcmd()
-    def hello(self, msg, args):
+    @arg_botcmd('rooms', type=str, help="Comma-separated room list without @conference-room suffix")
+    @arg_botcmd('repo', type=str, help="repo to start watching for MRs in NAMESPACE/PROJECT_NAME format")
+    def watchrepo(self, msg, repo, rooms):
         """
         """
-        A simple command to check if the bot is responding
+        Add the margebot webhook to a repo, and prepopulate any open MRs in the repo with margebot
         """
         """
-        return "Hi there"
+        self.log.info("msg={}".format(msg))
+        self.log.info("repo={}".format(repo))
+        self.log.info("rooms={}".format(rooms))
+
+        # get the group/repo repo, error out if it doesn't exist
+        project = self.gitlab.getproject(repo)
+        if not project:
+            msg = "Couldn't find repo {}".format(repo)
+            self.log.info("watchrepo: {}".format(msg))
+            return msg
+
+        self.log.info('project: {}'.format(project))
+
+        target_project_id = project['id']
+
+        # Check is the project already includes the margebot hook
+        # If no hooks, will it return False or [] ?
+        marge_hook = None
+        hooks = self.gitlab.getprojecthooks(target_project_id)
+        self.log.error("hooks = {}".format(hooks))
+        if hooks is False:
+            msg = "Couldn't find {} hooks".format(repo)
+            self.log.error("watchrepo: {}".format(msg))
+            return msg
+        else:
+            for a_hook in hooks:
+                self.log.info('a_hook: {}'.format(a_hook))
+                if a_hook['merge_requests_events'] and a_hook['url'].startswith(self.webhook_url):
+                    marge_hook = a_hook
+                    break
+
+        # If so replace it (or error out ?)
+        url = "{}{}".format(self.webhook_url, rooms)  # webhooks_url will end in '/'
+        if marge_hook:
+
+            old_rooms = marge_hook['url'].split(self.webhook_url, 1)[1]
+            if old_rooms == rooms:
+                msg = "Already reporting {} MRs to the {} room(s)".format(repo, rooms)
+                self.log.info('watchrepo: {}'.format(msg))
+                return msg
+            else:
+                hook_updated = self.gitlab.editprojecthook_extra(target_project_id, marge_hook['id'], url, merge_requests=True, extra_data={'enable_ssl_verification': False})
+                s_watch_msg = "Updating room list for {} MRs from {} to {}".format(repo, old_rooms, rooms)
+                s_action = "update"
+        else:
+            hook_updated = self.gitlab.addprojecthook_extra(target_project_id, url, merge_requests=True, extra_data={'enable_ssl_verification': False})
+            s_watch_msg = "Now watching for new MRs in the {} repo to the {} roomi(s)".format(repo, rooms)
+            s_action = "add"
+
+        if not hook_updated:
+            msg = "Couldn't {} hook: {}".format(s_action, repo)
+            self.log.error("watchrepo: {}".format(msg))
+            return msg
+
+        open_mrs = self['OPEN_MRS']
+        mr_count = 0
+        # get the open MRs in the repo
+
+        # If updating the room list, walk through the existing MR list
+        if s_action == "update":
+            new_open_mrs = {}
+            for (project_id, mr_id, old_rooms) in open_mrs:
+                # pragma pylint: disable=simplifiable-if-statement
+                if project_id == target_project_id:
+                    new_open_mrs[(project_id, mr_id, rooms)] = True
+                else:
+                    new_open_mrs[(project_id, mr_id, old_rooms)] = True
+                # pragma pylint: enable=simplifiable-if-statement
+            open_mrs = new_open_mrs
+
+        # If adding a new repo, check for existing opened/reopened MRs in the repo.
+        else:
+            for state in ['opened', 'reopened']:
+                page = 1
+                mr_list = self.gitlab.getmergerequests(target_project_id, page=page, per_page=100, state=state)
+                while (mr_list is not False) and (mr_list != []):
+                    for an_mr in mr_list:
+                        mr_count += 1
+                        self.log.info('watchrepo: an_mr WATS THE ID\n{}'.format(an_mr))
+                        mr_id = an_mr['id']
+                        open_mrs[(target_project_id, mr_id, rooms)] = True
+                    # Get the next page of MRs
+                    page += 1
+                    mr_list = self.gitlab.getmergerequests(target_project_id, page=page, per_page=100)
+
+        self['OPEN_MRS'] = open_mrs
+
+        if mr_count == 0:
+            mr_msg = "No open MRs were found in the repo."
+        elif mr_count == 1:
+            mr_msg = "1 open MR was found in the repo.  Run !reviews to see the updated MR list."
+        else:
+            mr_msg = "{} open MRs were found in the repo.  Run !reviews to see the updated MR list."
+
+        return "{}\n{}".format(s_watch_msg, mr_msg)
+
+    # pragma pylint: disable=unused-argument
 
     @botcmd()
     def xyzzy(self, msg, args):
 
     @botcmd()
     def xyzzy(self, msg, args):
@@ -377,18 +523,40 @@ class Marge(BotPlugin, CrontabMixin):
         sleep(5)
         yield "(just kidding)"
 
         sleep(5)
         yield "(just kidding)"
 
-    @re_botcmd(pattern=r"I blame [Mm]arge([Bb]ot)?")
+    @re_botcmd(pattern=r"I blame marge(bot)?", prefixed=False, flags=re.IGNORECASE)
     def dont_blame_margebot(self, msg, match):
         """
         margebot is innocent.
         """
     def dont_blame_margebot(self, msg, match):
         """
         margebot is innocent.
         """
-        yield "(」゚ロ゚)」NOOOooooo say it ain't so."
+        yield u"(\u300D\uFF9F\uFF9B\uFF9F)\uFF63NOOOooooo say it ain't so."
 
 
-    @re_botcmd(pattern=r"good bot")
+    @re_botcmd(pattern=r"good bot", prefixed=False, flags=re.IGNORECASE)
     def best_bot(self, msg, match):
         """
         margebot is the best.
         """
         yield "Best bot"
 
     def best_bot(self, msg, match):
         """
         margebot is the best.
         """
         yield "Best bot"
 
+    @re_botcmd(pattern=r"magfest", prefixed=False, flags=re.IGNORECASE)
+    def margefest(self, msg, args):
+        """
+        margefest4ever
+        """
+        return "More like MargeFest, amirite ?"
+
+    @re_botcmd(pattern=r"margebot sucks", prefixed=False, flags=re.IGNORECASE)
+    def bring_it_up_with_the_steering_committee(self, msg, args):
+        """
+        Bring it up with the committee
+        """
+        return "Bring it up with the Margebot steering committee."
+
+    @re_botcmd(pattern=r".*", prefixed=True)
+    def catchall(self, msg,args):
+       """
+       Don't have the bot complain about unknown commands if the first word in a msg is its name
+       """
+       return
+
+
     # pragma pylint: enable=unused-argument
     # pragma pylint: enable=unused-argument
index 7a5bb92..06eac90 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,39 @@
 Setup.py
 """
 # pragma pylint: disable=invalid-name
 Setup.py
 """
 # pragma pylint: disable=invalid-name
+from distutils.errors import DistutilsSetupError
+from distutils.filelist import FileList
+import os
 from setuptools import setup
 from setuptools import setup
+import setuptools.command.sdist
+import setuptools.command.upload
+import setuptools.command.install
+
+# Errbot plugins aren't distributed as eggs so stub out the sdist, install and upload commands
+
+class ErrbotSdistCommand(setuptools.command.sdist.sdist):
+    """
+    No sdist command for now.
+    """
+    def run(self):
+        raise DistutilsSetupError("No sdist for Errbot plugins.")
+
+
+class ErrbotUploadCommand(setuptools.command.upload.upload):
+    """
+    Don't try uploading Errbot plugins to pypi
+    """
+    def run(self):
+        raise DistutilsSetupError("No uploading Errbot plugins to pypi.")
+
+
+class ErrbotInstallCommand(setuptools.command.install.install):
+    """
+    Short circuit the install command
+    """
+    def run(self): 
+        raise DistutilsSetupError("No install command - copy the ./plugin files to a 'Marge' directory under your Errbot's plugins directory.")
+
 
 with open('requirements.txt') as f:
     install_required = f.read().splitlines()
 
 with open('requirements.txt') as f:
     install_required = f.read().splitlines()
@@ -11,25 +43,24 @@ with open('test-requirements.txt') as f:
     tests_required = f.read().splitlines()
 
 setup(
     tests_required = f.read().splitlines()
 
 setup(
-    name='Margebot',
+    name='Marge',
     version='1.0.0',
     version='1.0.0',
-    # packages=['plugins'],
-    data_files=[('/opt/errbot/plugins/Marge',
-                 ['./plugins/marge.plug',
-                  './plugins/marge.py',
-                  './plugins//templates/reviews.md',
-                  'README.md',
-                  'LICENCE'])],
     license='GPLv3',
     description='A Errbot plugin for reminding you about outstanding Gitlab merge requests',
     long_description=open('README.md').read(),
     url='https://pwan.org/git/?p=margebot.git;a=summary',
     author='JudeN',
     author_email='margebot_spam@pwan.org',
     license='GPLv3',
     description='A Errbot plugin for reminding you about outstanding Gitlab merge requests',
     long_description=open('README.md').read(),
     url='https://pwan.org/git/?p=margebot.git;a=summary',
     author='JudeN',
     author_email='margebot_spam@pwan.org',
-    include_package_data=True,
 
 
+    include_package_data=True,
     install_requires=install_required,
 
     setup_requires=['pytest-runner'],
     tests_require=tests_required,
     install_requires=install_required,
 
     setup_requires=['pytest-runner'],
     tests_require=tests_required,
+
+    cmdclass={
+        'install' : ErrbotInstallCommand,
+        'sdist' : ErrbotSdistCommand,
+        'upload' : ErrbotUploadCommand,
+    }
 )
 )