# Zulip trac plugin -- sends zulips when tickets change. # # Install by copying this file and zulip_trac_config.py to the trac # plugins/ subdirectory, customizing the constants in # zulip_trac_config.py, and then adding "zulip_trac" to the # components section of the conf/trac.ini file, like so: # # [components] # zulip_trac = enabled # # You may then need to restart trac (or restart Apache) for the bot # (or changes to the bot) to actually be loaded by trac. import os.path import sys from trac.core import Component, implements from trac.ticket import ITicketChangeListener sys.path.insert(0, os.path.dirname(__file__)) import zulip_trac_config as config VERSION = "0.9" from typing import Any, Dict if config.ZULIP_API_PATH is not None: sys.path.append(config.ZULIP_API_PATH) import zulip client = zulip.Client( email=config.ZULIP_USER, site=config.ZULIP_SITE, api_key=config.ZULIP_API_KEY, client="ZulipTrac/" + VERSION, ) def markdown_ticket_url(ticket: Any, heading: str = "ticket") -> str: return "[%s #%s](%s/%s)" % (heading, ticket.id, config.TRAC_BASE_TICKET_URL, ticket.id) def markdown_block(desc: str) -> str: return "\n\n>" + "\n> ".join(desc.split("\n")) + "\n" def truncate(string: str, length: int) -> str: if len(string) <= length: return string return string[: length - 3] + "..." def trac_subject(ticket: Any) -> str: return truncate("#%s: %s" % (ticket.id, ticket.values.get("summary")), 60) def send_update(ticket: Any, content: str) -> None: client.send_message( { "type": "stream", "to": config.STREAM_FOR_NOTIFICATIONS, "content": content, "subject": trac_subject(ticket), } ) class ZulipPlugin(Component): implements(ITicketChangeListener) def ticket_created(self, ticket: Any) -> None: """Called when a ticket is created.""" content = "%s created %s in component **%s**, priority **%s**:\n" % ( ticket.values.get("reporter"), markdown_ticket_url(ticket), ticket.values.get("component"), ticket.values.get("priority"), ) # Include the full subject if it will be truncated if len(ticket.values.get("summary")) > 60: content += "**%s**\n" % (ticket.values.get("summary"),) if ticket.values.get("description") != "": content += "%s" % (markdown_block(ticket.values.get("description")),) send_update(ticket, content) def ticket_changed( self, ticket: Any, comment: str, author: str, old_values: Dict[str, Any] ) -> None: """Called when a ticket is modified. `old_values` is a dictionary containing the previous values of the fields that have changed. """ if not ( set(old_values.keys()).intersection(set(config.TRAC_NOTIFY_FIELDS)) or (comment and "comment" in set(config.TRAC_NOTIFY_FIELDS)) ): return content = "%s updated %s" % (author, markdown_ticket_url(ticket)) if comment: content += ' with comment: %s\n\n' % (markdown_block(comment),) else: content += ":\n\n" field_changes = [] for key, value in old_values.items(): if key == "description": content += '- Changed %s from %s\n\nto %s' % ( key, markdown_block(value), markdown_block(ticket.values.get(key)), ) elif old_values.get(key) == "": field_changes.append('%s: => **%s**' % (key, ticket.values.get(key))) elif ticket.values.get(key) == "": field_changes.append('%s: **%s** => ""' % (key, old_values.get(key))) else: field_changes.append( '%s: **%s** => **%s**' % (key, old_values.get(key), ticket.values.get(key)) ) content += ", ".join(field_changes) send_update(ticket, content) def ticket_deleted(self, ticket: Any) -> None: """Called when a ticket is deleted.""" content = "%s was deleted." % (markdown_ticket_url(ticket, heading="Ticket"),) send_update(ticket, content)