Discord notificaton workflow

Hey all, 

I've updated the slack notification workflow for Discord and added a few features: 

  • notify about issue comments and more status changes 
  • use discord-native payload instead of their legacy slack hook - 
  • mention (@-tag) discord users (you need to add a mapping between your youtrack users and their numerical(!) discord IDs) 

@-mentions are smart about omitting mentions if editor == assignee, comment author == potential notify target, etc.

/**
 * Copyright JetBrains s.r.o. & Anatol Ulrich/@spookyvision
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Thanks to Michael Rush for the original version of this rule (https://software-development.dfstudio.com/youtracks-new-javascript-workflows-make-slack-integration-a-breeze-d3275605d565)

// Discord updates by Anatol Ulrich/@spookyvision:
//  - include issue comments and more status changes
//  - use discord instead of slack, and prefer discord-native payload instead of their legacy slack hook
// - mention (@-tag) discord users  

const DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/...';

const entities = require('@jetbrains/youtrack-scripting-api/entities');
const http = require('@jetbrains/youtrack-scripting-api/http');


// add your discord user IDs here
// keys are yt_user.login
// values are *numerical* discord IDs (NOT login names/legacy aliases) - those can be discovered by enabling developer mode
// in discord settings - advanced
// and then in a user's profile popout -> [...] icon -> "copy user ID"
// for your OWN user ID, click your avatar and then "copy user ID".
const user_lut = {
  'admin': '11111',
  'youtrack_user1': '22222',
  'yt_u2': '3333',
}

function format_discord_mention(yt_id) {
  const discord_id = user_lut[yt_id];
  if (!discord_id) {return null;}
  return `<@${discord_id}>`;
}

exports.rule = entities.Issue.onChange({
  title: 'Send notification to Discord when an issue is updated',
  guard: (ctx) => {
    return ctx.issue.becomesReported || ctx.issue.becomesResolved || ctx.issue.becomesUnresolved || ctx.issue.isReported;
  },
  action: (ctx) => {
    const issue = ctx.issue;
    const assignee_changed = issue.isChanged('Assignee');
    const description_changed = issue.isChanged('description');
    const statusChange = issue.becomesReported || issue.becomesResolved || issue.becomesUnresolved || description_changed || assignee_changed;
    const added_comments = issue.comments.added;
    const commented = !!added_comments;
    
    
    if (!statusChange && !commented) {
      return;
    }

    let message;
    let isNew;
    let fields;
    let editor_id;
    const issue_title = `[${issue.id} - ${issue.summary}](${issue.url})`;
    const assignee_id = issue.fields.Assignee ? issue.fields.Assignee.login : null;
    const mentions = {};

    
    // issue created/updated
    if (statusChange) {
      if (issue.becomesReported) {
        message = 'Created: ';
        isNew = true;
      } else if (issue.becomesResolved) {
        message = 'Resolved: ';
        isNew = false;
      } else if (issue.becomesUnresolved) {
        message = 'Reopened: ';
        isNew = false;
      } else if (assignee_changed) {
        message = 'Reassigned: ';
        isNew = false;
      } else if (description_changed) {
        message = 'Description updated: ';
        isNew = false;
      }

      message += issue_title;


      let changedByTitle = '';
      let changedByName = '';
      

      if (isNew) {
        changedByTitle = 'Created By';
        changedByName = issue.reporter.fullName;
        editor_id = issue.reporter.login;
      } else {
        changedByTitle = 'Updated By';
        changedByName = issue.updatedBy.fullName;
        editor_id = issue.updatedBy.login;
      }

      fields = [{
          'name': 'State',
          'value': issue.fields.State.name,
          'inline': true
        },
        {
          'name': 'Priority',
          'value': issue.fields.Priority.name,
          'inline': true
        },
        {
          'name': 'Assignee',
          'value': issue.fields.Assignee ? issue.fields.Assignee.fullName : '',
          'inline': true
        },
        {
          'name': changedByTitle,
          'value': changedByName,
          'inline': true
        },
      ];
      
      // if issue is updated by someone who isn't the assignee, mention assignee

      if (editor_id != assignee_id) {
          mentions[assignee_id] = true;
      }

    } else { // issue has new comments or other edits took place
      message = `new comments for ${issue_title}`;
      const comments = [];
      
      added_comments.forEach(function(comment) {
        // I think this check is needed because work hours logged to an issue also count as comments
        // we want to skip those
        if (comment.text.length > 0 ) {
          
          // if someone comments who isn't the issue assignee, mention assignee
          if (comment.author.login != assignee_id) {
              mentions[assignee_id] = true;
          }

          comments.push(`${comment.author.fullName}: ${comment.text}`);
          for (let yt_id of Object.keys(user_lut)) {
			// skip mentioning comment author
            const author_id = comment.author.login;
            if (yt_id == author_id) { 
              continue; 
            }
            if (comment.text.includes('@' + yt_id)) {
              mentions[yt_id] = true;
            }
          }
        }
      });
      
      
      if (comments.length == 0) {
		console.info("no new comments found, bailing out");
        return;
      }
      
      // if there are multiple comments, separate them with newline-dash-newline
      // (I have not encountered this in practice, maybe only happens in very high volume environments)
      fields = [{
          'name': 'Content',
          'value': comments.join("\n-\n"),
        },

      ];
    }

    // add all valid mentions and format a mention message if there are any
	const mention_list = [];
	for (const yt_id of Object.keys(mentions)) {
      const discord_mention = format_discord_mention(yt_id);
      if (discord_mention) {
        mention_list.push(discord_mention);
      }
    }
    if (mention_list.length > 0) {
      const mention_message = mention_list.join(" ");
      message += `\nping ${mention_message}`;
    }
    
    
    const payload = {
      'content': message,
	  'allowed_mentions': {
        'parse': ['users', 'roles']
      },
	  'embeds': [{ 'fields': fields  }]
	};

    const connection = new http.Connection(DISCORD_WEBHOOK_URL, null, 2000);
    connection.addHeader({
      name: 'Content-Type',
      value: 'application/json'
    });
    const response = connection.postSync('', null, JSON.stringify(payload));
    if (!response.isSuccess) {
      console.warn('Failed to post notification to Discord. Details: ' + response.toString());
    }
  },
  requirements: {
    Priority: {
      type: entities.EnumField.fieldType
    },
    State: {
      type: entities.State.fieldType
    },
    Assignee: {
      type: entities.User.fieldType
    }
  }
});
0
2 comments

Hi Anatol! Thanks for sharing your custom workflow. We have an existing request for a native integration with Discord: JT-58155. If you want to share your approach with more people, you could leave a comment about it in JT-58155. In any case, we appreciate your input!

0

done, thanks for the heads up!

0

Please sign in to leave a comment.