Ejemplo n.º 1
0
 def __init__(self, *args, **kwargs):
     super(PrometheusPlugin, self).__init__(*args, **kwargs)
     self.store = KeyValueStore("prometheus.json")
     self.rooms = RoomContextStore(
         [PrometheusPlugin.TYPE_TRACK]
     )
     self.queue_counter = 1L
     self.consumer = MessageConsumer(self.matrix)
     self.consumer.daemon = True
     self.consumer.start()
Ejemplo n.º 2
0
    def __init__(self, *args, **kwargs):
        super(JenkinsPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jenkins.json")
        self.rooms = RoomContextStore([JenkinsPlugin.TYPE_TRACK])

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        self.failed_builds = {
            # projectName:branch: { commit:x }
        }
Ejemplo n.º 3
0
    def __init__(self, *args, **kwargs):
        super(GithubPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("github.json")
        self.rooms = RoomContextStore([GithubPlugin.TYPE_TRACK])

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        if not self.store.has("github_access_token"):
            log.info(
                "A github access_token is required to create github issues.")
            log.info("Issues will be created as this user.")
            token = input("(Optional) Github token: ").strip()
            if token:
                self.store.set("github_access_token", token)
            else:
                log.info("You will not be able to create Github issues.")
Ejemplo n.º 4
0
    def __init__(self, *args, **kwargs):
        super(JiraPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jira.json")
        self.rooms = RoomContextStore(
            [JiraPlugin.TYPE_TRACK, JiraPlugin.TYPE_EXPAND])

        if not self.store.has("url"):
            url = raw_input("JIRA URL: ").strip()
            self.store.set("url", url)

        if not self.store.has("user") or not self.store.has("pass"):
            user = raw_input("(%s) JIRA Username: "******"url")).strip()
            pw = getpass.getpass("(%s) JIRA Password: "******"url")).strip()
            self.store.set("user", user)
            self.store.set("pass", pw)

        self.auth = (self.store.get("user"), self.store.get("pass"))
        self.regex = re.compile(r"\b(([A-Za-z]+)-\d+)\b")
Ejemplo n.º 5
0
    def __init__(self, *args, **kwargs):
        super(GithubPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("github.json")
        self.rooms = RoomContextStore(
            [GithubPlugin.TYPE_TRACK]
        )

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")
Ejemplo n.º 6
0
class PrometheusPlugin(Plugin):
    """Plugin for interacting with Prometheus.
    """
    name = "prometheus"

    #Webhooks:
        #    /neb/prometheus
    TYPE_TRACK = "org.matrix.neb.plugin.prometheus.projects.tracking"


    def __init__(self, *args, **kwargs):
        super(PrometheusPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("prometheus.json")
        self.rooms = RoomContextStore(
            [PrometheusPlugin.TYPE_TRACK]
        )
        self.queue_counter = 1L
        self.consumer = MessageConsumer(self.matrix)
        self.consumer.daemon = True
        self.consumer.start()

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_sync(self, sync):
        log.debug("Plugin: Prometheus sync state:")
        self.rooms.init_from_sync(sync)

    def get_webhook_key(self):
        return "prometheus"

    def on_receive_webhook(self, url, data, ip, headers):
        json_data = json.loads(data)
        log.info("recv %s", json_data)
        template = Template(self.store.get("message_template"))
        for alert in json_data.get("alert", []):
            for room_id in self.rooms.get_room_ids():
                log.debug("queued message for room " + room_id + " at " + str(self.queue_counter) + ": %s", alert)
                queue.put((self.queue_counter, room_id, template.render(alert)))
                self.queue_counter += 1
Ejemplo n.º 7
0
    def __init__(self, *args, **kwargs):
        super(JenkinsPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jenkins.json")
        self.rooms = RoomContextStore(
            [JenkinsPlugin.TYPE_TRACK]
        )

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        self.failed_builds = {
            # projectName:branch: { commit:x }
        }
Ejemplo n.º 8
0
    def __init__(self, *args, **kwargs):
        super(JiraPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jira.json")
        self.rooms = RoomContextStore(
            [JiraPlugin.TYPE_TRACK, JiraPlugin.TYPE_EXPAND]
        )

        if not self.store.has("url"):
            url = raw_input("JIRA URL: ").strip()
            self.store.set("url", url)

        if not self.store.has("user") or not self.store.has("pass"):
            user = raw_input("(%s) JIRA Username: "******"url")).strip()
            pw = getpass.getpass("(%s) JIRA Password: "******"url")).strip()
            self.store.set("user", user)
            self.store.set("pass", pw)

        self.auth = (self.store.get("user"), self.store.get("pass"))
        self.regex = re.compile(r"\b(([A-Za-z]+)-\d+)\b")
Ejemplo n.º 9
0
    def __init__(self, *args, **kwargs):
        super(GithubPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("github.json")
        self.rooms = RoomContextStore(
            [GithubPlugin.TYPE_TRACK]
        )

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        if not self.store.has("github_access_token"):
            log.info("A github access_token is required to create github issues.")
            log.info("Issues will be created as this user.")
            token = raw_input("(Optional) Github token: ").strip()
            if token:
                self.store.set("github_access_token", token)
            else:
                log.info("You will not be able to create Github issues.")
Ejemplo n.º 10
0
class GithubPlugin(Plugin):
    """Plugin for interacting with Github.
    github show projects : Show which github projects this bot recognises.
    github show track|tracking : Show which projects are being tracked.
    github track "owner/repo" "owner/repo" : Track the given projects.
    github add owner/repo : Add the given repo to the tracking list.
    github remove owner/repo : Remove the given repo from the tracking list.
    github stop track|tracking : Stop tracking github projects.
    github create owner/repo "Bug title" "Bug desc" : Create an issue on Github.
    github label add|remove owner/repo issue# label : Label an issue on Github.
    """
    name = "github"
    # New events:
    #    Type: org.matrix.neb.plugin.github.projects.tracking
    #    State: Yes
    #    Content: {
    #        projects: [projectName1, projectName2, ...]
    #    }

    # Webhooks:
    #    /neb/github
    TYPE_TRACK = "org.matrix.neb.plugin.github.projects.tracking"
    TYPE_COLOR = "org.matrix.neb.plugin.github.projects.color"

    TRACKING = ["track", "tracking"]

    def __init__(self, *args, **kwargs):
        super(GithubPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("github.json")
        self.rooms = RoomContextStore([GithubPlugin.TYPE_TRACK])

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        if not self.store.has("github_access_token"):
            log.info(
                "A github access_token is required to create github issues.")
            log.info("Issues will be created as this user.")
            token = input("(Optional) Github token: ").strip()
            if token:
                self.store.set("github_access_token", token)
            else:
                log.info("You will not be able to create Github issues.")

    def on_receive_github_push(self, info):
        log.info("recv %s", info)

        # add the project if we didn't know about it before
        if info["repo"] not in self.store.get("known_projects"):
            log.info("Added new repo: %s", info["repo"])
            projects = self.store.get("known_projects")
            projects.append(info["repo"])
            self.store.set("known_projects", projects)

        if info["type"] == "delete":
            push_message = '[<u>%s</u>] %s <font color="red"><b>deleted</font> %s</b>' % (
                info["repo"], info["commit_username"], info["branch"])
        elif info["type"] == "commit":
            # form the template:
            # [<repo>] <username> pushed <num> commits to <branch>: <git.io link>
            # 1<=3 of <branch name> <short hash> <full username>: <comment>
            if info["num_commits"] == 1:
                push_message = "[<u>%s</u>] %s pushed to <b>%s</b>: %s  - %s" % (
                    info["repo"], info["commit_username"], info["branch"],
                    info["commit_msg"], info["commit_link"])
            else:
                summary = ""
                max_commits = 3
                count = 0
                for c in info["commits_summary"]:
                    if count == max_commits:
                        break
                    summary += "\n%s: %s" % (c["author"], c["summary"])
                    count += 1

                push_message = "[<u>%s</u>] %s pushed %s commits to <b>%s</b>: %s %s" % (
                    info["repo"], info["commit_username"], info["num_commits"],
                    info["branch"], info["commit_link"], summary)
        else:
            log.warning("Unknown push type. %s", info["type"])
            return

        self.send_message_to_repos(info["repo"], push_message)

    def send_message_to_repos(self, repo, push_message):
        # send messages to all rooms registered with this project.
        for room_id in self.rooms.get_room_ids():
            try:
                if repo in self.rooms.get_content(
                        room_id, GithubPlugin.TYPE_TRACK)["projects"]:
                    self.matrix.send_message_event(
                        room_id, "m.room.message",
                        self.matrix.get_html_body(push_message,
                                                  msgtype="m.notice"))
            except KeyError:
                pass

    def cmd_show(self, event, action):
        """Show information on projects or projects being tracked.
        Show which projects are being tracked. 'github show tracking'
        Show which proejcts are recognised so they could be tracked. 'github show projects'
        """
        if action == "projects":
            projects = self.store.get("known_projects")
            return "Available projects: %s - To add more projects, you must register a webhook on Github." % json.dumps(
                projects)
        elif action in self.TRACKING:
            return self._get_tracking(event["room_id"])
        else:
            return self.cmd_show.__doc__

    @admin_only
    def cmd_add(self, event, repo):
        """Add a repo for tracking. 'github add owner/repo'"""
        if repo not in self.store.get("known_projects"):
            return "Unknown project name: %s." % repo

        try:
            room_repos = self.rooms.get_content(
                event["room_id"], GithubPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_repos = []

        if repo in room_repos:
            return "%s is already being tracked." % repo

        room_repos.append(repo)
        self._send_track_event(event["room_id"], room_repos)

        return "Added %s. Commits for projects %s will be displayed as they are commited." % (
            repo, room_repos)

    @admin_only
    def cmd_remove(self, event, repo):
        """Remove a repo from tracking. 'github remove owner/repo'"""
        try:
            room_repos = self.rooms.get_content(
                event["room_id"], GithubPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_repos = []

        if repo not in room_repos:
            return "Cannot remove %s : It isn't being tracked." % repo

        room_repos.remove(repo)
        self._send_track_event(event["room_id"], room_repos)

        return "Removed %s. Commits for projects %s will be displayed as they are commited." % (
            repo, room_repos)

    @admin_only
    def cmd_track(self, event, *args):
        if len(args) == 0:
            return self._get_tracking(event["room_id"])

        for project in args:
            if project not in self.store.get("known_projects"):
                return "Unknown project name: %s." % project

        self._send_track_event(event["room_id"], args)

        return "Commits for projects %s will be displayed as they are commited." % (
            args, )

    @admin_only
    def cmd_stop(self, event, action):
        """Stop tracking projects. 'github stop tracking'"""
        if action in self.TRACKING:
            self._send_track_event(event["room_id"], [])
            return "Stopped tracking projects."
        else:
            return self.cmd_stop.__doc__

    @admin_only
    def cmd_create(self, event, *args):
        """Create a new issue. Format: 'create <owner/repo> <title> <desc(optional)>'
        E.g. 'create matrix-org/synapse A bug goes here
        'create matrix-org/synapse "Title here" "desc here" """
        if not args or len(args) < 2:
            return self.cmd_create.__doc__
        project = args[0]
        others = args[1:]
        # others must contain a title, may contain a description. If it contains
        # a description, it MUST be in [1] and be longer than 1 word.
        title = ' '.join(others)
        desc = ""
        try:
            possible_desc = others[1]
            if ' ' in possible_desc:
                desc = possible_desc
                title = others[0]
        except Exception as e:
            log.error(e)

        return self._create_issue(event["user_id"], project, title, desc)

    @admin_only
    def cmd_label_remove(self, event, repo, issue_num, *args):
        """Remove a label on an issue. Format: 'label remove <owner/repo> <issue num> <label> <label> <label>'
        E.g. 'label remove matrix-org/synapse 323 bug p2 blocked'
        """
        e = self._is_valid_issue_request(repo, issue_num)
        if e:
            return e
        if len(args) == 0:
            return "You must specify at least one label."

        errs = []

        for label in args:
            url = "https://api.github.com/repos/%s/issues/%s/labels/%s" % (
                repo, issue_num, label)
            res = requests.delete(url,
                                  headers={
                                      "Authorization":
                                      "token %s" %
                                      self.store.get("github_access_token")
                                  })
            if res.status_code < 200 or res.status_code >= 300:
                errs.append("Problem removing label %s : HTTP %s" %
                            (label, res.status_code))
                return errs

        if len(errs) == 0:
            return "Removed labels %s" % (json.dumps(args), )
        else:
            return "There was a problem removing some labels:\n" + "\n".join(
                errs)

    @admin_only
    def cmd_label_add(self, event, repo, issue_num, *args):
        """Label an issue. Format: 'label add <owner/repo> <issue num> <label> <label> <label>'
        E.g. 'label add matrix-org/synapse 323 bug p2 blocked'
        """
        e = self._is_valid_issue_request(repo, issue_num)
        if e:
            return e
        if len(args) == 0:
            return "You must specify at least one label."

        url = "https://api.github.com/repos/%s/issues/%s/labels" % (repo,
                                                                    issue_num)
        res = requests.post(url,
                            data=json.dumps(args),
                            headers={
                                "Content-Type":
                                "application/json",
                                "Authorization":
                                "token %s" %
                                self.store.get("github_access_token")
                            })
        if res.status_code < 200 or res.status_code >= 300:
            err = "%s Failed: HTTP %s" % (
                url,
                res.status_code,
            )
            log.error(err)
            return err

        return "Added labels %s" % (json.dumps(args), )

    def _create_issue(self, user_id, project, title, desc=""):
        if not self.store.has("github_access_token"):
            return "This plugin isn't configured to create Github issues."

        # Add a space after the @ to avoid pinging people on Github!
        user_id = user_id.replace("@", "@ ")

        desc = "Created by %s.\n\n%s" % (user_id, desc)
        info = {"title": title, "body": desc}

        url = "https://api.github.com/repos/%s/issues" % project
        res = requests.post(url,
                            data=json.dumps(info),
                            headers={
                                "Content-Type":
                                "application/json",
                                "Authorization":
                                "token %s" %
                                self.store.get("github_access_token")
                            })
        if res.status_code < 200 or res.status_code >= 300:
            err = "%s Failed: HTTP %s" % (
                url,
                res.status_code,
            )
            log.error(err)
            return err

        response = json.loads(res.text)
        return "Created issue: %s" % response["html_url"]

    def _is_valid_issue_request(self, repo, issue_num):
        try:
            issue_is_num = int(issue_num)
        except ValueError:
            issue_is_num = False

        if "/" not in repo:
            return "Repo must be in the form 'owner/repo' e.g. 'matrix-org/synapse'."

        if not issue_is_num:
            return "Issue number must be a number"

        if not self.store.has("github_access_token"):
            return "This plugin isn't configured to interact with Github issues."

    def _send_track_event(self, room_id, project_names):
        self.matrix.send_state_event(room_id, self.TYPE_TRACK,
                                     {"projects": project_names})

    def _get_tracking(self, room_id):
        try:
            return ("Currently tracking %s" % json.dumps(
                self.rooms.get_content(room_id,
                                       GithubPlugin.TYPE_TRACK)["projects"]))
        except KeyError:
            return "Not tracking any projects currently."

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_sync(self, sync):
        log.debug("Plugin: Github sync state:")
        self.rooms.init_from_sync(sync)

    def get_webhook_key(self):
        return "github"

    def on_receive_pull_request(self, data):
        action = data["action"]
        pull_req_num = data["number"]
        repo_name = data["repository"]["full_name"]
        pr = data["pull_request"]
        pr_url = pr["html_url"]
        pr_state = pr["state"]
        pr_title = pr["title"]

        user = data["sender"]["login"]

        action_target = ""
        if pr.get("assignee") and pr["assignee"].get("login"):
            action_target = " to %s" % (pr["assignee"]["login"], )

        msg = "[<u>%s</u>] %s %s <b>pull request #%s</b>: %s [%s]%s - %s" % (
            repo_name, user, action, pull_req_num, pr_title, pr_state,
            action_target, pr_url)

        self.send_message_to_repos(repo_name, msg)

    def on_receive_create(self, data):
        if data["ref_type"] != "branch":
            return  # only echo branch creations for now.

        branch_name = data["ref"]
        user = data["sender"]["login"]
        repo_name = data["repository"]["full_name"]

        msg = '[<u>%s</u>] %s <font color="green">created</font> a new branch: <b>%s</b>' % (
            repo_name, user, branch_name)

        self.send_message_to_repos(repo_name, msg)

    def on_receive_ping(self, data):
        repo_name = data.get("repository", {}).get("full_name")
        # add the project if we didn't know about it before
        if repo_name and repo_name not in self.store.get("known_projects"):
            log.info("Added new repo: %s", repo_name)
            projects = self.store.get("known_projects")
            projects.append(repo_name)
            self.store.set("known_projects", projects)

    def on_receive_comment(self, data):
        repo_name = data["repository"]["full_name"]
        issue = data["issue"]
        comment = data["comment"]
        is_pull_request = "pull_request" in issue
        if not is_pull_request:
            return  # don't bother displaying issue comments

        pr_title = issue["title"]
        pr_num = issue["number"]
        pr_username = issue["user"]["login"]
        comment_url = comment["html_url"]
        username = comment["user"]["login"]

        msg = "[<u>%s</u>] %s commented on %s's <b>pull request #%s</b>: %s - %s" % (
            repo_name, username, pr_username, pr_num, pr_title, comment_url)
        self.send_message_to_repos(repo_name, msg)

    def on_receive_pull_request_comment(self, data):
        repo_name = data["repository"]["full_name"]
        username = data["sender"]["login"]
        pull_request = data["pull_request"]
        pr_username = pull_request["user"]["login"]
        pr_num = pull_request["number"]
        assignee = "None"
        if data["pull_request"].get("assignee"):
            assignee = data["pull_request"]["assignee"]["login"]
        pr_title = pull_request["title"]
        comment_url = data["comment"]["html_url"]

        msg = "[<u>%s</u>] %s made a line comment on %s's <b>pull request #%s</b> (assignee: %s): %s - %s" % (
            repo_name, username, pr_username, pr_num, assignee, pr_title,
            comment_url)
        self.send_message_to_repos(repo_name, msg)

    def on_receive_issue(self, data):
        action = data["action"]
        repo_name = data["repository"]["full_name"]
        issue = data["issue"]
        title = issue["title"]
        issue_num = issue["number"]
        url = issue["html_url"]

        user = data["sender"]["login"]

        if action == "assigned":
            try:
                assignee = data["assignee"]["login"]
                msg = "[<u>%s</u>] %s assigned issue #%s to %s: %s - %s" % (
                    repo_name, user, issue_num, assignee, title, url)
                self.send_message_to_repos(repo_name, msg)
                return
            except Exception as e:
                print(e)

        msg = "[<u>%s</u>] %s %s issue #%s: %s - %s" % (
            repo_name, user, action, issue_num, title, url)

        self.send_message_to_repos(repo_name, msg)

    def on_receive_webhook(self, url, data, ip, headers):
        if self.store.get("secret_token"):
            token_sha1 = headers.get('X-Hub-Signature')
            payload_body = data
            calc = hmac.new(str(self.store.get("secret_token")), payload_body,
                            sha1)
            calc_sha1 = "sha1=" + calc.hexdigest()
            if token_sha1 != calc_sha1:
                log.warning("GithubWebServer: FAILED SECRET TOKEN AUTH. IP=%s",
                            ip)
                return "", 403, {}

        json_data = json.loads(data)
        is_private_repo = json_data.get("repository", {}).get("private")
        if is_private_repo:
            log.info("Received private repo event for %s",
                     json_data["repository"].get("name"))
            return

        event_type = headers.get('X-GitHub-Event')
        if event_type == "pull_request":
            self.on_receive_pull_request(json_data)
            return
        elif event_type == "issues":
            self.on_receive_issue(json_data)
            return
        elif event_type == "create":
            self.on_receive_create(json_data)
            return
        elif event_type == "ping":
            self.on_receive_ping(json_data)
            return
        elif event_type == "issue_comment":
            # INCLUDES PR COMMENTS!!!
            # But not line comments!
            self.on_receive_comment(json_data)
            return
        elif event_type == "pull_request_review_comment":
            self.on_receive_pull_request_comment(json_data)
            return

        j = json_data
        repo_name = j["repository"]["full_name"]
        # strip 'refs/heads' from 'refs/heads/branch_name'
        branch = '/'.join(j["ref"].split('/')[2:])

        commit_msg = ""
        commit_name = ""
        commit_link = ""
        short_hash = ""
        push_type = "commit"

        if j["head_commit"]:
            commit_msg = j["head_commit"]["message"]
            commit_name = j["head_commit"]["committer"]["name"]
            commit_link = j["head_commit"]["url"]
            # short hash please
            short_hash = commit_link.split('/')[-1][0:8]
            commit_link = '/'.join(
                commit_link.split('/')[0:-1]) + "/" + short_hash
        elif j["deleted"]:
            # looks like this branch was deleted, no commit and deleted=true
            commit_name = j["pusher"]["name"]
            push_type = "delete"

        try:
            commit_uname = j["head_commit"]["committer"]["username"]
        except Exception:
            # possible if they haven't tied up with a github account
            commit_uname = commit_name

        # look for multiple commits
        num_commits = 1
        commits_summary = []
        if "commits" in j and len(j["commits"]) > 1:
            num_commits = len(j["commits"])
            for c in j["commits"]:
                try:
                    cname = c["author"]["username"]
                except Exception:
                    cname = c["author"]["name"]
                commits_summary.append({
                    "author": cname,
                    "summary": c["message"]
                })

        self.on_receive_github_push({
            "branch": branch,
            "repo": repo_name,
            "commit_msg": commit_msg,
            "commit_username": commit_uname,
            "commit_name": commit_name,
            "commit_link": commit_link,
            "commit_hash": short_hash,
            "type": push_type,
            "num_commits": num_commits,
            "commits_summary": commits_summary
        })
Ejemplo n.º 11
0
class JiraPlugin(Plugin):
    """ Plugin for interacting with JIRA.
    jira version : Display version information for this platform.
    jira track <project> <project2> ... : Track multiple projects
    jira expand <project> <project2> ... : Expand issue IDs for the given projects with issue information.
    jira stop track|tracking : Stops tracking for all projects.
    jira stop expand|expansion|expanding : Stop expanding jira issues.
    jira show track|tracking : Show which projects are being tracked.
    jira show expansion|expand|expanding : Show which project keys will result in issue expansion.
    jira create <project> <priority> <title> <desc> : Create a new JIRA issue.
    jira comment <issue-id> <comment> : Comment on a JIRA issue.
    """
    name = "jira"

    TRACK = ["track", "tracking"]
    EXPAND = ["expansion", "expand", "expanding"]

    # New events:
    #    Type: org.matrix.neb.plugin.jira.issues.tracking / expanding
    #    State: Yes
    #    Content: {
    #        projects: [projectKey1, projectKey2, ...]
    #    }
    TYPE_TRACK = "org.matrix.neb.plugin.jira.issues.tracking"
    TYPE_EXPAND = "org.matrix.neb.plugin.jira.issues.expanding"

    def __init__(self, *args, **kwargs):
        super(JiraPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jira.json")
        self.rooms = RoomContextStore(
            [JiraPlugin.TYPE_TRACK, JiraPlugin.TYPE_EXPAND]
        )

        if not self.store.has("url"):
            url = raw_input("JIRA URL: ").strip()
            self.store.set("url", url)

        if not self.store.has("user") or not self.store.has("pass"):
            user = raw_input("(%s) JIRA Username: "******"url")).strip()
            pw = getpass.getpass("(%s) JIRA Password: "******"url")).strip()
            self.store.set("user", user)
            self.store.set("pass", pw)

        self.auth = (self.store.get("user"), self.store.get("pass"))
        self.regex = re.compile(r"\b(([A-Za-z]+)-\d+)\b")

    @admin_only
    def cmd_stop(self, event, action):
        """ Clear project keys from tracking/expanding.
        Stop tracking projects. 'jira stop tracking'
        Stop expanding projects. 'jira stop expanding'
        """
        if action in self.TRACK:
            self._send_state(JiraPlugin.TYPE_TRACK, event["room_id"], [])
            url = self.store.get("url")
            return "Stopped tracking project keys from %s." % (url)
        elif action in self.EXPAND:
            self._send_state(JiraPlugin.TYPE_EXPAND, event["room_id"], [])
            url = self.store.get("url")
            return "Stopped expanding project keys from %s." % (url)
        else:
            return "Invalid arg '%s'.\n %s" % (action, self.cmd_stop.__doc__)

    @admin_only
    def cmd_track(self, event, *args):
        """Track project keys. 'jira track FOO BAR'"""
        if not args:
            return self._get_tracking(event["room_id"])

        args = [k.upper() for k in args]
        for key in args:
            if re.search("[^A-Z]", key):  # something not A-Z
                return "Key %s isn't a valid project key." % key

        self._send_state(JiraPlugin.TYPE_TRACK, event["room_id"], args)

        url = self.store.get("url")
        return "Issues for projects %s from %s will be displayed as they are updated." % (args, url)

    @admin_only
    def cmd_expand(self, event, *args):
        """Expand issues when mentioned for the given project keys. 'jira expand FOO BAR'"""
        if not args:
            return self._get_expanding(event["room_id"])

        args = [k.upper() for k in args]
        for key in args:
            if re.search("[^A-Z]", key):  # something not A-Z
                return "Key %s isn't a valid project key." % key

        self._send_state(JiraPlugin.TYPE_EXPAND, event["room_id"], args)

        url = self.store.get("url")
        return "Issues for projects %s from %s will be expanded as they are mentioned." % (args, url)

    @admin_only
    def cmd_create(self, event, *args):
        """Create a new issue. Format: 'create <project> <priority(optional;default 3)> <title> <desc(optional)>'
        E.g. 'create syn p1 This is the title without quote marks'
        'create syn p1 "Title here" "desc here"
        """
        if not args or len(args) < 2:
            return self.cmd_create.__doc__
        project = args[0]
        priority = 3
        others = args[1:]
        if re.match("[Pp][0-9]", args[1]):
            if len(args) < 3:  # priority without title
                return self.cmd_create.__doc__
            try:
                priority = int(args[1][1:])
                others = args[2:]
            except ValueError:
                return self.cmd_create.__doc__
        elif re.match("[Pp][0-9]", args[0]):
            priority = int(args[0][1:])
            project = args[1]
            others = args[2:]
        # others must contain a title, may contain a description. If it contains
        # a description, it MUST be in [1] and be longer than 1 word.
        title = ' '.join(others)
        desc = ""
        try:
            possible_desc = others[1]
            if ' ' in possible_desc:
                desc = possible_desc
                title = others[0]
        except:
            pass

        return self._create_issue(
            event["user_id"], project, priority, title, desc
        )

    @admin_only
    def cmd_comment(self, event, *args):
        """Comment on an issue. Format: 'comment <key> <comment text>'
        E.g. 'comment syn-56 A comment goes here'
        """
        if not args or len(args) < 2:
            return self.cmd_comment.__doc__
        key = args[0].upper()
        text = ' '.join(args[1:])
        return self._comment_issue(event["user_id"], key, text)

    def cmd_version(self, event):
        """Display version information for the configured JIRA platform. 'jira version'"""
        url = self._url("/rest/api/2/serverInfo")
        response = json.loads(requests.get(url).text)

        info = "%s : version %s : build %s" % (response["serverTitle"],
               response["version"], response["buildNumber"])

        return info

    def cmd_show(self, event, action):
        """Show which project keys are being tracked/expanded.
        Show which project keys are being expanded. 'jira show expanding'
        Show which project keys are being tracked. 'jira show tracking'
        """
        action = action.lower()
        if action in self.TRACK:
            return self._get_tracking(event["room_id"])
        elif action in self.EXPAND:
            return self._get_expanding(event["room_id"])

    def _get_tracking(self, room_id):
        try:
            return ("Currently tracking %s" %
                json.dumps(
                    self.rooms.get_content(
                        room_id,
                        JiraPlugin.TYPE_TRACK
                    )["projects"]
                )
            )
        except KeyError:
            return "Not tracking any projects currently."

    def _get_expanding(self, room_id):
        try:
            return ("Currently expanding %s" %
                json.dumps(
                    self.rooms.get_content(
                        room_id,
                        JiraPlugin.TYPE_EXPAND
                    )["projects"]
                )
            )
        except KeyError:
            return "Not expanding any projects currently."

    def _send_state(self, etype, room_id, project_keys):
        self.matrix.send_state_event(
            room_id,
            etype,
            {
                "projects": project_keys
            }
        )

    def on_msg(self, event, body):
        room_id = event["room_id"]
        body = body.upper()
        groups = self.regex.findall(body)
        if not groups:
            return

        projects = []
        try:
            projects = self.rooms.get_content(
                    room_id, JiraPlugin.TYPE_EXPAND
                )["projects"]
        except KeyError:
            return

        for (key, project) in groups:
            if project in projects:
                try:
                    issue_info = self._get_issue_info(key)
                    if issue_info:
                        self.matrix.send_message(
                            event["room_id"],
                            issue_info,
                            msgtype="m.notice"
                        )
                except Exception as e:
                    log.exception(e)

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_receive_jira_push(self, info):
        log.debug("on_recv %s", info)
        project = self.regex.match(info["key"]).groups()[1]

        # form the message
        link = self._linkify(info["key"])
        push_message = "%s %s <b>%s</b> - %s %s" % (info["user"], info["action"],
                       info["key"], info["summary"], link)

        # send messages to all rooms registered with this project.
        for room_id in self.rooms.get_room_ids():
            try:
                content = self.rooms.get_content(room_id, JiraPlugin.TYPE_TRACK)
                if project in content["projects"]:
                    self.matrix.send_message_event(
                        room_id,
                        "m.room.message",
                        self.matrix.get_html_body(push_message, msgtype="m.notice")
                    )
            except KeyError:
                pass

    def on_sync(self, sync):
        log.debug("Plugin: JIRA sync state:")
        self.rooms.init_from_sync(sync)

    def _get_issue_info(self, issue_key):
        url = self._url("/rest/api/2/issue/%s" % issue_key)
        res = requests.get(url, auth=self.auth)
        if res.status_code != 200:
            return

        response = json.loads(res.text)
        link = self._linkify(issue_key)
        desc = response["fields"]["summary"]
        status = response["fields"]["status"]["name"]
        priority = response["fields"]["priority"]["name"]
        reporter = response["fields"]["reporter"]["displayName"]
        assignee = ""
        if response["fields"]["assignee"]:
            assignee = response["fields"]["assignee"]["displayName"]

        info = "%s : %s [%s,%s,reporter=%s,assignee=%s]" % (link, desc, status,
               priority, reporter, assignee)
        return info

    def _create_issue(self, user_id, project, priority, title, desc=""):
        if priority < 1:
            priority = 1
        if priority > 5:
            priority = 5
        desc = "Submitted by %s\n%s" % (user_id, desc)

        fields = {}
        fields["priority"] = {
            "name": ("P%s" % priority)
        }
        fields["project"] = {
            "key": project.upper().strip()
        }
        fields["issuetype"] = {
            "name": "Bug"
        }
        fields["summary"] = title
        fields["description"] = desc

        info = {
            "fields": fields
        }

        url = self._url("/rest/api/2/issue")
        res = requests.post(url, auth=self.auth, data=json.dumps(info), headers={
            "Content-Type": "application/json"
        })

        if res.status_code < 200 or res.status_code >= 300:
            err = "Failed: HTTP %s - %s" % (res.status_code, res.text)
            log.error(err)
            return err

        response = json.loads(res.text)
        issue_key = response["key"]
        link = self._linkify(issue_key)

        return "Created issue: %s" % link

    def _comment_issue(self, user_id, key, text):
        text = "By %s: %s" % (user_id, text)
        info = {
            "body": text
        }

        url = self._url("/rest/api/2/issue/%s/comment" % key)
        res = requests.post(url, auth=self.auth, data=json.dumps(info), headers={
            "Content-Type": "application/json"
        })

        if res.status_code < 200 or res.status_code >= 300:
            err = "Failed: HTTP %s - %s" % (res.status_code, res.text)
            log.error(err)
            return err
        link = self._linkify(key)
        return "Commented on issue %s" % link

    def _linkify(self, key):
        return "%s/browse/%s" % (self.store.get("url"), key)

    def _url(self, path):
        return self.store.get("url") + path

    def get_webhook_key(self):
        return "jira"

    def on_receive_webhook(self, url, data, ip, headers):
        j = json.loads(data)

        info = self.get_webhook_json_keys(j)
        self.on_receive_jira_push(info)

    def get_webhook_json_keys(self, j):
        key = j['issue']['key']
        user = j['user']['name']
        self_key = j['issue']['self']
        summary = self.get_webhook_summary(j)
        action = ""

        if j['webhookEvent'] == "jira:issue_updated":
            action = "updated"
        elif j['webhookEvent'] == "jira:issue_deleted":
            action = "deleted"
        elif j['webhookEvent'] == "jira:issue_created":
            action = "created"

        return {
            "key": key,
            "user": user,
            "summary": summary,
            "self": self_key,
            "action": action
        }

    def get_webhook_summary(self, j):
        summary = j['issue']['fields']['summary']
        priority = j['issue']['fields']['priority']['name']
        status = j['issue']['fields']['status']['name']

        if "resolution" in j['issue']['fields'] \
            and j['issue']['fields']['resolution'] is not None:
            status = "%s (%s)" \
                % (status, j['issue']['fields']['resolution']['name'])

        return "%s [%s, %s]" \
            % (summary, priority, status)
Ejemplo n.º 12
0
class GithubPlugin(Plugin):
    """Plugin for interacting with Github.
    github show projects : Show which github projects this bot recognises.
    github show track|tracking : Show which projects are being tracked.
    github track "owner/repo" "owner/repo" : Track the given projects.
    github add owner/repo : Add the given repo to the tracking list.
    github remove owner/repo : Remove the given repo from the tracking list.
    github stop track|tracking : Stop tracking github projects.
    """
    name = "github"
    #New events:
    #    Type: org.matrix.neb.plugin.github.projects.tracking
    #    State: Yes
    #    Content: {
    #        projects: [projectName1, projectName2, ...]
    #    }

    #Webhooks:
    #    /neb/github
    TYPE_TRACK = "org.matrix.neb.plugin.github.projects.tracking"
    TYPE_COLOR = "org.matrix.neb.plugin.github.projects.color"

    TRACKING = ["track", "tracking"]

    def __init__(self, *args, **kwargs):
        super(GithubPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("github.json")
        self.rooms = RoomContextStore(
            [GithubPlugin.TYPE_TRACK]
        )

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

    def on_receive_github_push(self, info):
        log.info("recv %s", info)

        # add the project if we didn't know about it before
        if info["repo"] not in self.store.get("known_projects"):
            log.info("Added new repo: %s", info["repo"])
            projects = self.store.get("known_projects")
            projects.append(info["repo"])
            self.store.set("known_projects", projects)

        push_message = ""

        if info["type"] == "delete":
            push_message = '[%s] %s <font color="red"><b>deleted</font> %s</b>' % (
                info["repo"],
                info["commit_username"],
                info["branch"]
            )
        elif info["type"] == "commit":
            # form the template:
            # [<repo>] <username> pushed <num> commits to <branch>: <git.io link>
            # 1<=3 of <branch name> <short hash> <full username>: <comment>
            if info["num_commits"] == 1:
                push_message = "[%s] %s pushed to <b>%s</b>: %s  - %s" % (
                    info["repo"],
                    info["commit_username"],
                    info["branch"],
                    info["commit_msg"],
                    info["commit_link"]
                )
            else:
                summary = ""
                max_commits = 3
                count = 0
                for c in info["commits_summary"]:
                    if count == max_commits:
                        break
                    summary += "\n%s: %s" % (c["author"], c["summary"])
                    count += 1

                push_message = "[%s] %s pushed %s commits to <b>%s</b>: %s %s" % (
                    info["repo"],
                    info["commit_username"],
                    info["num_commits"],
                    info["branch"],
                    info["commit_link"],
                    summary
                )
        else:
            log.warn("Unknown push type. %s", info["type"])
            return

        self.send_message_to_repos(info["repo"], push_message)

    def send_message_to_repos(self, repo, push_message):
        # send messages to all rooms registered with this project.
        for room_id in self.rooms.get_room_ids():
            try:
                if repo in self.rooms.get_content(room_id, GithubPlugin.TYPE_TRACK)["projects"]:
                    self.matrix.send_message_event(
                        room_id,
                        "m.room.message",
                        self.matrix.get_html_body(push_message, msgtype="m.notice")
                    )
            except KeyError:
                pass

    def cmd_show(self, event, action):
        """Show information on projects or projects being tracked.
        Show which projects are being tracked. 'github show tracking'
        Show which proejcts are recognised so they could be tracked. 'github show projects'
        """
        if action == "projects":
            projects = self.store.get("known_projects")
            return "Available projects: %s - To add more projects, you must register a webhook on Github." % json.dumps(projects)
        elif action in self.TRACKING:
            return self._get_tracking(event["room_id"])
        else:
            return self.cmd_show.__doc__

    @admin_only
    def cmd_add(self, event, repo):
        """Add a repo for tracking. 'github add owner/repo'"""
        if repo not in self.store.get("known_projects"):
            return "Unknown project name: %s." % repo

        try:
            room_repos = self.rooms.get_content(
                event["room_id"],
                GithubPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_repos = []

        if repo in room_repos:
            return "%s is already being tracked." % repo

        room_repos.append(repo)
        self._send_track_event(event["room_id"], room_repos)

        return "Added %s. Commits for projects %s will be displayed as they are commited." % (repo, room_repos)

    @admin_only
    def cmd_remove(self, event, repo):
        """Remove a repo from tracking. 'github remove owner/repo'"""
        try:
            room_repos = self.rooms.get_content(
                event["room_id"],
                GithubPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_repos = []

        if repo not in room_repos:
            return "Cannot remove %s : It isn't being tracked." % repo

        room_repos.remove(repo)
        self._send_track_event(event["room_id"], room_repos)

        return "Removed %s. Commits for projects %s will be displayed as they are commited." % (repo, room_repos)

    @admin_only
    def cmd_track(self, event, *args):
        if len(args) == 0:
            return self._get_tracking(event["room_id"])

        for project in args:
            if not project in self.store.get("known_projects"):
                return "Unknown project name: %s." % project

        self._send_track_event(event["room_id"], args)

        return "Commits for projects %s will be displayed as they are commited." % (args,)

    @admin_only
    def cmd_stop(self, event, action):
        """Stop tracking projects. 'github stop tracking'"""
        if action in self.TRACKING:
            self._send_track_event(event["room_id"], [])
            return "Stopped tracking projects."
        else:
            return self.cmd_stop.__doc__

    def xcmd_color(self, event, repo, branch, color):
        """"Set the color of notifications for a project and branch. The color must be hex or an HTML 4 named color.
        'github color project branch color' e.g. github color bob/repo develop #0000ff
        """

        if not repo in self.store.get("known_projects"):
            return "Unknown github repo: %s" % repo

        # basic color validation
        valid = False
        color = color.strip().lower()
        if color in ["white","silver","gray","black","red","maroon","yellow","olive","lime","green","aqua","teal","blue","navy","fuchsia","purple"]:
            valid = True
        else:
            test_color = color
            if color[0] == '#':
                test_color = color[1:]
            try:
                color_int = int(test_color, 16)
                valid = color_int <= 0xFFFFFF
                color = "#%06x" % color_int
            except:
                return "Color should be like '#112233', '0x112233' or 'green'"

        if not valid:
            return "Color should be like '#112233', '0x112233' or 'green'"

        return "Not yet implemented. Valid. Repo=%s Branch=%s Color=%s" % (repo, branch, color)

    def _send_track_event(self, room_id, project_names):
        self.matrix.send_state_event(
            room_id,
            self.TYPE_TRACK,
            {
                "projects": project_names
            }
        )

    def _get_tracking(self, room_id):
        try:
            return ("Currently tracking %s" % json.dumps(
                self.rooms.get_content(room_id, GithubPlugin.TYPE_TRACK)["projects"]
            ))
        except KeyError:
            return "Not tracking any projects currently."

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_sync(self, sync):
        log.debug("Plugin: Github sync state:")
        self.rooms.init_from_sync(sync)

    def get_webhook_key(self):
        return "github"

    def on_receive_pull_request(self, data):
        action = data["action"]
        pull_req_num = data["number"]
        repo_name = data["repository"]["full_name"]
        pr = data["pull_request"]
        pr_url = pr["html_url"]
        pr_state = pr["state"]
        pr_title = pr["title"]

        user = data["sender"]["login"]

        msg = "[%s] %s %s <b>pull request #%s</b>: %s [%s] - %s" % (
            repo_name,
            user,
            action,
            pull_req_num,
            pr_title,
            pr_state,
            pr_url
        )

        self.send_message_to_repos(repo_name, msg)

    def on_receive_create(self, data):
        if data["ref_type"] != "branch":
            return  # only echo branch creations for now.

        branch_name = data["ref"]
        user = data["sender"]["login"]
        repo_name = data["repository"]["full_name"]

        msg = '[%s] %s <font color="green">created</font> a new branch: <b>%s</b>' % (
            repo_name,
            user,
            branch_name
        )

        self.send_message_to_repos(repo_name, msg)

    def on_receive_ping(self, data):
        repo_name = data["repository"]["full_name"]
        # add the project if we didn't know about it before
        if repo_name not in self.store.get("known_projects"):
            log.info("Added new repo: %s", repo_name)
            projects = self.store.get("known_projects")
            projects.append(repo_name)
            self.store.set("known_projects", projects)

    def on_receive_comment(self, data):
        repo_name = data["repository"]["full_name"]
        issue = data["issue"]
        comment = data["comment"]
        is_pull_request = "pull_request" in issue
        if not is_pull_request:
            return  # don't bother displaying issue comments

        pr_title = issue["title"]
        pr_num = issue["number"]
        pr_username = issue["user"]["login"]
        comment_url = comment["html_url"]
        username = comment["user"]["login"]

        msg = "[%s] %s commented on %s's <b>pull request #%s</b>: %s - %s" % (
            repo_name,
            username,
            pr_username,
            pr_num,
            pr_title,
            comment_url
        )
        self.send_message_to_repos(repo_name, msg)


    def on_receive_issue(self, data):
        action = data["action"]
        repo_name = data["repository"]["full_name"]
        issue = data["issue"]
        title = issue["title"]
        issue_num = issue["number"]
        url = issue["html_url"]

        user = data["sender"]["login"]

        if action == "assigned":
            try:
                assignee = data["assignee"]["login"]
                msg = "[%s] %s assigned issue #%s to %s: %s - %s" % (
                    repo_name,
                    user,
                    issue_num,
                    assignee,
                    title,
                    url
                )
                self.send_message_to_repos(repo_name, msg)
                return
            except:
                pass


        msg = "[%s] %s %s issue #%s: %s - %s" % (
            repo_name,
            user,
            action,
            issue_num,
            title,
            url
        )

        self.send_message_to_repos(repo_name, msg)


    def on_receive_webhook(self, url, data, ip, headers):
        if self.store.get("secret_token"):
            token_sha1 = headers.get('X-Hub-Signature')
            payload_body = data
            calc = hmac.new(str(self.store.get("secret_token")), payload_body,
                            sha1)
            calc_sha1 = "sha1=" + calc.hexdigest()
            if token_sha1 != calc_sha1:
                log.warn("GithubWebServer: FAILED SECRET TOKEN AUTH. IP=%s",
                         ip)
                return ("", 403, {})

        event_type = headers.get('X-GitHub-Event')
        if event_type == "pull_request":
            self.on_receive_pull_request(json.loads(data))
            return
        elif event_type == "issues":
            self.on_receive_issue(json.loads(data))
            return
        elif event_type == "create":
            self.on_receive_create(json.loads(data))
            return
        elif event_type == "ping":
            self.on_receive_ping(json.loads(data))
            return
        elif event_type == "issue_comment":  # INCLUDES PR COMMENTS!!!
            self.on_receive_comment(json.loads(data))
            return

        j = json.loads(data)
        repo_name = j["repository"]["full_name"]
        # strip 'refs/heads' from 'refs/heads/branch_name'
        branch = '/'.join(j["ref"].split('/')[2:])

        commit_msg = ""
        commit_name = ""
        commit_link = ""
        short_hash = ""
        push_type = "commit"

        if j["head_commit"]:
            commit_msg = j["head_commit"]["message"]
            commit_name = j["head_commit"]["committer"]["name"]
            commit_link = j["head_commit"]["url"]
            # short hash please
            short_hash = commit_link.split('/')[-1][0:8]
            commit_link = '/'.join(commit_link.split('/')[0:-1]) + "/" + short_hash
        elif j["deleted"]:
            # looks like this branch was deleted, no commit and deleted=true
            commit_name = j["pusher"]["name"]
            push_type = "delete"

        commit_uname = None
        try:
            commit_uname = j["head_commit"]["committer"]["username"]
        except Exception:
            # possible if they haven't tied up with a github account
            commit_uname = commit_name

        # look for multiple commits
        num_commits = 1
        commits_summary = []
        if "commits" in j and len(j["commits"]) > 1:
            num_commits = len(j["commits"])
            for c in j["commits"]:
                cname = None
                try:
                    cname = c["author"]["username"]
                except:
                    cname = c["author"]["name"]
                commits_summary.append({
                    "author": cname,
                    "summary": c["message"]
                })


        self.on_receive_github_push({
            "branch": branch,
            "repo": repo_name,
            "commit_msg": commit_msg,
            "commit_username": commit_uname,
            "commit_name": commit_name,
            "commit_link": commit_link,
            "commit_hash": short_hash,
            "type": push_type,
            "num_commits": num_commits,
            "commits_summary": commits_summary
        })
Ejemplo n.º 13
0
class JenkinsPlugin(Plugin):
    """ Plugin for receiving Jenkins notifications via the Notification Plugin.
    jenkins show projects : Display which projects this bot recognises.
    jenkins show track|tracking : Display which projects this bot is tracking.
    jenkins track project1 project2 ... : Track Jenkins notifications for the named projects.
    jenkins stop track|tracking : Stop tracking Jenkins notifications.
    jenkins add projectName : Start tracking projectName.
    jenkins remove projectName : Stop tracking projectName.
    """
    name = "jenkins"

    # https://wiki.jenkins-ci.org/display/JENKINS/Notification+Plugin

    # New events:
    #    Type: org.matrix.neb.plugin.jenkins.projects.tracking
    #    State: Yes
    #    Content: {
    #        projects: [projectName1, projectName2, ...]
    #    }

    # Webhooks:
    #    /neb/jenkins

    TRACKING = ["track", "tracking"]
    TYPE_TRACK = "org.matrix.neb.plugin.jenkins.projects.tracking"

    def __init__(self, *args, **kwargs):
        super(JenkinsPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jenkins.json")
        self.rooms = RoomContextStore([JenkinsPlugin.TYPE_TRACK])

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        self.failed_builds = {
            # projectName:branch: { commit:x }
        }

    def cmd_show(self, event, action):
        """Show information on projects or projects being tracked.
        Show which projects are being tracked. 'jenkins show tracking'
        Show which proejcts are recognised so they could be tracked. 'jenkins show projects'
        """
        if action in self.TRACKING:
            return self._get_tracking(event["room_id"])
        elif action == "projects":
            projects = self.store.get("known_projects")
            return "Available projects: %s" % json.dumps(projects)
        else:
            return "Invalid arg '%s'.\n %s" % (action, self.cmd_show.__doc__)

    @admin_only
    def cmd_track(self, event, *args):
        """Track projects. 'jenkins track Foo "bar with spaces"'"""
        if len(args) == 0:
            return self._get_tracking(event["room_id"])

        for project in args:
            if project not in self.store.get("known_projects"):
                return "Unknown project name: %s." % project

        self._send_track_event(event["room_id"], args)

        return "Jenkins notifications for projects %s will be displayed when they fail." % args

    @admin_only
    def cmd_add(self, event, project):
        """Add a project for tracking. 'jenkins add projectName'"""
        if project not in self.store.get("known_projects"):
            return "Unknown project name: %s." % project

        try:
            room_projects = self.rooms.get_content(
                event["room_id"], JenkinsPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_projects = []

        if project in room_projects:
            return "%s is already being tracked." % project

        room_projects.append(project)
        self._send_track_event(event["room_id"], room_projects)

        return "Added %s. Jenkins notifications for projects %s will be displayed when they fail." % (
            project, room_projects)

    @admin_only
    def cmd_remove(self, event, project):
        """Remove a project from tracking. 'jenkins remove projectName'"""
        try:
            room_projects = self.rooms.get_content(
                event["room_id"], JenkinsPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_projects = []

        if project not in room_projects:
            return "Cannot remove %s : It isn't being tracked." % project

        room_projects.remove(project)
        self._send_track_event(event["room_id"], room_projects)

        return "Removed %s. Jenkins notifications for projects %s will be displayed when they fail." % (
            project, room_projects)

    @admin_only
    def cmd_stop(self, event, action):
        """Stop tracking projects. 'jenkins stop tracking'"""
        if action in self.TRACKING:
            self._send_track_event(event["room_id"], [])
            return "Stopped tracking projects."
        else:
            return "Invalid arg '%s'.\n %s" % (action, self.cmd_stop.__doc__)

    def _get_tracking(self, room_id):
        try:
            return ("Currently tracking %s" % json.dumps(
                self.rooms.get_content(room_id,
                                       JenkinsPlugin.TYPE_TRACK)["projects"]))
        except KeyError:
            return "Not tracking any projects currently."

    def _send_track_event(self, room_id, project_names):
        self.matrix.send_state_event(room_id, self.TYPE_TRACK,
                                     {"projects": project_names})

    def send_message_to_repos(self, repo, push_message):
        # send messages to all rooms registered with this project.
        for room_id in self.rooms.get_room_ids():
            try:
                if (repo in self.rooms.get_content(
                        room_id, JenkinsPlugin.TYPE_TRACK)["projects"]):
                    self.matrix.send_message_event(
                        room_id, "m.room.message",
                        self.matrix.get_html_body(push_message,
                                                  msgtype="m.notice"))
            except KeyError:
                pass

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_sync(self, sync):
        log.debug("Plugin: Jenkins sync state:")
        self.rooms.init_from_sync(sync)

    def get_webhook_key(self):
        return "jenkins"

    def on_receive_webhook(self, url, data, ip, headers):
        # data is of the form:
        # {
        #   "name":"Synapse",
        #   "url":"job/Synapse/",
        #   "build": {
        #     "full_url":"http://*****:*****@github.com:matrix-org/synapse.git",
        #        "branch":"origin/develop",
        #        "commit":"72aef114ab1201f5a5cd734220c9ec738c4e2910"
        #      },
        #     "artifacts":{}
        #    }
        # }
        log.info("URL: %s", url)
        log.info("Data: %s", data)
        log.info("Headers: %s", headers)

        j = json.loads(data)
        name = j["name"]

        query_dict = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
        if self.store.get("secret_token"):
            if "secret" not in query_dict:
                log.warning("Jenkins webhook: Missing secret.")
                return "", 403, {}

            # The jenkins Notification plugin does not support any sort of
            # "execute this code on this json object before you send" so we can't
            # send across HMAC SHA1s like with github :( so a secret token will
            # have to do.
            secrets = query_dict["secret"]
            if len(secrets) > 1:
                log.warning(
                    "Jenkins webhook: FAILED SECRET TOKEN AUTH. Too many secrets. IP=%s",
                    ip)
                return "", 403, {}
            elif secrets[0] != self.store.get("secret_token"):
                log.warning(
                    "Jenkins webhook: FAILED SECRET TOKEN AUTH. Mismatch. IP=%s",
                    ip)
                return "", 403, {}
            else:
                log.info("Jenkins webhook: Secret verified.")

        # add the project if we didn't know about it before
        if name not in self.store.get("known_projects"):
            log.info("Added new job: %s", name)
            projects = self.store.get("known_projects")
            projects.append(name)
            self.store.set("known_projects", projects)

        status = j["build"]["status"]
        branch = None
        commit = None
        jenkins_url = None
        info = ""
        try:
            branch = j["build"]["scm"]["branch"]
            commit = j["build"]["scm"]["commit"]
            git_url = j["build"]["scm"]["url"]
            jenkins_url = j["build"]["full_url"]
            # try to format the git url nicely
            if (git_url.startswith("*****@*****.**")
                    and git_url.endswith(".git")):
                # [email protected]:matrix-org/synapse.git
                org_and_repo = git_url.split(":")[1][:-4]
                commit = "https://github.com/%s/commit/%s" % (org_and_repo,
                                                              commit)

            info = "%s commit %s - %s" % (branch, commit, jenkins_url)
        except KeyError:
            pass

        fail_key = "%s:%s" % (name, branch)

        if status.upper() != "SUCCESS":
            # complain
            msg = '<font color="red">[%s] <b>%s - %s</b></font>' % (
                name, status, info)

            if fail_key in self.failed_builds:
                info = "%s failing since commit %s - %s" % (
                    branch, self.failed_builds[fail_key]["commit"],
                    jenkins_url)
                msg = '<font color="red">[%s] <b>%s - %s</b></font>' % (
                    name, status, info)
            else:  # add it to the list
                self.failed_builds[fail_key] = {"commit": commit}

            self.send_message_to_repos(name, msg)
        else:
            # do we need to prod people?
            if fail_key in self.failed_builds:
                info = "%s commit %s" % (branch, commit)
                msg = '<font color="green">[%s] <b>%s - %s</b></font>' % (
                    name, status, info)
                self.send_message_to_repos(name, msg)
                self.failed_builds.pop(fail_key)
Ejemplo n.º 14
0
class GithubPlugin(Plugin):
    """Plugin for interacting with Github.
    github show projects : Show which github projects this bot recognises.
    github show track|tracking : Show which projects are being tracked.
    github track "owner/repo" "owner/repo" : Track the given projects.
    github add owner/repo : Add the given repo to the tracking list.
    github remove owner/repo : Remove the given repo from the tracking list.
    github stop track|tracking : Stop tracking github projects.
    github create owner/repo "Bug title" "Bug desc" : Create an issue on Github.
    github label add|remove owner/repo issue# label : Label an issue on Github.
    """
    name = "github"
    #New events:
    #    Type: org.matrix.neb.plugin.github.projects.tracking
    #    State: Yes
    #    Content: {
    #        projects: [projectName1, projectName2, ...]
    #    }

    #Webhooks:
    #    /neb/github
    TYPE_TRACK = "org.matrix.neb.plugin.github.projects.tracking"
    TYPE_COLOR = "org.matrix.neb.plugin.github.projects.color"

    TRACKING = ["track", "tracking"]

    def __init__(self, *args, **kwargs):
        super(GithubPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("github.json")
        self.rooms = RoomContextStore(
            [GithubPlugin.TYPE_TRACK]
        )

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        if not self.store.has("github_access_token"):
            log.info("A github access_token is required to create github issues.")
            log.info("Issues will be created as this user.")
            token = raw_input("(Optional) Github token: ").strip()
            if token:
                self.store.set("github_access_token", token)
            else:
                log.info("You will not be able to create Github issues.")

    def on_receive_github_push(self, info):
        log.info("recv %s", info)

        # add the project if we didn't know about it before
        if info["repo"] not in self.store.get("known_projects"):
            log.info("Added new repo: %s", info["repo"])
            projects = self.store.get("known_projects")
            projects.append(info["repo"])
            self.store.set("known_projects", projects)

        push_message = ""

        if info["type"] == "delete":
            push_message = '[<u>%s</u>] %s <font color="red"><b>deleted</font> %s</b>' % (
                info["repo"],
                info["commit_username"],
                info["branch"]
            )
        elif info["type"] == "commit":
            # form the template:
            # [<repo>] <username> pushed <num> commits to <branch>: <git.io link>
            # 1<=3 of <branch name> <short hash> <full username>: <comment>
            if info["num_commits"] == 1:
                push_message = "[<u>%s</u>] %s pushed to <b>%s</b>: %s  - %s" % (
                    info["repo"],
                    info["commit_username"],
                    info["branch"],
                    info["commit_msg"],
                    info["commit_link"]
                )
            else:
                summary = ""
                max_commits = 3
                count = 0
                for c in info["commits_summary"]:
                    if count == max_commits:
                        break
                    summary += "\n%s: %s" % (c["author"], c["summary"])
                    count += 1

                push_message = "[<u>%s</u>] %s pushed %s commits to <b>%s</b>: %s %s" % (
                    info["repo"],
                    info["commit_username"],
                    info["num_commits"],
                    info["branch"],
                    info["commit_link"],
                    summary
                )
        else:
            log.warn("Unknown push type. %s", info["type"])
            return

        self.send_message_to_repos(info["repo"], push_message)

    def send_message_to_repos(self, repo, push_message):
        # send messages to all rooms registered with this project.
        for room_id in self.rooms.get_room_ids():
            try:
                if repo in self.rooms.get_content(room_id, GithubPlugin.TYPE_TRACK)["projects"]:
                    self.matrix.send_message_event(
                        room_id,
                        "m.room.message",
                        self.matrix.get_html_body(push_message, msgtype="m.notice")
                    )
            except KeyError:
                pass

    def cmd_show(self, event, action):
        """Show information on projects or projects being tracked.
        Show which projects are being tracked. 'github show tracking'
        Show which proejcts are recognised so they could be tracked. 'github show projects'
        """
        if action == "projects":
            projects = self.store.get("known_projects")
            return "Available projects: %s - To add more projects, you must register a webhook on Github." % json.dumps(projects)
        elif action in self.TRACKING:
            return self._get_tracking(event["room_id"])
        else:
            return self.cmd_show.__doc__

    @admin_only
    def cmd_add(self, event, repo):
        """Add a repo for tracking. 'github add owner/repo'"""
        if repo not in self.store.get("known_projects"):
            return "Unknown project name: %s." % repo

        try:
            room_repos = self.rooms.get_content(
                event["room_id"],
                GithubPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_repos = []

        if repo in room_repos:
            return "%s is already being tracked." % repo

        room_repos.append(repo)
        self._send_track_event(event["room_id"], room_repos)

        return "Added %s. Commits for projects %s will be displayed as they are commited." % (repo, room_repos)

    @admin_only
    def cmd_remove(self, event, repo):
        """Remove a repo from tracking. 'github remove owner/repo'"""
        try:
            room_repos = self.rooms.get_content(
                event["room_id"],
                GithubPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_repos = []

        if repo not in room_repos:
            return "Cannot remove %s : It isn't being tracked." % repo

        room_repos.remove(repo)
        self._send_track_event(event["room_id"], room_repos)

        return "Removed %s. Commits for projects %s will be displayed as they are commited." % (repo, room_repos)

    @admin_only
    def cmd_track(self, event, *args):
        if len(args) == 0:
            return self._get_tracking(event["room_id"])

        for project in args:
            if not project in self.store.get("known_projects"):
                return "Unknown project name: %s." % project

        self._send_track_event(event["room_id"], args)

        return "Commits for projects %s will be displayed as they are commited." % (args,)

    @admin_only
    def cmd_stop(self, event, action):
        """Stop tracking projects. 'github stop tracking'"""
        if action in self.TRACKING:
            self._send_track_event(event["room_id"], [])
            return "Stopped tracking projects."
        else:
            return self.cmd_stop.__doc__

    @admin_only
    def cmd_create(self, event, *args):
        """Create a new issue. Format: 'create <owner/repo> <title> <desc(optional)>'
        E.g. 'create matrix-org/synapse A bug goes here
        'create matrix-org/synapse "Title here" "desc here" """
        if not args or len(args) < 2:
            return self.cmd_create.__doc__
        project = args[0]
        others = args[1:]
        # others must contain a title, may contain a description. If it contains
        # a description, it MUST be in [1] and be longer than 1 word.
        title = ' '.join(others)
        desc = ""
        try:
            possible_desc = others[1]
            if ' ' in possible_desc:
                desc = possible_desc
                title = others[0]
        except:
            pass

        return self._create_issue(
            event["user_id"], project, title, desc
        )

    @admin_only
    def cmd_label_remove(self, event, repo, issue_num, *args):
        """Remove a label on an issue. Format: 'label remove <owner/repo> <issue num> <label> <label> <label>'
        E.g. 'label remove matrix-org/synapse 323 bug p2 blocked'
        """
        e = self._is_valid_issue_request(repo, issue_num)
        if e:
            return e
        if len(args) == 0:
            return "You must specify at least one label."

        errs = []

        for label in args:
            url = "https://api.github.com/repos/%s/issues/%s/labels/%s" % (repo, issue_num, label)
            res = requests.delete(url, headers={
                "Authorization": "token %s" % self.store.get("github_access_token")
            })
            if res.status_code < 200 or res.status_code >= 300:
                errs.append(
                    "Problem removing label %s : HTTP %s" % (label, res.status_code)
                )
                return err

        if len(errs) == 0:
            return "Removed labels %s" % (json.dumps(args),)
        else:
            return "There was a problem removing some labels:\n" + "\n".join(errs)

    @admin_only
    def cmd_label_add(self, event, repo, issue_num, *args):
        """Label an issue. Format: 'label add <owner/repo> <issue num> <label> <label> <label>'
        E.g. 'label add matrix-org/synapse 323 bug p2 blocked'
        """
        e = self._is_valid_issue_request(repo, issue_num)
        if e:
            return e
        if len(args) == 0:
            return "You must specify at least one label."

        url = "https://api.github.com/repos/%s/issues/%s/labels" % (repo, issue_num)
        res = requests.post(url, data=json.dumps(args), headers={
            "Content-Type": "application/json",
            "Authorization": "token %s" % self.store.get("github_access_token")
        })
        if res.status_code < 200 or res.status_code >= 300:
            err = "%s Failed: HTTP %s" % (url, res.status_code,)
            log.error(err)
            return err

        return "Added labels %s" % (json.dumps(args),)

    def _create_issue(self, user_id, project, title, desc=""):
        if not self.store.has("github_access_token"):
            return "This plugin isn't configured to create Github issues."

        # Add a space after the @ to avoid pinging people on Github!
        user_id = user_id.replace("@", "@ ")

        desc = "Created by %s.\n\n%s" % (user_id, desc)
        info = {
            "title": title,
            "body": desc
        }

        url = "https://api.github.com/repos/%s/issues" % project
        res = requests.post(url, data=json.dumps(info), headers={
            "Content-Type": "application/json",
            "Authorization": "token %s" % self.store.get("github_access_token")
        })
        if res.status_code < 200 or res.status_code >= 300:
            err = "%s Failed: HTTP %s" % (url, res.status_code,)
            log.error(err)
            return err

        response = json.loads(res.text)
        return "Created issue: %s" % response["html_url"]

    def _is_valid_issue_request(self, repo, issue_num):
        issue_is_num = True
        try:
            issue_is_num = int(issue_num)
        except ValueError:
            issue_is_num = False

        if "/" not in repo:
            return "Repo must be in the form 'owner/repo' e.g. 'matrix-org/synapse'."

        if not issue_is_num:
            return "Issue number must be a number"
        
        if not self.store.has("github_access_token"):
            return "This plugin isn't configured to interact with Github issues."

    def _send_track_event(self, room_id, project_names):
        self.matrix.send_state_event(
            room_id,
            self.TYPE_TRACK,
            {
                "projects": project_names
            }
        )

    def _get_tracking(self, room_id):
        try:
            return ("Currently tracking %s" % json.dumps(
                self.rooms.get_content(room_id, GithubPlugin.TYPE_TRACK)["projects"]
            ))
        except KeyError:
            return "Not tracking any projects currently."

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_sync(self, sync):
        log.debug("Plugin: Github sync state:")
        self.rooms.init_from_sync(sync)

    def get_webhook_key(self):
        return "github"

    def on_receive_pull_request(self, data):
        action = data["action"]
        pull_req_num = data["number"]
        repo_name = data["repository"]["full_name"]
        pr = data["pull_request"]
        pr_url = pr["html_url"]
        pr_state = pr["state"]
        pr_title = pr["title"]

        user = data["sender"]["login"]

        action_target = ""
        if pr.get("assignee") and pr["assignee"].get("login"):
            action_target = " to %s" % (pr["assignee"]["login"],)

        msg = "[<u>%s</u>] %s %s <b>pull request #%s</b>: %s [%s]%s - %s" % (
            repo_name,
            user,
            action,
            pull_req_num,
            pr_title,
            pr_state,
            action_target,
            pr_url
        )

        self.send_message_to_repos(repo_name, msg)

    def on_receive_create(self, data):
        if data["ref_type"] != "branch":
            return  # only echo branch creations for now.

        branch_name = data["ref"]
        user = data["sender"]["login"]
        repo_name = data["repository"]["full_name"]

        msg = '[<u>%s</u>] %s <font color="green">created</font> a new branch: <b>%s</b>' % (
            repo_name,
            user,
            branch_name
        )

        self.send_message_to_repos(repo_name, msg)

    def on_receive_ping(self, data):
        repo_name = data.get("repository", {}).get("full_name")
        # add the project if we didn't know about it before
        if repo_name and repo_name not in self.store.get("known_projects"):
            log.info("Added new repo: %s", repo_name)
            projects = self.store.get("known_projects")
            projects.append(repo_name)
            self.store.set("known_projects", projects)

    def on_receive_comment(self, data):
        repo_name = data["repository"]["full_name"]
        issue = data["issue"]
        comment = data["comment"]
        is_pull_request = "pull_request" in issue
        if not is_pull_request:
            return  # don't bother displaying issue comments

        pr_title = issue["title"]
        pr_num = issue["number"]
        pr_username = issue["user"]["login"]
        comment_url = comment["html_url"]
        username = comment["user"]["login"]

        msg = "[<u>%s</u>] %s commented on %s's <b>pull request #%s</b>: %s - %s" % (
            repo_name,
            username,
            pr_username,
            pr_num,
            pr_title,
            comment_url
        )
        self.send_message_to_repos(repo_name, msg)


    def on_receive_pull_request_comment(self, data):
        repo_name = data["repository"]["full_name"]
        username = data["sender"]["login"]
        pull_request = data["pull_request"]
        pr_username = pull_request["user"]["login"]
        pr_num = pull_request["number"]
        assignee = "None"
        if data["pull_request"].get("assignee"):
            assignee = data["pull_request"]["assignee"]["login"]
        pr_title = pull_request["title"]
        comment_url = data["comment"]["html_url"]

        msg = "[<u>%s</u>] %s made a line comment on %s's <b>pull request #%s</b> (assignee: %s): %s - %s" % (
            repo_name,
            username,
            pr_username,
            pr_num,
            assignee,
            pr_title,
            comment_url
        )
        self.send_message_to_repos(repo_name, msg)


    def on_receive_issue(self, data):
        action = data["action"]
        repo_name = data["repository"]["full_name"]
        issue = data["issue"]
        title = issue["title"]
        issue_num = issue["number"]
        url = issue["html_url"]

        user = data["sender"]["login"]

        if action == "assigned":
            try:
                assignee = data["assignee"]["login"]
                msg = "[<u>%s</u>] %s assigned issue #%s to %s: %s - %s" % (
                    repo_name,
                    user,
                    issue_num,
                    assignee,
                    title,
                    url
                )
                self.send_message_to_repos(repo_name, msg)
                return
            except:
                pass


        msg = "[<u>%s</u>] %s %s issue #%s: %s - %s" % (
            repo_name,
            user,
            action,
            issue_num,
            title,
            url
        )

        self.send_message_to_repos(repo_name, msg)


    def on_receive_webhook(self, url, data, ip, headers):
        if self.store.get("secret_token"):
            token_sha1 = headers.get('X-Hub-Signature')
            payload_body = data
            calc = hmac.new(str(self.store.get("secret_token")), payload_body,
                            sha1)
            calc_sha1 = "sha1=" + calc.hexdigest()
            if token_sha1 != calc_sha1:
                log.warn("GithubWebServer: FAILED SECRET TOKEN AUTH. IP=%s",
                         ip)
                return ("", 403, {})

        json_data = json.loads(data)
        is_private_repo = json_data.get("repository", {}).get("private")
        if is_private_repo:
            log.info(
                "Received private repo event for %s", json_data["repository"].get("name")
            )
            return


        event_type = headers.get('X-GitHub-Event')
        if event_type == "pull_request":
            self.on_receive_pull_request(json_data)
            return
        elif event_type == "issues":
            self.on_receive_issue(json_data)
            return
        elif event_type == "create":
            self.on_receive_create(json_data)
            return
        elif event_type == "ping":
            self.on_receive_ping(json_data)
            return
        elif event_type == "issue_comment":
            # INCLUDES PR COMMENTS!!!
            # But not line comments!
            self.on_receive_comment(json_data)
            return
        elif event_type == "pull_request_review_comment":
            self.on_receive_pull_request_comment(json_data)
            return

        j = json_data
        repo_name = j["repository"]["full_name"]
        # strip 'refs/heads' from 'refs/heads/branch_name'
        branch = '/'.join(j["ref"].split('/')[2:])

        commit_msg = ""
        commit_name = ""
        commit_link = ""
        short_hash = ""
        push_type = "commit"

        if j["head_commit"]:
            commit_msg = j["head_commit"]["message"]
            commit_name = j["head_commit"]["committer"]["name"]
            commit_link = j["head_commit"]["url"]
            # short hash please
            short_hash = commit_link.split('/')[-1][0:8]
            commit_link = '/'.join(commit_link.split('/')[0:-1]) + "/" + short_hash
        elif j["deleted"]:
            # looks like this branch was deleted, no commit and deleted=true
            commit_name = j["pusher"]["name"]
            push_type = "delete"

        commit_uname = None
        try:
            commit_uname = j["head_commit"]["committer"]["username"]
        except Exception:
            # possible if they haven't tied up with a github account
            commit_uname = commit_name

        # look for multiple commits
        num_commits = 1
        commits_summary = []
        if "commits" in j and len(j["commits"]) > 1:
            num_commits = len(j["commits"])
            for c in j["commits"]:
                cname = None
                try:
                    cname = c["author"]["username"]
                except:
                    cname = c["author"]["name"]
                commits_summary.append({
                    "author": cname,
                    "summary": c["message"]
                })


        self.on_receive_github_push({
            "branch": branch,
            "repo": repo_name,
            "commit_msg": commit_msg,
            "commit_username": commit_uname,
            "commit_name": commit_name,
            "commit_link": commit_link,
            "commit_hash": short_hash,
            "type": push_type,
            "num_commits": num_commits,
            "commits_summary": commits_summary
        })
Ejemplo n.º 15
0
class JiraPlugin(Plugin):
    """ Plugin for interacting with JIRA.
    jira version : Display version information for this platform.
    jira track <project> <project2> ... : Track multiple projects
    jira expand <project> <project2> ... : Expand issue IDs for the given projects with issue information.
    jira stop track|tracking : Stops tracking for all projects.
    jira stop expand|expansion|expanding : Stop expanding jira issues.
    jira show track|tracking : Show which projects are being tracked.
    jira show expansion|expand|expanding : Show which project keys will result in issue expansion.
    jira create <project> <priority> <title> <desc> : Create a new JIRA issue.
    jira comment <issue-id> <comment> : Comment on a JIRA issue.
    """
    name = "jira"

    TRACK = ["track", "tracking"]
    EXPAND = ["expansion", "expand", "expanding"]

    # New events:
    #    Type: org.matrix.neb.plugin.jira.issues.tracking / expanding
    #    State: Yes
    #    Content: {
    #        projects: [projectKey1, projectKey2, ...]
    #    }
    TYPE_TRACK = "org.matrix.neb.plugin.jira.issues.tracking"
    TYPE_EXPAND = "org.matrix.neb.plugin.jira.issues.expanding"

    def __init__(self, *args, **kwargs):
        super(JiraPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jira.json")
        self.rooms = RoomContextStore(
            [JiraPlugin.TYPE_TRACK, JiraPlugin.TYPE_EXPAND])

        if not self.store.has("url"):
            url = raw_input("JIRA URL: ").strip()
            self.store.set("url", url)

        if not self.store.has("user") or not self.store.has("pass"):
            user = raw_input("(%s) JIRA Username: "******"url")).strip()
            pw = getpass.getpass("(%s) JIRA Password: "******"url")).strip()
            self.store.set("user", user)
            self.store.set("pass", pw)

        self.auth = (self.store.get("user"), self.store.get("pass"))
        self.regex = re.compile(r"\b(([A-Za-z]+)-\d+)\b")

    @admin_only
    def cmd_stop(self, event, action):
        """ Clear project keys from tracking/expanding.
        Stop tracking projects. 'jira stop tracking'
        Stop expanding projects. 'jira stop expanding'
        """
        if action in self.TRACK:
            self._send_state(JiraPlugin.TYPE_TRACK, event["room_id"], [])
            url = self.store.get("url")
            return "Stopped tracking project keys from %s." % (url)
        elif action in self.EXPAND:
            self._send_state(JiraPlugin.TYPE_EXPAND, event["room_id"], [])
            url = self.store.get("url")
            return "Stopped expanding project keys from %s." % (url)
        else:
            return "Invalid arg '%s'.\n %s" % (action, self.cmd_stop.__doc__)

    @admin_only
    def cmd_track(self, event, *args):
        """Track project keys. 'jira track FOO BAR'"""
        if not args:
            return self._get_tracking(event["room_id"])

        args = [k.upper() for k in args]
        for key in args:
            if re.search("[^A-Z]", key):  # something not A-Z
                return "Key %s isn't a valid project key." % key

        self._send_state(JiraPlugin.TYPE_TRACK, event["room_id"], args)

        url = self.store.get("url")
        return "Issues for projects %s from %s will be displayed as they are updated." % (
            args, url)

    @admin_only
    def cmd_expand(self, event, *args):
        """Expand issues when mentioned for the given project keys. 'jira expand FOO BAR'"""
        if not args:
            return self._get_expanding(event["room_id"])

        args = [k.upper() for k in args]
        for key in args:
            if re.search("[^A-Z]", key):  # something not A-Z
                return "Key %s isn't a valid project key." % key

        self._send_state(JiraPlugin.TYPE_EXPAND, event["room_id"], args)

        url = self.store.get("url")
        return "Issues for projects %s from %s will be expanded as they are mentioned." % (
            args, url)

    @admin_only
    def cmd_create(self, event, *args):
        """Create a new issue. Format: 'create <project> <priority(optional;default 3)> <title> <desc(optional)>'
        E.g. 'create syn p1 This is the title without quote marks'
        'create syn p1 "Title here" "desc here"
        """
        if not args or len(args) < 2:
            return self.cmd_create.__doc__
        project = args[0]
        priority = 3
        others = args[1:]
        if re.match("[Pp][0-9]", args[1]):
            if len(args) < 3:  # priority without title
                return self.cmd_create.__doc__
            try:
                priority = int(args[1][1:])
                others = args[2:]
            except ValueError:
                return self.cmd_create.__doc__
        elif re.match("[Pp][0-9]", args[0]):
            priority = int(args[0][1:])
            project = args[1]
            others = args[2:]
        # others must contain a title, may contain a description. If it contains
        # a description, it MUST be in [1] and be longer than 1 word.
        title = ' '.join(others)
        desc = ""
        try:
            possible_desc = others[1]
            if ' ' in possible_desc:
                desc = possible_desc
                title = others[0]
        except:
            pass

        return self._create_issue(event["user_id"], project, priority, title,
                                  desc)

    @admin_only
    def cmd_comment(self, event, *args):
        """Comment on an issue. Format: 'comment <key> <comment text>'
        E.g. 'comment syn-56 A comment goes here'
        """
        if not args or len(args) < 2:
            return self.cmd_comment.__doc__
        key = args[0].upper()
        text = ' '.join(args[1:])
        return self._comment_issue(event["user_id"], key, text)

    def cmd_version(self, event):
        """Display version information for the configured JIRA platform. 'jira version'"""
        url = self._url("/rest/api/2/serverInfo")
        response = json.loads(requests.get(url).text)

        info = "%s : version %s : build %s" % (response["serverTitle"],
                                               response["version"],
                                               response["buildNumber"])

        return info

    def cmd_show(self, event, action):
        """Show which project keys are being tracked/expanded.
        Show which project keys are being expanded. 'jira show expanding'
        Show which project keys are being tracked. 'jira show tracking'
        """
        action = action.lower()
        if action in self.TRACK:
            return self._get_tracking(event["room_id"])
        elif action in self.EXPAND:
            return self._get_expanding(event["room_id"])

    def _get_tracking(self, room_id):
        try:
            return ("Currently tracking %s" % json.dumps(
                self.rooms.get_content(room_id,
                                       JiraPlugin.TYPE_TRACK)["projects"]))
        except KeyError:
            return "Not tracking any projects currently."

    def _get_expanding(self, room_id):
        try:
            return ("Currently expanding %s" % json.dumps(
                self.rooms.get_content(room_id,
                                       JiraPlugin.TYPE_EXPAND)["projects"]))
        except KeyError:
            return "Not expanding any projects currently."

    def _send_state(self, etype, room_id, project_keys):
        self.matrix.send_state_event(room_id, etype,
                                     {"projects": project_keys})

    def on_msg(self, event, body):
        room_id = event["room_id"]
        body = body.upper()
        groups = self.regex.findall(body)
        if not groups:
            return

        projects = []
        try:
            projects = self.rooms.get_content(
                room_id, JiraPlugin.TYPE_EXPAND)["projects"]
        except KeyError:
            return

        for (key, project) in groups:
            if project in projects:
                try:
                    issue_info = self._get_issue_info(key)
                    if issue_info:
                        self.matrix.send_message(event["room_id"],
                                                 issue_info,
                                                 msgtype="m.notice")
                except Exception as e:
                    log.exception(e)

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_receive_jira_push(self, info):
        log.debug("on_recv %s", info)
        project = self.regex.match(info["key"]).groups()[1]

        # form the message
        link = self._linkify(info["key"])
        push_message = "%s %s <b>%s</b> - %s %s" % (
            info["user"], info["action"], info["key"], info["summary"], link)

        # send messages to all rooms registered with this project.
        for room_id in self.rooms.get_room_ids():
            try:
                content = self.rooms.get_content(room_id,
                                                 JiraPlugin.TYPE_TRACK)
                if project in content["projects"]:
                    self.matrix.send_message_event(
                        room_id, "m.room.message",
                        self.matrix.get_html_body(push_message,
                                                  msgtype="m.notice"))
            except KeyError:
                pass

    def on_sync(self, sync):
        log.debug("Plugin: JIRA sync state:")
        self.rooms.init_from_sync(sync)

    def _get_issue_info(self, issue_key):
        url = self._url("/rest/api/2/issue/%s" % issue_key)
        res = requests.get(url, auth=self.auth)
        if res.status_code != 200:
            return

        response = json.loads(res.text)
        link = self._linkify(issue_key)
        desc = response["fields"]["summary"]
        status = response["fields"]["status"]["name"]
        priority = response["fields"]["priority"]["name"]
        reporter = response["fields"]["reporter"]["displayName"]
        assignee = ""
        if response["fields"]["assignee"]:
            assignee = response["fields"]["assignee"]["displayName"]

        info = "%s : %s [%s,%s,reporter=%s,assignee=%s]" % (
            link, desc, status, priority, reporter, assignee)
        return info

    def _create_issue(self, user_id, project, priority, title, desc=""):
        if priority < 1:
            priority = 1
        if priority > 5:
            priority = 5
        desc = "Submitted by %s\n%s" % (user_id, desc)

        fields = {}
        fields["priority"] = {"name": ("P%s" % priority)}
        fields["project"] = {"key": project.upper().strip()}
        fields["issuetype"] = {"name": "Bug"}
        fields["summary"] = title
        fields["description"] = desc

        info = {"fields": fields}

        url = self._url("/rest/api/2/issue")
        res = requests.post(url,
                            auth=self.auth,
                            data=json.dumps(info),
                            headers={"Content-Type": "application/json"})

        if res.status_code < 200 or res.status_code >= 300:
            err = "Failed: HTTP %s - %s" % (res.status_code, res.text)
            log.error(err)
            return err

        response = json.loads(res.text)
        issue_key = response["key"]
        link = self._linkify(issue_key)

        return "Created issue: %s" % link

    def _comment_issue(self, user_id, key, text):
        text = "By %s: %s" % (user_id, text)
        info = {"body": text}

        url = self._url("/rest/api/2/issue/%s/comment" % key)
        res = requests.post(url,
                            auth=self.auth,
                            data=json.dumps(info),
                            headers={"Content-Type": "application/json"})

        if res.status_code < 200 or res.status_code >= 300:
            err = "Failed: HTTP %s - %s" % (res.status_code, res.text)
            log.error(err)
            return err
        link = self._linkify(key)
        return "Commented on issue %s" % link

    def _linkify(self, key):
        return "%s/browse/%s" % (self.store.get("url"), key)

    def _url(self, path):
        return self.store.get("url") + path

    def get_webhook_key(self):
        return "jira"

    def on_receive_webhook(self, url, data, ip, headers):
        j = json.loads(data)

        info = self.get_webhook_json_keys(j)
        self.on_receive_jira_push(info)

    def get_webhook_json_keys(self, j):
        key = j['issue']['key']
        user = j['user']['name']
        self_key = j['issue']['self']
        summary = self.get_webhook_summary(j)
        action = ""

        if j['webhookEvent'] == "jira:issue_updated":
            action = "updated"
        elif j['webhookEvent'] == "jira:issue_deleted":
            action = "deleted"
        elif j['webhookEvent'] == "jira:issue_created":
            action = "created"

        return {
            "key": key,
            "user": user,
            "summary": summary,
            "self": self_key,
            "action": action
        }

    def get_webhook_summary(self, j):
        summary = j['issue']['fields']['summary']
        priority = j['issue']['fields']['priority']['name']
        status = j['issue']['fields']['status']['name']

        if "resolution" in j['issue']['fields'] \
            and j['issue']['fields']['resolution'] is not None:
            status = "%s (%s)" \
                % (status, j['issue']['fields']['resolution']['name'])

        return "%s [%s, %s]" \
            % (summary, priority, status)
Ejemplo n.º 16
0
class JenkinsPlugin(Plugin):
    """ Plugin for receiving Jenkins notifications via the Notification Plugin.
    jenkins show projects : Display which projects this bot recognises.
    jenkins show track|tracking : Display which projects this bot is tracking.
    jenkins track project1 project2 ... : Track Jenkins notifications for the named projects.
    jenkins stop track|tracking : Stop tracking Jenkins notifications.
    jenkins add projectName : Start tracking projectName.
    jenkins remove projectName : Stop tracking projectName.
    """
    name = "jenkins"

    # https://wiki.jenkins-ci.org/display/JENKINS/Notification+Plugin

    #New events:
    #    Type: org.matrix.neb.plugin.jenkins.projects.tracking
    #    State: Yes
    #    Content: {
    #        projects: [projectName1, projectName2, ...]
    #    }

    #Webhooks:
    #    /neb/jenkins

    TRACKING = ["track", "tracking"]
    TYPE_TRACK = "org.matrix.neb.plugin.jenkins.projects.tracking"

    def __init__(self, *args, **kwargs):
        super(JenkinsPlugin, self).__init__(*args, **kwargs)
        self.store = KeyValueStore("jenkins.json")
        self.rooms = RoomContextStore(
            [JenkinsPlugin.TYPE_TRACK]
        )

        if not self.store.has("known_projects"):
            self.store.set("known_projects", [])

        if not self.store.has("secret_token"):
            self.store.set("secret_token", "")

        self.failed_builds = {
            # projectName:branch: { commit:x }
        }

    def cmd_show(self, event, action):
        """Show information on projects or projects being tracked.
        Show which projects are being tracked. 'jenkins show tracking'
        Show which proejcts are recognised so they could be tracked. 'jenkins show projects'
        """
        if action in self.TRACKING:
            return self._get_tracking(event["room_id"])
        elif action == "projects":
            projects = self.store.get("known_projects")
            return "Available projects: %s" % json.dumps(projects)
        else:
            return "Invalid arg '%s'.\n %s" % (action, self.cmd_show.__doc__)

    @admin_only
    def cmd_track(self, event, *args):
        """Track projects. 'jenkins track Foo "bar with spaces"'"""
        if len(args) == 0:
            return self._get_tracking(event["room_id"])

        for project in args:
            if not project in self.store.get("known_projects"):
                return "Unknown project name: %s." % project

        self._send_track_event(event["room_id"], args)

        return "Jenkins notifications for projects %s will be displayed when they fail." % (args)

    @admin_only
    def cmd_add(self, event, project):
        """Add a project for tracking. 'jenkins add projectName'"""
        if project not in self.store.get("known_projects"):
            return "Unknown project name: %s." % project

        try:
            room_projects = self.rooms.get_content(
                event["room_id"],
                JenkinsPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_projects = []

        if project in room_projects:
            return "%s is already being tracked." % project

        room_projects.append(project)
        self._send_track_event(event["room_id"], room_projects)

        return "Added %s. Jenkins notifications for projects %s will be displayed when they fail." % (project, room_projects)

    @admin_only
    def cmd_remove(self, event, project):
        """Remove a project from tracking. 'jenkins remove projectName'"""
        try:
            room_projects = self.rooms.get_content(
                event["room_id"],
                JenkinsPlugin.TYPE_TRACK)["projects"]
        except KeyError:
            room_projects = []

        if project not in room_projects:
            return "Cannot remove %s : It isn't being tracked." % project

        room_projects.remove(project)
        self._send_track_event(event["room_id"], room_projects)

        return "Removed %s. Jenkins notifications for projects %s will be displayed when they fail." % (project, room_projects)

    @admin_only
    def cmd_stop(self, event, action):
        """Stop tracking projects. 'jenkins stop tracking'"""
        if action in self.TRACKING:
            self._send_track_event(event["room_id"], [])
            return "Stopped tracking projects."
        else:
            return "Invalid arg '%s'.\n %s" % (action, self.cmd_stop.__doc__)

    def _get_tracking(self, room_id):
        try:
            return ("Currently tracking %s" %
                json.dumps(self.rooms.get_content(
                    room_id, JenkinsPlugin.TYPE_TRACK)["projects"]
                )
            )
        except KeyError:
            return "Not tracking any projects currently."

    def _send_track_event(self, room_id, project_names):
        self.matrix.send_state_event(
            room_id,
            self.TYPE_TRACK,
            {
                "projects": project_names
            }
        )

    def send_message_to_repos(self, repo, push_message):
        # send messages to all rooms registered with this project.
        for room_id in self.rooms.get_room_ids():
            try:
                if (repo in self.rooms.get_content(
                        room_id, JenkinsPlugin.TYPE_TRACK)["projects"]):
                    self.matrix.send_message_event(
                        room_id,
                        "m.room.message",
                        self.matrix.get_html_body(push_message, msgtype="m.notice")
                    )
            except KeyError:
                pass

    def on_event(self, event, event_type):
        self.rooms.update(event)

    def on_sync(self, sync):
        log.debug("Plugin: Jenkins sync state:")
        self.rooms.init_from_sync(sync)

    def get_webhook_key(self):
        return "jenkins"

    def on_receive_webhook(self, url, data, ip, headers):
        # data is of the form:
        # {
        #   "name":"Synapse",
        #   "url":"job/Synapse/",
        #   "build": {
        #     "full_url":"http://*****:*****@github.com:matrix-org/synapse.git",
        #        "branch":"origin/develop",
        #        "commit":"72aef114ab1201f5a5cd734220c9ec738c4e2910"
        #      },
        #     "artifacts":{}
        #    }
        # }
        log.info("URL: %s", url)
        log.info("Data: %s", data)
        log.info("Headers: %s", headers)

        j = json.loads(data)
        name = j["name"]

        query_dict = urlparse.parse_qs(urlparse.urlparse(url).query)
        if self.store.get("secret_token"):
            if "secret" not in query_dict:
                log.warn("Jenkins webhook: Missing secret.")
                return ("", 403, {})
            
            # The jenkins Notification plugin does not support any sort of
            # "execute this code on this json object before you send" so we can't
            # send across HMAC SHA1s like with github :( so a secret token will
            # have to do.
            secrets = query_dict["secret"]
            if len(secrets) > 1:
                log.warn("Jenkins webhook: FAILED SECRET TOKEN AUTH. Too many secrets. IP=%s",
                         ip)
                return ("", 403, {})
            elif secrets[0] != self.store.get("secret_token"):
                log.warn("Jenkins webhook: FAILED SECRET TOKEN AUTH. Mismatch. IP=%s",
                         ip)
                return ("", 403, {})
            else:
                log.info("Jenkins webhook: Secret verified.")


        # add the project if we didn't know about it before
        if name not in self.store.get("known_projects"):
            log.info("Added new job: %s", name)
            projects = self.store.get("known_projects")
            projects.append(name)
            self.store.set("known_projects", projects)

        status = j["build"]["status"]
        branch = None
        commit = None
        git_url = None
        jenkins_url = None
        info = ""
        try:
            branch = j["build"]["scm"]["branch"]
            commit = j["build"]["scm"]["commit"]
            git_url = j["build"]["scm"]["url"]
            jenkins_url = j["build"]["full_url"]
            # try to format the git url nicely
            if (git_url.startswith("*****@*****.**") and
                    git_url.endswith(".git")):
                # [email protected]:matrix-org/synapse.git
                org_and_repo = git_url.split(":")[1][:-4]
                commit = "https://github.com/%s/commit/%s" % (org_and_repo, commit)


            info = "%s commit %s - %s" % (branch, commit, jenkins_url)
        except KeyError:
            pass

        fail_key = "%s:%s" % (name, branch)

        if status.upper() != "SUCCESS":
            # complain
            msg = '<font color="red">[%s] <b>%s - %s</b></font>' % (
                name,
                status,
                info
            )

            if fail_key in self.failed_builds:
                info = "%s failing since commit %s - %s" % (branch, self.failed_builds[fail_key]["commit"], jenkins_url)
                msg = '<font color="red">[%s] <b>%s - %s</b></font>' % (
                    name,
                    status,
                    info
                )
            else:  # add it to the list
                self.failed_builds[fail_key] = {
                    "commit": commit
                }

            self.send_message_to_repos(name, msg)
        else:
            # do we need to prod people?
            if fail_key in self.failed_builds:
                info = "%s commit %s" % (branch, commit)
                msg = '<font color="green">[%s] <b>%s - %s</b></font>' % (
                    name,
                    status,
                    info
                )
                self.send_message_to_repos(name, msg)
                self.failed_builds.pop(fail_key)