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 __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 __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 __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")
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", "")
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
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 __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")
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.")
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 })
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)
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 })
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)
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 })
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)
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)