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
}
}
});
Please sign in to leave a comment.
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!
done, thanks for the heads up!