Custom Event Listener

Working with Custom Event Listeners

The script binding shares information between the script and the application. An event listener variable in the script binding is provided. This variable corresponds to the event which triggered the listener. For example, if you are listening for PageCreateEvent the event variable will be a PageCreateEvent object.

You can choose to have your handler listen for multiple events. If you need to do different things depending on the type of event, you can check that with instanceof. Alternatively, you can type the event variable to the most specific superclass. In the previous example, that would be PageEvent.

In the following example, the event is getting page content for both PageCreateEvent and PageUpdateEvent. Since PageEvent is a common superclass for both PageCreateEvent and PageUpdateEvent, you could cover both events by using the following code:

Include Bitbucket for Confluence: File content cannot be shown

Unauthenticated access to this resource is not allowed. Please login to Confluence first.

Line 1: The event is passed in the binding. This line is only used to give type information when using an IDE, and has no functional impact.

Samples

Add a Comment on Page Create

Some organizations have a particular style guide or would like to enforce specific rules. This event listener example looks at the content of new pages for banned words. If the page content contains any of a list of banned words, a comment is automatically added with an alternative suggestion. The following image is a comment generated in response to a banned word:

The code for this event listener follows:

import com.atlassian.confluence.event.events.content.page.PageEvent
import com.atlassian.confluence.pages.CommentManager
import com.atlassian.sal.api.component.ComponentLocator

def event = event as PageEvent
// use event.getPage().getSpace() if you want to restrict only to certain spaces

def commentManager = ComponentLocator.getComponent(CommentManager)
def body = event.content.bodyAsString

def alternatives = [
    "air hostess": "flight attendant",
    "amuck"      : "amok",
]

def commentBody = alternatives.findAll { badWord, goodWord ->
    body.contains(badWord)
}.collect { badWord, goodWord ->
    "<li><b>${badWord}</b> should be avoided. Consider using: <b>${goodWord}</b>.</li>"
}.join("")

if (commentBody) {
    commentManager.addCommentToObject(event.content, null, "<p>Please consider the following issues: <ul>$commentBody</ul> </p>")
}

In practice, you would also want to watch page updates and only look at the diff between old and new versions.

Add Inline Comment on Page Create

Similar to Add a Comment on a Page Create, you can configure your listener to add inline comments instead. As you get these comments, you can dismiss them. The following image is an example of an inline comment:

The code for this event listener follows:

import com.atlassian.confluence.core.DefaultSaveContext
import com.atlassian.confluence.event.events.content.page.PageEvent
import com.atlassian.confluence.pages.CommentManager
import com.atlassian.confluence.plugins.highlight.SelectionStorageFormatModifier
import com.atlassian.confluence.plugins.highlight.model.TextSearch
import com.atlassian.confluence.plugins.highlight.model.XMLModification
import com.atlassian.sal.api.component.ComponentLocator
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import com.onresolve.scriptrunner.runner.customisers.WithPlugin

import java.time.LocalDateTime

import com.atlassian.confluence.pages.Comment

def commentManager = ComponentLocator.getComponent(CommentManager)

@PluginModule
@WithPlugin("com.atlassian.confluence.plugins.confluence-highlight-actions")
SelectionStorageFormatModifier selectionStorageFormatModifier

event = event as PageEvent

def alternatives = [
    "air hostess": "flight attendant",
    "amuck"      : "amok",
]

/**
 * find occurrence of word in a String
 */
Integer keyFinder(String pageBody, String key) {
    def keyToFind = /$key/
    def keyFinder = (pageBody =~ /$keyToFind/)
    keyFinder.count
}

def inlineCommentMarker = { markerRef ->
    "<ac:inline-comment-marker ac:ref=\"${markerRef}\"></ac:inline-comment-marker>"
}

def commentTemplate = { String word ->
    "<p>Please use <b>${word}</b> instead</p>"
}

alternatives.each { badWord, goodWord ->
    def matches = keyFinder(event.content.bodyAsString, badWord)

    if (matches == 0) {
        log.warn("Word \"${badWord}\" not found.")
        return
    }

    (0..matches - 1).each { matchIndex ->
        def generatedMarkerRef = UUID.randomUUID().toString()

        // highlight text in the content
        selectionStorageFormatModifier.markSelection(
            event.page.id,
            event.timestamp,
            new TextSearch(badWord, matches, matchIndex),
            new XMLModification(inlineCommentMarker(generatedMarkerRef))
        )

        // add an inline comment related to the highlighted text
        def comment = commentManager.addCommentToObject(event.page, null, commentTemplate(goodWord)).tap {
            setInlineComment(true)
            delegate.getProperties().setStringProperty(Comment.MARKER_REF_PROP, generatedMarkerRef)
            delegate.getProperties().setStringProperty(Comment.ORIGINAL_SELECTION_PROP, badWord)
            setLastModificationDate(LocalDateTime.now().toDate())
        }
        commentManager.saveContentEntity(comment, DefaultSaveContext.DEFAULT)

        log.warn("Found word \"${badWord}\". Inline commented with word \"${goodWord}\"")
    }
}

Create Page When User Created

This event listener example automatically creates a user profile page in the Team space. You can use this profile page to list their skills and profile. The following image is an example of a profile page:

The event selected for this example is UserCreateEvent, and the code follows:

import com.atlassian.confluence.core.DefaultSaveContext
import com.atlassian.confluence.event.events.user.UserCreateEvent
import com.atlassian.confluence.pages.Page
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.user.ConfluenceUser
import com.atlassian.sal.api.component.ComponentLocator
import groovy.xml.MarkupBuilder

try {
    def event = event as UserCreateEvent
    def user = event.user as ConfluenceUser
    def pageManager = ComponentLocator.getComponent(PageManager)

    def spaceManager = ComponentLocator.getComponent(SpaceManager)
    def teamSpace = spaceManager.getSpace("TEAM")

    def writer = new StringWriter()
    def builder = new MarkupBuilder(writer)
    builder.table {
        tbody {
            tr {
                td("About")
                td {
                    "ac:link" {
                        "ri:user"("ri:userkey": user.key)
                    }
                }
            }
            tr {
                td("Profile")
                td("")
            }
            tr {
                td("Skillz")
                td("")
            }
        }
    }

    def parentPage = teamSpace.getHomePage()
    assert parentPage

    def targetPage = new Page(title: "About ${user.fullName}",
        bodyAsString: writer.toString(),
        space: teamSpace,
        parentPage: parentPage
    )
    pageManager.saveContentEntity(targetPage, DefaultSaveContext.DEFAULT)
    parentPage.addChild(targetPage)
    pageManager.saveContentEntity(parentPage, DefaultSaveContext.MINOR_EDIT)
}
catch (anyException) {
    log.warn("Failed to create page for new user", anyException)
}

Collecting Stats

This event listener example automatically sends statistics to statsd for page views, space views, and users/pages views. The following image is an example of the statistics for page views:

In the example, Grafana is used to visualize page views metrics.

The event selected for this example is PageViewEvent, and the code follows:

import com.atlassian.confluence.event.events.content.page.PageEvent
import com.atlassian.confluence.user.AuthenticatedUserThreadLocal
import groovy.transform.Field

@Field final def host = "http://192.168.59.103/"
@Field final def port = 8125

def event = event as PageEvent
def currentUser = AuthenticatedUserThreadLocal.get()

// keys to create unique nodes for counters
def spaceKey = event.page.spaceKey
def pageId = event.page.id as String
def userKey = currentUser.name
def nodeId = "confluence.stats.views"

// build the unique metric keys
def pageViewMetricKey = "${nodeId}.page.${pageId}"
def spaceViewMetricKey = "${nodeId}.space.${spaceKey}"
def userViewMetricKey = "${nodeId}.user.${userKey}.${pageId}"

// increase by one the counters for the following metric keys
increaseByOne(pageViewMetricKey, userViewMetricKey, spaceViewMetricKey)

void increaseByOne(String... keys) {
    def dataToSend = ""
    def value = 1 //increase counter by one

    //syntax for counter according to https://github.com/etsy/statsd/blob/master/docs/metric_types.md
    for (key in keys) {
        dataToSend += "${key}:${value}|c\n"
    }

    def data = dataToSend.getBytes()
    def address = InetAddress.getByName(host as String)
    def packet = new DatagramPacket(data, data.length, address, port as int)
    def socket = new DatagramSocket()
    try {
        socket.send(packet)
    } finally {
        socket.close()
    }
}

Creating a Jira Project Whenever a Confluence Space Is Created

This event listener example automatically creates a Jira project every time a space is created.

The event selected for this example is SpaceCreateEvent, and the code follows:

import com.atlassian.applinks.api.ApplicationLinkService
import com.atlassian.applinks.api.application.jira.JiraApplicationType
import com.atlassian.confluence.event.events.space.SpaceCreateEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.sal.api.net.Response
import com.atlassian.sal.api.net.ResponseException
import com.atlassian.sal.api.net.ResponseHandler
import groovy.json.JsonBuilder

import static com.atlassian.sal.api.net.Request.MethodType.POST

def appLinkService = ComponentLocator.getComponent(ApplicationLinkService)
def appLink = appLinkService.getPrimaryApplicationLink(JiraApplicationType)
def applicationLinkRequestFactory = appLink.createAuthenticatedRequestFactory()

def event = event as SpaceCreateEvent
def space = event.space

def input = new JsonBuilder([
    projectTypeKey    : "business",
    projectTemplateKey: "com.atlassian.jira-core-project-templates:jira-core-task-management",
    name              : space.name,
    key               : space.key,
    lead              : event.space.creator.name,
]).toString()

def request = applicationLinkRequestFactory.createRequest(POST, "/rest/api/2/project")
    .addHeader("Content-Type", "application/json")
    .setEntity(input)

request.execute(new ResponseHandler<Response>() {
    @Override
    void handle(Response response) throws ResponseException {
        if (response.statusCode != 201) {
            log.error("Creating jira project failed: ${response.responseBodyAsString}")
        }
    }
})

Creating a Page When a Form Is Submitted

This event listener example automatically creates a new page when a form is submitted.

If you have Forms for Confluence installed, you can listen for the FormSubmitEvent and run a script to create a new page. You can specify the space, title, and page content of the new page.

Follow these steps to create a new page when a user submits an internal feature request or events proposal using Forms for Confluence.

  1. Create a new form that includes input fields to collect a specified pageTitle, spaceKey, and pageContent.

    You can also include additional fields, like an attachment.

  2. Configure the event listener for FormsSubmitEvent where the spaceKey, pageTitle, and pageContent corresponds to the name parameter defined for the macros in your form.

    When the form is submitted, a new page is created with the values entered by the user.

    The code for this example follows:

    import com.atlassian.confluence.core.DefaultSaveContext
    import com.atlassian.confluence.pages.Page
    import com.atlassian.confluence.pages.PageManager
    import com.atlassian.confluence.spaces.SpaceManager
    import com.atlassian.sal.api.component.ComponentLocator
    import com.atlassian.xwork.FileUploadUtils
    import groovy.json.JsonSlurper
    
    def spaceManager = ComponentLocator.getComponent(SpaceManager)
    def pageManager = ComponentLocator.getComponent(PageManager)
    
    def eventFormSubmission = binding.variables.get("event")
    
    String eventData = eventFormSubmission.structuredValue
    
    //  any uploaded files. ie - uploaded using the "Forms - Attachment" macro
    List<FileUploadUtils.UploadedFile> uploadedFiles = eventFormSubmission.uploadedFiles
    
    // Space key, page title, page content, in a map.
    Map<String, String[]> inputtedValues = new JsonSlurper().parseText(eventData) as Map<String, String[]>
    
    def parentPageTitle = getValue(inputtedValues, "parentPageTitle")
    def pageTitle = getValue(inputtedValues, "pageTitle")
    def spaceKey = getValue(inputtedValues, "spaceKey")
    def pageContent = getValue(inputtedValues, "pageContent")
    
    // construct a new page
    Page targetPage = constructNewPage(spaceManager, spaceKey, pageTitle, pageContent)
    
    validateSpace(spaceKey, spaceManager)
    validateParentPage(spaceKey, parentPageTitle, pageManager)
    
    setPageAncestryAndSave(parentPageTitle, targetPage, spaceKey, pageManager)
    
    private static String getValue(Map<String, String[]> data, String key) {
        if (!data.get(key) || data.get(key)[0].isEmpty()) {
            throw new IllegalArgumentException("A \"" + key + "\" was not provided.")
        } else if (hasMultipleUniqueEntries(data.get(key) as List<String>)) {
            throw new IllegalArgumentException("multiple \" " + key + "\"'s were provided, please enter a single \"" + key + "\"")
        }
        data.get(key)[0]
    }
    
    private static boolean hasMultipleUniqueEntries(List<String> entries) {
        Set uniqueEntries = [] as Set
        uniqueEntries.addAll(entries)
        uniqueEntries.size() != 1
    }
    
    private static Page constructNewPage(SpaceManager spaceManager, String spaceKey, String pageTitle, String pageContent) {
        def targetPage = new Page(
            space: spaceManager.getSpace(spaceKey),
            title: pageTitle,
            bodyAsString: pageContent,
        )
        targetPage
    }
    
    private static void validateSpace(String spaceKey, SpaceManager spaceManager) {
        def space = spaceManager.getSpace(spaceKey)
        if (space == null) {
            throw new IllegalArgumentException("invalid space key")
        }
    }
    
    private static void validateParentPage(String spaceKey, String parentPageTitle, PageManager pageManager) {
        def parentPage = pageManager.getPage(spaceKey, parentPageTitle)
        if (parentPage == null) {
            throw new IllegalArgumentException("invalid parentPageTitle. " + parentPageTitle + " is not found in " + spaceKey)
        }
    }
    
    private static void setPageAncestryAndSave(
        String parentPageTitle, Page targetPage, String spaceKey, PageManager pageManager
    ) {
        Page parentPage = pageManager.getPage(spaceKey, parentPageTitle)
        parentPage.addChild(targetPage)
        targetPage.setParentPage(parentPage)
    
        pageManager.saveContentEntity(parentPage, DefaultSaveContext.DEFAULT)
        pageManager.saveContentEntity(targetPage, DefaultSaveContext.DEFAULT)
    }
    


Results

The following image is the resulting form:

The following image is the resulting page that is created when the form is submitted:

This event listener is not directly tied to Forms configuration. The results of the Forms configuration can be recorded and/or sent to a different destination that you choose.

On this page