diff --git a/server/.env.sample b/server/.env.sample index 15d66ed..dc6c471 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -46,6 +46,9 @@ SECRET_KEY=notsecretkey # SLACK_BOT_TOKEN= # SLACK_CHANNEL_ID= +# WEBHOOK_URL= +# WEBHOOK_BEARER_TOKEN= + ## Do not edit this TZ=UTC diff --git a/server/api/controllers/cards/delete.js b/server/api/controllers/cards/delete.js index dad82de..a176574 100755 --- a/server/api/controllers/cards/delete.js +++ b/server/api/controllers/cards/delete.js @@ -28,7 +28,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - let { card } = await sails.helpers.cards + const { card, board } = await sails.helpers.cards .getProjectPath(inputs.id) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); @@ -45,13 +45,14 @@ module.exports = { throw Errors.NOT_ENOUGH_RIGHTS; } - card = await sails.helpers.cards.deleteOne.with({ + const deletedCard = await sails.helpers.cards.deleteOne.with({ record: card, + board, user: currentUser, request: this.req, }); - if (!card) { + if (!deletedCard) { throw Errors.CARD_NOT_FOUND; } diff --git a/server/api/controllers/comment-actions/delete.js b/server/api/controllers/comment-actions/delete.js index b09ce26..f9a47c0 100755 --- a/server/api/controllers/comment-actions/delete.js +++ b/server/api/controllers/comment-actions/delete.js @@ -36,7 +36,7 @@ module.exports = { .intercept('pathNotFound', () => Errors.COMMENT_ACTION_NOT_FOUND); let { action } = path; - const { board, project } = path; + const { board, project, card } = path; const isProjectManager = await sails.helpers.users.isProjectManager(currentUser.id, project.id); @@ -59,9 +59,14 @@ module.exports = { } } - action = await sails.helpers.actions.deleteOne.with({ + action = await sails.helpers.actions.createOne.with({ board, - record: action, + values: { + data: _.pick(inputs, ['id']), + card, + type: Action.Types.DELETE_COMMENT_CARD, + user: currentUser, + }, request: this.req, }); diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js index b03adfd..519b895 100644 --- a/server/api/helpers/actions/create-one.js +++ b/server/api/helpers/actions/create-one.js @@ -14,30 +14,6 @@ const valuesValidator = (value) => { return true; }; -const buildAndSendSlackMessage = async (user, card, action) => { - const cardLink = `<${sails.config.custom.baseUrl}/cards/${card.id}|${card.name}>`; - - let markdown; - switch (action.type) { - case Action.Types.CREATE_CARD: - markdown = `${cardLink} was created by ${user.name} in *${action.data.list.name}*`; - - break; - case Action.Types.MOVE_CARD: - markdown = `${cardLink} was moved by ${user.name} to *${action.data.toList.name}*`; - - break; - case Action.Types.COMMENT_CARD: - markdown = `*${user.name}* commented on ${cardLink}:\n>${action.data.text}`; - - break; - default: - return; - } - - await sails.helpers.utils.sendSlackMessage(markdown); -}; - module.exports = { inputs: { values: { @@ -91,9 +67,7 @@ module.exports = { ), ); - if (sails.config.custom.slackBotToken) { - buildAndSendSlackMessage(values.user, values.card, action); - } + await sails.helpers.utils.sendMessage(action, values.user, values.card, inputs.board); return action; }, diff --git a/server/api/helpers/actions/delete-one.js b/server/api/helpers/actions/delete-one.js index 6b93597..5103e2a 100644 --- a/server/api/helpers/actions/delete-one.js +++ b/server/api/helpers/actions/delete-one.js @@ -4,6 +4,10 @@ module.exports = { type: 'ref', required: true, }, + values: { + type: 'ref', + required: true, + }, board: { type: 'ref', required: true, @@ -25,6 +29,13 @@ module.exports = { }, inputs.request, ); + + await sails.helpers.utils.sendMessage( + action, + inputs.values.user, + inputs.values.card, + inputs.board, + ); } return action; diff --git a/server/api/helpers/cards/delete-one.js b/server/api/helpers/cards/delete-one.js index a947f73..1abe448 100644 --- a/server/api/helpers/cards/delete-one.js +++ b/server/api/helpers/cards/delete-one.js @@ -1,13 +1,13 @@ -const buildAndSendSlackMessage = async (user, card) => { - await sails.helpers.utils.sendSlackMessage(`*${card.name}* was deleted by ${user.name}`); -}; - module.exports = { inputs: { record: { type: 'ref', required: true, }, + board: { + type: 'ref', + required: true, + }, user: { type: 'ref', required: true, @@ -30,9 +30,15 @@ module.exports = { inputs.request, ); - if (sails.config.custom.slackBotToken) { - buildAndSendSlackMessage(inputs.user, card); - } + await sails.helpers.actions.createOne.with({ + values: { + card, + type: Action.Types.DELETE_CARD, + data: {}, + user: inputs.user, + }, + board: inputs.board, + }); } return card; diff --git a/server/api/helpers/utils/send-message.js b/server/api/helpers/utils/send-message.js new file mode 100644 index 0000000..a934312 --- /dev/null +++ b/server/api/helpers/utils/send-message.js @@ -0,0 +1,172 @@ +function buildMessage(user, card, action) { + const cardLink = `<${sails.config.custom.baseUrl}/cards/${card.id}|${card.name}>`; + + let markdown; + switch (action.type) { + case Action.Types.CREATE_CARD: + markdown = `${cardLink} was created by ${user.name} in *${action.data.list.name}*`; + break; + case Action.Types.MOVE_CARD: + markdown = `${cardLink} was moved by ${user.name} to *${action.data.toList.name}*`; + break; + case Action.Types.COMMENT_CARD: + markdown = `*${user.name}* commented on ${cardLink}:\n>${action.data.text}`; + break; + case Action.Types.DELETE_COMMENT_CARD: + markdown = `Comment on ${cardLink} was deleted by ${user.name}`; + break; + case Action.Types.DELETE_CARD: + markdown = `${cardLink} was deleted by ${user.name}`; + break; + default: + return ''; + } + + return markdown; +} + +const handleSlack = () => { + const POST_MESSAGE_API_URL = 'https://slack.com/api/chat.postMessage'; + + function getTokens() { + if (!sails.config.custom.slackBotToken || !sails.config.custom.slackChannelId) { + return false; + } + return { + token: sails.config.custom.slackBotToken, + channel: sails.config.custom.slackChannelId, + }; + } + + const send = async ({ action, user, card }) => { + const tokens = getTokens(); + if (!tokens) { + return; + } + + const markdown = buildMessage(user, card, action); + + if (!markdown) { + sails.log.warn('Missing message markdown. Skipping Slack message. Action:', action.type); + return; + } + + const data = { + channel: tokens.channel, + text: markdown, + as_user: false, + username: user.name, + icon_url: user.avatarUrl, + }; + + try { + const response = await fetch(POST_MESSAGE_API_URL, { + method: 'POST', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${tokens.token}`, + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + sails.log.error('Failed to send Slack message:', errorData); + } else { + const responseJson = await response.json(); + sails.log.debug('Slack message sent successfully:', responseJson); + } + } catch (error) { + sails.log.error('Error sending Slack message:', error); + } + }; + + return { + send, + }; +}; + +const handleWebhook = () => { + function getWebhookUrl() { + return sails.config.custom.webhookUrl || false; + } + + function buildHeaders() { + const bearer = sails.config.custom.webhookBearerToken || false; + + const headers = { + 'Content-Type': 'application/json', + }; + + if (bearer) { + headers.Authorization = `Bearer ${bearer}`; + } + + return headers; + } + + const send = async ({ action, user, card, board }) => { + const url = getWebhookUrl(); + if (!url) { + return; + } + + const markdown = buildMessage(user, card, action); + + const data = { + text: markdown, + action, + board, + card, + }; + + try { + const response = await fetch(url, { + method: 'POST', + headers: buildHeaders(), + body: JSON.stringify(data), + }); + + if (!response.ok) { + const errorData = await response.json(); + sails.log.error('Failed to send Webhook message:', errorData); + } else { + sails.log.debug('Webhook message sent successfully.'); + } + } catch (error) { + sails.log.error('Error sending Webhook message:', error); + } + }; + + return { + send, + }; +}; + +module.exports = { + inputs: { + action: { + type: 'ref', + required: true, + }, + user: { + type: 'ref', + required: true, + }, + card: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + }, + + async fn(inputs) { + const slack = handleSlack(); + const webhook = handleWebhook(); + + await Promise.allSettled([slack.send(inputs), webhook.send(inputs)]); + }, +}; diff --git a/server/api/helpers/utils/send-slack-message.js b/server/api/helpers/utils/send-slack-message.js deleted file mode 100644 index 573fcf8..0000000 --- a/server/api/helpers/utils/send-slack-message.js +++ /dev/null @@ -1,53 +0,0 @@ -const POST_MESSAGE_API_URL = 'https://slack.com/api/chat.postMessage'; - -module.exports = { - inputs: { - markdown: { - type: 'string', - required: true, - }, - }, - - async fn(inputs) { - const headers = { - Authorization: `Bearer ${sails.config.custom.slackBotToken}`, - 'Content-Type': 'application/json; charset=utf-8', - }; - - const body = { - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: inputs.markdown, - }, - }, - ], - channel: sails.config.custom.slackChannelId, - }; - - let response; - try { - response = await fetch(POST_MESSAGE_API_URL, { - headers, - method: 'POST', - body: JSON.stringify(body), - }); - } catch (error) { - sails.log.error(error); // TODO: provide description text? - return; - } - - if (!response.ok) { - sails.log.error('Error sending to Slack: %s', response.error); - return; - } - - const responseJson = await response.json(); - - if (!responseJson.ok) { - sails.log.error('Error sending to Slack: %s', responseJson.error); - } - }, -}; diff --git a/server/api/models/Action.js b/server/api/models/Action.js index 9aedbd1..9e9f183 100755 --- a/server/api/models/Action.js +++ b/server/api/models/Action.js @@ -9,6 +9,8 @@ const Types = { CREATE_CARD: 'createCard', MOVE_CARD: 'moveCard', COMMENT_CARD: 'commentCard', + DELETE_COMMENT_CARD: 'deleteCommentCard', + DELETE_CARD: 'deleteCard', }; module.exports = { diff --git a/server/config/custom.js b/server/config/custom.js index ac344d0..93cfe29 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -61,4 +61,7 @@ module.exports.custom = { slackBotToken: process.env.SLACK_BOT_TOKEN, slackChannelId: process.env.SLACK_CHANNEL_ID, + + webhookUrl: process.env.WEBHOOK_URL, + webhookBearerToken: process.env.WEBHOOK_BEARER_TOKEN, };