Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
53ab5b8
Add share reaction collection and update recognize to use share channel
RobertKelly Jan 17, 2025
29bf4e2
Add share reaction collection and update recognize to use share channel
RobertKelly Jan 17, 2025
e45f748
Add share reaction collection and update recognize to use share channel
RobertKelly Jan 18, 2025
bfe017a
Updating to the #share-me-please channel id C04RRPC9S1E
RobertKelly Jan 18, 2025
0a64cd7
Adding tests for the recognize feature
RobertKelly Jan 18, 2025
231f628
chore(deps-dev): bump @commitlint/config-conventional (#645)
dependabot[bot] Nov 25, 2024
92d9568
chore(deps-dev): bump husky from 9.1.6 to 9.1.7 (#646)
dependabot[bot] Nov 25, 2024
4889e32
chore(deps-dev): bump eslint from 9.15.0 to 9.16.0 (#648)
dependabot[bot] Dec 1, 2024
b612613
chore(deps): bump @slack/bolt from 3.22.0 to 4.1.1 (#650)
dependabot[bot] Dec 1, 2024
3ed0917
chore(deps): bump express from 4.21.1 to 4.21.2 (#653)
dependabot[bot] Dec 16, 2024
7e22bd2
chore(deps): bump node from 22.11-alpine3.19 to 22.12-alpine3.19 (#654)
dependabot[bot] Dec 16, 2024
df35426
rebase with main
RobertKelly Jan 18, 2025
7e10d6c
chore(deps): bump liatrio/terraform-change-pr-commenter (#655)
dependabot[bot] Dec 16, 2024
56c55bd
chore(deps-dev): bump @commitlint/cli from 19.5.0 to 19.6.0 (#652)
dependabot[bot] Dec 16, 2024
de259a9
chore(deps-dev): bump eslint from 9.16.0 to 9.17.0 (#658)
dependabot[bot] Dec 23, 2024
86665ff
chore(deps-dev): bump @commitlint/cli from 19.6.0 to 19.6.1 (#657)
dependabot[bot] Dec 23, 2024
3729b3e
chore(deps-dev): bump mocha from 10.8.2 to 11.0.1 (#656)
dependabot[bot] Dec 23, 2024
1c23f0b
feat: add renovate (#659)
Pactionly Dec 23, 2024
295100d
chore: update renovate automerge (#663)
Pactionly Dec 23, 2024
abb30dc
fix(deps): update dependency @slack/bolt to v4.2.0 (#662)
renovate[bot] Dec 23, 2024
2b89d17
chore(deps): pin dependencies (#661)
renovate[bot] Dec 23, 2024
45cbce6
chore(deps): update dependency chai to v4.5.0 (#664)
renovate[bot] Dec 23, 2024
8cc49ab
chore: configure renovate autoapprove (#665)
Pactionly Dec 23, 2024
aba085c
chore(deps): update dependency globals to v15.14.0 (#666)
renovate[bot] Dec 23, 2024
661d409
chore: remove unmaintained devcontainer (#667)
Pactionly Dec 23, 2024
9d3f544
ci: remove obsolete changelog release automation (#668)
Pactionly Dec 23, 2024
0ba5e2a
fix: update release messages (#669)
Pactionly Dec 23, 2024
bf7111e
chore: remove dependabot (#670)
Pactionly Dec 23, 2024
51590e2
chore(deps): update mongo docker tag to v3.7 (#671)
renovate[bot] Dec 23, 2024
217a5c3
chore(deps): update dependency chai to v5 (#672)
renovate[bot] Dec 23, 2024
a5acbaa
chore(deps): update dependency chai-as-promised to v8 (#673)
renovate[bot] Dec 23, 2024
9fcfa13
chore(deps): update dependency semantic-release to v24.2.1 (#675)
renovate[bot] Jan 6, 2025
f3eeafd
chore(deps): update github/codeql-action action to v3.28.1 (#676)
renovate[bot] Jan 11, 2025
cec00e4
chore(deps): update eslint monorepo to v9.18.0 (#677)
renovate[bot] Jan 11, 2025
0a8caa9
Added Rich Montbriand (CRO) to the Exempt_Users list (#678)
mayormcclish Jan 13, 2025
f0f1e86
feat: add liatrio neon sign (#679)
Pactionly Jan 13, 2025
65c9400
Adding tests for the recognize feature
RobertKelly Jan 18, 2025
9934c61
Updating dependencies from main
RobertKelly Jan 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ config.goldenRecognizeEmoji =
config.goldenRecognizeChannel =
process.env.GOLDEN_RECOGNIZE_CHANNEL || "liatrio";
config.reactionEmoji = process.env.REACTION_EMOJI || ":nail_care:";
config.shareChannel = process.env.SHARE_CHANNEL || "C04RRPC9S1E";
config.shareConfirmReaction =
process.env.SHARE_CONFIRM_REACTION || "white_check_mark";
config.maximum = process.env.GRATIBOT_LIMIT || 5;
config.minimumMessageLength = 20;
config.botName = process.env.BOT_NAME || "gratibot";
Expand Down
9 changes: 9 additions & 0 deletions database/shareReactionCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const db = require('./db')
const shareReactionCollection = db.get('share_reactions')

// Create indexes for efficient querying
shareReactionCollection.createIndex('messageTs')
shareReactionCollection.createIndex('userId')
shareReactionCollection.createIndex({ 'messageTs': 1, 'userId': 1 }, { unique: true })

module.exports = shareReactionCollection
255 changes: 213 additions & 42 deletions features/recognize.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,39 @@ const config = require("../config");
const recognition = require("../service/recognition");
const winston = require("../winston");
const { SlackError, GratitudeError } = require("../service/errors");
const { reactionMatches } = require("../middleware");
const { userInfo } = require("../service/apiwrappers");
const shareReactionCollection = require("../database/shareReactionCollection");
const {
handleSlackError,
handleGratitudeError,
handleGenericError,
sendNotificationToReceivers,
} = require("../service/messageutils");

const { recognizeEmoji, reactionEmoji } = config;
const { recognizeEmoji, shareChannel, shareConfirmReaction, reactionEmoji } =
config;

module.exports = function (app) {
app.message(recognizeEmoji, respondToRecognitionMessage);
app.event(
"reaction_added",
reactionMatches(reactionEmoji),
respondToRecognitionReaction,
);
app.event("reaction_added", async ({ event, client }) => {
winston.debug("Received reaction event", {
event: JSON.stringify(event),
channel: event.item.channel,
});

// Handle share confirmation reactions in the share-me-please channel
if (
event.item.channel === shareChannel &&
event.reaction === shareConfirmReaction
) {
await handleShareConfirmation({ event, client });
return;
}
// Handle regular recognition reactions
if (event.reaction === reactionEmoji.slice(1, -1)) {
await respondToRecognitionReaction({ event, client });
}
});
};

async function respondToRecognitionMessage({ message, client }) {
Expand Down Expand Up @@ -82,26 +97,190 @@ async function respondToRecognitionMessage({ message, client }) {
}),
client.reactions.add({
channel: message.channel,
name: config.reactionEmoji.slice(1, -1),
name: reactionEmoji.slice(1, -1),
timestamp: message.ts,
}),
]);
}

/**
* Retrieves message details from Slack
* @param {Object} client - Slack client instance
* @param {Object} message - Message details containing channel and timestamp
* @returns {Promise<Object>} The message details from Slack
* @throws {SlackError} If the API call fails
*/
async function messageReactedTo(client, message) {
try {
const response = await client.conversations.replies({
channel: message.channel,
ts: message.ts,
limit: 1,
});

if (!response.ok) {
throw new SlackError(
"conversations.replies",
response.error,
`Failed to retrieve message information: ${response.error}`,
);
}

return response.messages[0];
} catch (error) {
winston.error("Error retrieving message details", {
error: error.message,
channel: message.channel,
ts: message.ts,
});
throw error;
}
}

/**
* Creates a gratitude object for share confirmation
* @param {Object} params - Parameters for creating gratitude
* @param {Object} params.user - User information from Slack
* @param {string} params.channel - Channel ID
* @param {string} params.messageTs - Original message timestamp
* @param {string} params.timezone - User's timezone
* @returns {Object} Gratitude object
*/
function createShareGratitude({ user, channel, messageTs, timezone }) {
return {
giver: {
id: "GRATIBOT",
real_name: "Gratibot",
username: "gratibot",
tz: timezone,
},
receivers: [
{
id: user.id,
real_name: user.real_name,
username: user.name,
tz: timezone,
},
],
count: 1,
message: `Thank you for sharing this with your network! Here's a ${recognizeEmoji} for helping spread the word!`,
trimmedMessage: "Reward for sharing content",
channel,
tags: ["share-confirmation"],
type: recognizeEmoji,
giver_in_receivers: false,
metadata: {
originalMessageTs: messageTs,
},
};
}

/**
* Handles share confirmation reactions
* @param {Object} params - Event parameters from Slack
* @param {Object} params.event - Reaction event details
* @param {Object} params.client - Slack client instance
*/
async function handleShareConfirmation({ event, client }) {
try {
// Get user info and timezone
const userInfo = await client.users.info({ user: event.user });
if (!userInfo.ok) {
throw new SlackError(
"users.info",
userInfo.error,
"Failed to retrieve user information",
);
}

// Check for existing reaction
const existingReaction = await shareReactionCollection.findOne({
messageTs: event.item.ts,
userId: event.user,
});

if (existingReaction) {
return client.chat.postEphemeral({
channel: event.item.channel,
user: event.user,
text: "You've already confirmed sharing this post. Thank you for your engagement! 🙌",
});
}

// Verify the original message exists
await messageReactedTo(client, {
channel: event.item.channel,
ts: event.item.ts,
});

// Record the share reaction
await shareReactionCollection.insert({
messageTs: event.item.ts,
userId: event.user,
channel: event.item.channel,
timestamp: new Date(),
});

// Create and validate gratitude
const gratitude = createShareGratitude({
user: userInfo.user,
channel: event.item.channel,
messageTs: event.item.ts,
timezone: userInfo.user.tz,
});

await recognition.validateAndSendGratitude(gratitude);

// Notify the user
await client.chat.postEphemeral({
channel: event.item.channel,
user: event.user,
text: `Thank you for sharing! You've received a ${recognizeEmoji} as a reward for your contribution! 🎉`,
});

winston.debug("Share confirmation processed", {
func: "handleShareConfirmation",
user: userInfo.user.real_name,
messageTs: event.item.ts,
channel: event.item.channel,
});
} catch (e) {
winston.error("Share confirmation failed", {
error: e.message,
stack: e.stack,
event,
});

const errorMessage = {
channel: event.item.channel,
user: event.user,
};

if (e instanceof SlackError) {
return handleSlackError(client, errorMessage, e);
} else if (e instanceof GratitudeError) {
return handleGratitudeError(client, errorMessage, e);
}
return handleGenericError(client, errorMessage, e);
}
}

async function respondToRecognitionReaction({ event, client }) {
winston.info(`Saw a reaction containing ${reactionEmoji}`, {
func: "features.recognize.respondToRecognitionReaction",
callingUser: event.user,
reactionEmoji: event.reaction,
});

event.channel = event.item.channel;

let originalMessage;
let allUsers = [];
let gratitude;
let originalMessage;

try {
originalMessage = await messageReactedTo(client, event);
originalMessage = await messageReactedTo(client, {
channel: event.item.channel,
ts: event.item.ts,
});

if (!originalMessage.text.includes(recognizeEmoji)) {
return;
Expand All @@ -119,7 +298,7 @@ async function respondToRecognitionReaction({ event, client }) {
count: 1,
message: originalMessage.text,
trimmedMessage: recognition.trimmedGratitudeMessage(originalMessage.text),
channel: event.channel,
channel: event.item.channel,
tags: recognition.gratitudeTagsIn(originalMessage.text),
type: recognizeEmoji,
giver_in_receivers: false,
Expand All @@ -132,47 +311,39 @@ async function respondToRecognitionReaction({ event, client }) {

await recognition.validateAndSendGratitude(gratitude);

winston.debug(
`validated and stored reaction recognitions from ${gratitude.giver}`,
{
func: "features.recognize.respondToRecognitionReaction",
callingUser: event.user,
slackMessage: event.reactions,
},
);
winston.debug("Validated and stored reaction recognitions", {
func: "features.recognize.respondToRecognitionReaction",
callingUser: event.user,
slackMessage: event.reactions,
});
} catch (e) {
if (e instanceof SlackError) {
return handleSlackError(client, event, e);
return handleSlackError(
client,
{ channel: event.item.channel, user: event.user },
e,
);
} else if (e instanceof GratitudeError) {
return handleGratitudeError(client, event, e);
} else {
return handleGenericError(client, event, e);
return handleGratitudeError(
client,
{ channel: event.item.channel, user: event.user },
e,
);
}
return handleGenericError(
client,
{ channel: event.item.channel, user: event.user },
e,
);
}

return Promise.all([
sendNotificationToReceivers(client, gratitude),
client.chat.postEphemeral({
channel: event.channel,
channel: event.item.channel,
user: event.user,
text: `${recognizeEmoji} has been sent.`,
...(await recognition.giverSlackNotification(gratitude)),
}),
]);
}

async function messageReactedTo(client, message) {
const response = await client.conversations.replies({
channel: message.item.channel,
ts: message.item.ts,
limit: 1,
});
if (response.ok) {
return response.messages[0];
}
throw new SlackError(
"conversations.replies",
response.error,
`Something went wrong while sending recognition. When retreiving message information from Slack, the API responded with the following error: ${response.message} \n Recognition has not been sent.`,
);
}
Loading
Loading