Send a Slack notification when issues are created or (re)assigned

This script sends a notification on a specified Slack channel whenever an issue is created or modified.

Example of notifications sent automatically to a Slack channel using user scripting.

The script also sends a direct message from the Slack app bot to notify employees whenever an issue has been assigned to them.

Example of notifications sent automatically as a direct message in Slack using user scripting.

Two different methods are used to post the messages. An incoming webhook is used to post messages on a specific Slack channel. Slack API methods conversations.open and chat.postMessage are used to post direct messages to users from the app bot user. The script also uses the Slack API method users.lookupByEmail to identify the Slack users direct messages should posted to.

Follow the steps below or download the solutions file, see Creating Solutions for details.

Note:

You will need to create, configure and install a Slack app associated to your workspace. See Setup 1 — Create, Configure and Install a Slack App.

Four setup steps are required for this example

Setup 1 — Create, Configure and Install a Slack App

  1. Open the Slack desktop application and sign in to your Slack workspace using an Administrator account.

  2. Click your workspace name in the top left.

  3. Select Administration > Manage apps. This will open the app directory for your workspace on a new tab in your default web browser.

  4. Click Build on the top right. This will redirect you to the Slack API documentation web page.

  5. Click Start Building. The Create a Slack App dialog appears.

  6. Enter the name of your application and the workspace associated with the app.

    Create a Slack App popup window in Slack.
  7. Click Create App. You will be redirected to the Basic Information screen for your Slack app.

  8. Click Incoming Webhooks under Add Features and Functionality. The Incoming Webhooks screen will display.

  9. Enable the Activate Incoming Webhooks toggle.

  10. Navigate back to the Basic Information page.

  11. Click Bots under Add Features and Functionality. The Bot User screen will display.

  12. Click Add a Bot User and enter a Display name and a Default username for your bot. Optionally, enable the Always Show My Bot as Online toggle.

    Bot User popup window in Slack.
  13. Click Add Bot User. A confirmation message displays.

  14. Navigate back to the Basic Information page and click Permissions under Add Features and Functionality. The OAuth & Permissions screen will display.

  15. Scroll down to the Scopes section. The following permission scopes should already be listed:

    • Post to a specific channel in Slack (incoming-webhook)

    • Add a bot user with the username @yourbot_default_username (bot)

  16. Use the Select permission scopes dropdown to add the following permission scopes:

    • Send messages as Yourbot_Display_Name (chat:write:bot)

    • Access your workspace’s profile information (users:read)

    • View email addresses of people on this workspace (users:read.email)

    Scopes pop up window in Slack.
  17. Click Save Changes.

  18. Scroll back to the top of the page and click Install App to Workspace under OAuth Tokens & Redirect URLs. A new screen will display.

  19. Review the permission scopes for the app you are about to install and select the channel messages will be posted to using the Incoming Webhook.

    App authorization pop up window in Slack.

    In this example, Slack users are identified using their email address. This requires adding the permission scopes users:read and users:read.email. The users:read permission scope enables the app to access profile information for all users on the Slack workspace. If this is not desirable, an alternative method to identify users for sending direct messages would be to use a custom field in OpenAir to store a Slack user ID in the User records.

  20. Click Authorize. You will be redirected back to the OAuth & Permissions screen.

  21. Take a note of the Bot User OAuth Access Token under OAuth Tokens & Redirect URLs.

    OAuth token settings in Slack
  22. Click the Incoming Webhook link on the left and take a note of the Webhook URL. You will need this when setting the script parameters in Setup 2 — Script Parameters.

    Incoming Webhooks settings in Slack.

Posting messages on a specified Slack channel as an app bot user can be done using a Webhook URL. In order to post direct messages to Slack users as an app bot user, a Bot User OAuth Access Token is required. You will need to set these as script parameters in Setup 2 — Script Parameters.

Setup 2 — Script Parameters

  1. Sign in to OpenAir as an Administrator and go to the OpenAir Scripting Center.

  2. Create and set the following Password parameters:

    For more information about creating parameters, see Creating Parameters.

  3. Create and set the following Text parameter:

    • SlackUrl — Set the value for this parameter to your Slack workspace URL (e.g. https://myslackworkspace.slack.com).

Parameters list view in OpenAir Scripting Center showing the parameters for this use case.

The parameters created will be referenced in the library script created in Setup 3 — Slack Notification Library Script.

Setup 3 — Slack Notification Library Script

  1. Create a new Library script deployment with the filename SlackMessageReIssues.js

    For more information about creating library scripts, see Creating Library Scripts.

  2. Locate and open the library script you have just created.

  3. Reference the three parameters created in Setup 2 — Script Parameters.

  4. Copy the script below and paste it in the Scripting Studio editor.

                    /*
    
    LIBRARY SCRIPT USED FOR ISSUE AND PROJECT ISSUE FORM SCRIPTS 
    
    SLACK MESSAGING REFERENCE
        > https://api.slack.com/messaging
    
    SLACK MESSAGE ATTACHMENT REFERENCE
        > https://api.slack.com/docs/message-attachments
    
     */
    
    /**
     * Post a message to slack after creating or modifying an issue.
     * @param  {Str} type Standard entrance function type.
     */
    
    function postIssuesOnSlack(type) {
    
          // Retrieve parameters required to post to the Slack workspace or channel
           
          var slackBotAuth = NSOA.context.getParameter('SlackBotOAuthAccessToken');
          var slackUrl = NSOA.context.getParameter('SlackUrl');
          var slackWebhook = NSOA.context.getParameter('SlackWebhookUrlForIssuesNotifications');
             
          // Only proceed if all the required parameters have been set
       
          if (!slackBotAuth || slackBotAuth.length === 0 || !slackUrl || slackUrl.length === 0  || !slackWebhook || slackWebhook.length === 0) { return; }
        
          // Only proceed if the issue record is new or has been modified
          var issue = NSOA.form.getNewRecord();
          var issueReAssigned = false;
          if (type !== 'new') {
                // Get issue record from database with the newly saved values and the previous values
                var issueOld = NSOA.form.getOldRecord();
                if (issue.updated === issueOld.updated) {return;}
                if (issue.user_id !== issueOld.user_id) issueReAssigned = true;
          }
    
          // Record the SOAP API requests and responses in the log 
          NSOA.wsapi.enableLog(true);
        
          // Execute the script independently of user filter sets
          NSOA.wsapi.disableFilterSet(true);  
        
          // Get user, project, issue severity and issue stage records associated with the issue
          var user = NSOA.record.oaUser(issue.user_id);
          var project = NSOA.record.oaProject(issue.project_id);
          var issueseverity = NSOA.record.oaIssueSeverity(issue.issue_severity_id);
          var issuestage = NSOA.record.oaIssueStage(issue.issue_stage_id);
        
          // Construct Slack messages content and attachments
    
          var issName = issue.name;
          var issDescr = issue.description;
          var issSeverity = issueseverity.name;
          var issStage = issuestage.name;
          var issNotes = issue.issue_notes;
          var issAssignee = user.name;
          var prjName = project.name;
          var prjCustName = project.customer_name;
          var issUpdated = issue.updated;
          var issCreated = issue.created;
    
          var createdEpoch = convertTimestampToEpoch(issUpdated);
          
          var attachmenticon = 'https://www.pngrepo.com/download/30662/warning.png';
          var messagetext = 'Issue *' + issue.name + '* has been modified.';   
          var attachmenttitle = 'Issue Updated';
          var attachmentcolor = 'warning';
    
          if (type === 'new') {
              attachmenttitle = 'New Issue';
                messagetext = 'An issue has been created on *' + prjCustName + ' : ' + prjName + '*.';
                attachmentcolor = 'danger';
                createdEpoch = convertTimestampToEpoch(issCreated);
          }
    
          if (issuestage.considered_closed) {
                messagetext = 'Issue *' + issue.name + '* is ' + issuestage.name + '.';
                attachmenttitle = 'Issue ' + issuestage.name; 
                attachmentcolor = 'good';
                issNotes = issue.resolution_notes;
                attachmenticon = 'https://www.pngrepo.com/download/167754/checked.png';
          }
          
          var fields = [
                {
                  title: 'Issue',
                  value: issName + ': ' + issDescr,
                  short: false
               },
               {
                  title: 'Customer : Project',
                  value: prjCustName + ' : ' + prjName,
                  short: false
               },
               {
                  title: 'Severity',
                  value: issSeverity,
                  short: true
               },
               {
                  title: 'Stage',
                  value: issStage,
                  short: true
               },
               {
                  title: 'Assigned to',
                  value: issAssignee,
                  short: false
               },
               {
                  title: 'Notes',
                  value: issNotes,
                  short: false
               }
          ];
          
          var issueattachment = {
               fallback: messagetext,
               color: attachmentcolor,
               author_name: attachmenttitle,
               author_icon: attachmenticon,
              fields: fields,
               footer: 'OpenAir User Scripting API',
               footer_icon: 'https://www.pngrepo.com/download/36709/programming-code-signs.png',
               ts: createdEpoch
          };
    
          // Post message onto slack channel using Webhook URL
          var response = postMessageToSlackChannel(slackWebhook, messagetext, [issueattachment]);
    
          // Post direct message to assignee if issue newly (re)assigned
          if (((type === 'new' && issue.user_id) || issueReAssigned )&& slackBotAuth) {
            
                var assignedmessagetext = 'Issue *' + issue.name + '* has been assigned to you.';
    
                var issueassignedattachment = {
                     fallback: assignedmessagetext,
                    color: 'danger',
                     author_name: 'Issue Assigned',
                     author_icon: 'https://www.pngrepo.com/download/30662/warning.png',
                     fields: fields,
                     footer: 'OpenAir User Scripting API',
                     footer_icon: 'https://www.pngrepo.com/download/36709/programming-code-signs.png',
                     ts: createdEpoch
              };
            
                response = postSlackDirectMessage (slackUrl, slackBotAuth, assignedmessagetext, user.addr_email, [issueassignedattachment]);
          }
    }
    
    exports.postIssuesOnSlack = postIssuesOnSlack;
    
    /**
     * Post a message to a slack channel using a webhook URL.
     * @param  {Str}   url         The webhook url to post a message on a specific channel (required). 
     * @param  {Str}   text        Text to display on message (required).
     * @param  {Array} attachments Array of attachment objects - must be provided if text param empty (optional).
     * @return {Obj}               An https.post response object.
     */
    function postMessageToSlackChannel (url, text, attachments) {
    
          // Check that url parameter has a value, otherwise return
          url = url || '';
          if (!url || url.length === 0) { return null; }
    
          // Check there's something to post, otherwise return
          text = text || '';
          if (!text || text.length === 0 || !(attachments && Object.prototype.toString.call(attachments) === '[object Array]')) { return null; }
    
          var body = {};
        
          //If text param is provided and not empty
          if (text && text.length!==0) { body.text = text; }
    
          // If attachments param is provided, and it is of type Array (isArray method isn't supported...)
          if (attachments && Object.prototype.toString.call(attachments) === '[object Array]') { body.attachments = attachments; }
    
          var headers = {
                'Content-Type': 'application/json'
          };
    
          var response = NSOA.https.post({
                url: url,
                body: body,
                headers: headers
          });
        
          return response;
    }
    
    /**
     * Post a Direct Message on Slack as bot using the Slack Web API.
     * @param  {Str}     url            The url for the slack workspace (required). 
     * @param  {Str}     auth           Authorization token (required)
     * @param  {Str}     text           Text to display on message (required).
     * @param  {Str}     recipient      The email address of the recipient (required).
     * @param  {Array}   attachments    Array of attachment objects - must be provided if text param empty (optional).
     * @return {Obj}                    An https.post response object.
     */
    
    function postSlackDirectMessage (url, auth, text, recipient, attachments) {
        
          // Check there's a message to post, otherwise return
          text = text || '';    
          if (!text || text.length === 0 || !(attachments && Object.prototype.toString.call(attachments) === '[object Array]')) { return null; }
        
          // Get recipient Slack User ID if found, otherwise return
          var recipientId = getSlackUserId (url, auth, recipient);
        
          if (!recipientId) { return null; }
           
          //Construct headers 
          var headers = {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + auth
          };
       
          // Open Conversation and get Slack Channel ID - return if Slack Channel not identified
          var request = {
                url : url + '/api/conversations.open',
                body: { users: recipientId },
                headers: headers
          };
        
          var response = NSOA.https.post(request);
        
          if (!response.body.channel.id) { return null; }
        
          var channelId = response.body.channel.id;
        
          //Construct body 
          var body = {channel: channelId};
        
          //If text param is provided and not empty, append to body
          if (text && text.length!==0) { body.text = text; }
    
          // If attachments param is provided, and it is of type Array (isArray method isn't supported...), append to body
          if (attachments && Object.prototype.toString.call(attachments) === '[object Array]') { body.attachments = attachments; }
    
          request = {
                url: url + '/api/chat.postMessage',
                body: body,
                headers: headers
          };
    
          response = NSOA.https.post(request);
        
          return response;
    }
    
    /**
     * Lookup Slack user ID by email.
     * @param  {Str}     url        The url for the slack workspace (required). 
     * @param  {Str}     auth       Authorization token (required)
     * @param  {Str}     email      The email address of the user (required).
     * @return {Str}                A Slack user ID. 
     */
    
    function getSlackUserId (url, auth, email) {
        
          // Check that url parameter has a value, otherwise return
          url = url || '';
          if (!url || url.length === 0) { return null; }
    
          // Check that auth parameter has a value, otherwise return
          auth = auth || '';
          if (!auth || auth.length === 0) { return null; }
        
          // Check that email parameter has a value, otherwise return
          email = email || '';    
          if (!email || email.length === 0) { return null; }
        
          // Get recipient Slack User ID if found, otherwise return
        
          var request = {
                url: url + '/api/users.lookupByEmail?token=' + auth + '&email=' + email,
                headers: {'Content-type': 'application/x-www-form-urlencoded'}
          };
        
          var response = NSOA.https.get(request);
        
          if (response.body.ok) {return response.body.user.id;}
          else return null;
    }
    
    /**
     * Converts an OpenAir datetime string into a javascript date object.
     * @private
     * @param  {Str} dateStr Datetime string.
     * @return {Obj}         Date object.
     */
    function _convertStringToDateParts(dateStr) {
          var regEx = /^(\d{4})-(\d{2})-(\d{2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/;
          var match = regEx.exec(dateStr);
    
          var year    = match[1];
          var month   = match[2] - 1;
          var day     = match[3];
          var hours   = match[4];
          var minutes = match[5];
          var seconds = match[6];
    
          var d = new Date(year, month, day, hours, minutes, seconds);
    
          return d;
    }
    
    /**
     * Converts an OpenAir datetime string to epoch time.
     * @param  {Str} dateStr An OpenAir datetime string.
     * @return {Int}         An epoch date value.
     */
    function convertTimestampToEpoch(dateStr) {
          var d = _convertStringToDateParts(dateStr);
          return d.getTime() / 1000;
    }
    
    /**
     * Converts Names from Surname, Fistname format to Firstname Surname.
     * @private
     * @param  {Str} name Full name formatted Surname, Firstname.
     * @return {Str}      Full name formatted Firstname Surname.
     */
    function _surCommaFirstToFirstSpaceSur(name) {
          var regEx = /^([^,]+), (.+?)$/g;
          return name.replace(regEx, '$2 $1');
    } 
    
                  
  5. Click Save.

The library script will be referenced by the two form scripts created in Setup 4 — Issue After Save / Project Issue After Save.

Setup 4 — Issue After Save / Project Issue After Save

  1. Create a new Issue form script deployment and give it a filename.

    For more information about creating form scripts, see Creating Form Scripts.

  2. Reference the library script SlackMessageReIssues.js created in Setup 3 — Slack Notification Library Script.

  3. Copy the script below and paste it in the Scripting Studio editor.

                    function afterSaveIssue(type) {
        var SlackMessageReIssues = require('SlackMessageReIssues');
        SlackMessageReIssues.postIssuesOnSlack(type);
    } 
    
                  
  4. Click Save & continue editing.

  5. Set the event triggering the execution of the script to After Save.

  6. Set the Entrance Function to afterSaveIssue.

  7. Click Save.

  8. Create a new project Issue form script deployment and give it a filename.

  9. Repeat steps 2 — 7.