Skip to content

Commit d0498ca

Browse files
Adding opting out
1 parent 1f516a6 commit d0498ca

3 files changed

Lines changed: 284 additions & 7 deletions

File tree

src/coffeeChats/controllers.ts

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CoffeeChatConfig,
66
CoffeeChatConfigModel,
77
CoffeeChatPairingModel,
8+
CoffeeChatUserPreferenceModel,
89
} from "./models";
910

1011
const COFFEE_CHAT_ACTIVITIES = [
@@ -81,7 +82,7 @@ const getRandomActivity = (): string => {
8182
};
8283

8384
/**
84-
* Gets all members from a Slack channel (excluding bots)
85+
* Gets all members from a Slack channel (excluding bots and opted-out users)
8586
*/
8687
const getChannelMembers = async (channelId: string): Promise<string[]> => {
8788
const result = await slackbot.client.conversations.members({
@@ -103,7 +104,18 @@ const getChannelMembers = async (channelId: string): Promise<string[]> => {
103104
}),
104105
);
105106

106-
return memberDetails.filter((m) => !m.isBot).map((m) => m.id);
107+
const nonBotMembers = memberDetails.filter((m) => !m.isBot).map((m) => m.id);
108+
109+
// Filter out opted-out users
110+
const preferences = await CoffeeChatUserPreferenceModel.find({
111+
channelId,
112+
userId: { $in: nonBotMembers },
113+
isOptedIn: false,
114+
});
115+
116+
const optedOutUserIds = new Set(preferences.map((p) => p.userId));
117+
118+
return nonBotMembers.filter((userId) => !optedOutUserIds.has(userId));
107119
};
108120

109121
/**
@@ -180,7 +192,10 @@ const createPairings = (
180192
/**
181193
* Creates a group DM and notifies paired users
182194
*/
183-
const notifyPairing = async (userIds: string[]): Promise<void> => {
195+
const notifyPairing = async (
196+
userIds: string[],
197+
channelId: string,
198+
): Promise<void> => {
184199
const userMentions = userIds.map((id) => `<@${id}>`).join(", ");
185200
const activity = getRandomActivity();
186201

@@ -195,10 +210,41 @@ const notifyPairing = async (userIds: string[]): Promise<void> => {
195210
return;
196211
}
197212

198-
// Send a message to the group DM
213+
// Send a message to the group DM with interactive buttons
199214
const messageResult = await slackbot.client.chat.postMessage({
200215
channel: conversation.channel.id!,
201-
text: `Hey ${userMentions}! You've been paired for a coffee chat. ☕\n\nSuggested activity: *${activity}*\n\nTake some time in the next two weeks to connect and get to know each other better!`,
216+
text: `Hey ${userMentions}! You've been paired for a coffee chat. ☕`,
217+
blocks: [
218+
{
219+
type: "section",
220+
text: {
221+
type: "mrkdwn",
222+
text: `Hey ${userMentions}! You've been paired for a coffee chat. ☕`,
223+
},
224+
},
225+
{
226+
type: "section",
227+
text: {
228+
type: "mrkdwn",
229+
text: `*Suggested activity:* ${activity}\n\nTake some time in the next two weeks to connect and get to know each other better!`,
230+
},
231+
},
232+
{
233+
type: "actions",
234+
elements: [
235+
{
236+
type: "button",
237+
text: {
238+
type: "plain_text",
239+
text: "⏸️ Pause Future Pairings",
240+
},
241+
style: "danger",
242+
action_id: "coffee_chat_opt_out",
243+
value: channelId,
244+
},
245+
],
246+
},
247+
],
202248
});
203249

204250
if (!messageResult.ok) {
@@ -252,7 +298,7 @@ export const processCoffeeChatChannel = async (
252298
await pairingDoc.save();
253299

254300
// Notify users
255-
await notifyPairing(pairing);
301+
await notifyPairing(pairing, config.channelId);
256302
}
257303

258304
// Update last pairing date
@@ -309,3 +355,55 @@ export const registerCoffeeChatChannel = async (
309355
await config.save();
310356
logWithTime(`✅ Registered ${channelName} for coffee chat pairings`);
311357
};
358+
359+
/**
360+
* Opts a user out of coffee chats for a specific channel
361+
*/
362+
export const optOutOfCoffeeChats = async (
363+
userId: string,
364+
channelId: string,
365+
): Promise<void> => {
366+
const now = moment().tz("America/New_York").toDate();
367+
368+
await CoffeeChatUserPreferenceModel.findOneAndUpdate(
369+
{ userId, channelId },
370+
{ isOptedIn: false, updatedAt: now },
371+
{ upsert: true, new: true },
372+
);
373+
374+
logWithTime(`User ${userId} opted out of coffee chats in channel ${channelId}`);
375+
};
376+
377+
/**
378+
* Opts a user back into coffee chats for a specific channel
379+
*/
380+
export const optInToCoffeeChats = async (
381+
userId: string,
382+
channelId: string,
383+
): Promise<void> => {
384+
const now = moment().tz("America/New_York").toDate();
385+
386+
await CoffeeChatUserPreferenceModel.findOneAndUpdate(
387+
{ userId, channelId },
388+
{ isOptedIn: true, updatedAt: now },
389+
{ upsert: true, new: true },
390+
);
391+
392+
logWithTime(`User ${userId} opted into coffee chats in channel ${channelId}`);
393+
};
394+
395+
/**
396+
* Gets the opt-in status for a user in a channel
397+
*/
398+
export const getCoffeeChatsOptInStatus = async (
399+
userId: string,
400+
channelId: string,
401+
): Promise<boolean> => {
402+
const preference = await CoffeeChatUserPreferenceModel.findOne({
403+
userId,
404+
channelId,
405+
});
406+
407+
// Default to opted in if no preference exists
408+
return preference?.isOptedIn ?? true;
409+
};

src/coffeeChats/models.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getModelForClass, prop } from "@typegoose/typegoose";
1+
import { getModelForClass, prop, index } from "@typegoose/typegoose";
22

33
class CoffeeChatPairing {
44
@prop({ required: true })
@@ -28,12 +28,30 @@ class CoffeeChatConfig {
2828
lastPairingDate?: Date;
2929
}
3030

31+
@index({ userId: 1, channelId: 1 }, { unique: true })
32+
class CoffeeChatUserPreference {
33+
@prop({ required: true })
34+
userId!: string;
35+
36+
@prop({ required: true })
37+
channelId!: string;
38+
39+
@prop({ default: true })
40+
isOptedIn!: boolean;
41+
42+
@prop()
43+
updatedAt?: Date;
44+
}
45+
3146
const CoffeeChatPairingModel = getModelForClass(CoffeeChatPairing);
3247
const CoffeeChatConfigModel = getModelForClass(CoffeeChatConfig);
48+
const CoffeeChatUserPreferenceModel = getModelForClass(CoffeeChatUserPreference);
3349

3450
export {
3551
CoffeeChatPairing,
3652
CoffeeChatPairingModel,
3753
CoffeeChatConfig,
3854
CoffeeChatConfigModel,
55+
CoffeeChatUserPreference,
56+
CoffeeChatUserPreferenceModel,
3957
};

src/slackbot.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { App } from "@slack/bolt";
22
import {
33
processCoffeeChatChannel,
44
registerCoffeeChatChannel,
5+
optOutOfCoffeeChats,
6+
optInToCoffeeChats,
7+
getCoffeeChatsOptInStatus,
58
} from "./coffeeChats/controllers";
69
import { CoffeeChatConfigModel } from "./coffeeChats/models";
710

@@ -16,6 +19,83 @@ slackbot.message("hello", async ({ message, say }: any) => {
1619
await say(`Hey there <@${message.user}>!`);
1720
});
1821

22+
// Action handler for opting out of coffee chats
23+
slackbot.action("coffee_chat_opt_out", async ({ ack, body, respond }: any) => {
24+
await ack();
25+
26+
try {
27+
const userId = body.user.id;
28+
const channelId = body.actions[0].value;
29+
30+
await optOutOfCoffeeChats(userId, channelId);
31+
32+
await respond({
33+
text: `You've been opted out of future coffee chat pairings.`,
34+
blocks: [
35+
{
36+
type: "section",
37+
text: {
38+
type: "mrkdwn",
39+
text: `✅ You've been opted out of future coffee chat pairings. You won't be included in upcoming rounds.`,
40+
},
41+
},
42+
{
43+
type: "actions",
44+
elements: [
45+
{
46+
type: "button",
47+
text: {
48+
type: "plain_text",
49+
text: "▶️ Resume Pairings",
50+
},
51+
style: "primary",
52+
action_id: "coffee_chat_opt_in",
53+
value: channelId,
54+
},
55+
],
56+
},
57+
],
58+
replace_original: false,
59+
});
60+
} catch (error) {
61+
await respond({
62+
text: `❌ Error opting out of coffee chats: ${error}`,
63+
replace_original: false,
64+
});
65+
}
66+
});
67+
68+
// Action handler for opting back into coffee chats
69+
slackbot.action("coffee_chat_opt_in", async ({ ack, body, respond }: any) => {
70+
await ack();
71+
72+
try {
73+
const userId = body.user.id;
74+
const channelId = body.actions[0].value;
75+
76+
await optInToCoffeeChats(userId, channelId);
77+
78+
await respond({
79+
text: `You've been opted back into coffee chat pairings!`,
80+
blocks: [
81+
{
82+
type: "section",
83+
text: {
84+
type: "mrkdwn",
85+
text: `✅ Welcome back! You've been opted back into coffee chat pairings. You'll be included in future rounds.`,
86+
},
87+
},
88+
],
89+
replace_original: false,
90+
});
91+
} catch (error) {
92+
await respond({
93+
text: `❌ Error opting into coffee chats: ${error}`,
94+
replace_original: false,
95+
});
96+
}
97+
});
98+
1999
// Command to register a channel for coffee chats
20100
slackbot.command("/register-coffee-chats", async ({ command, ack, say }) => {
21101
await ack();
@@ -83,4 +163,85 @@ slackbot.command("/disable-coffee-chats", async ({ command, ack, say }) => {
83163
}
84164
});
85165

166+
// Command to check coffee chat opt-in status
167+
slackbot.command("/coffee-chat-status", async ({ command, ack, say }) => {
168+
await ack();
169+
170+
try {
171+
const userId = command.user_id;
172+
const channelId = command.channel_id;
173+
174+
// Check if channel is registered for coffee chats
175+
const config = await CoffeeChatConfigModel.findOne({ channelId });
176+
if (!config) {
177+
await say({
178+
text: `❌ This channel is not registered for coffee chats.`,
179+
});
180+
return;
181+
}
182+
183+
const isOptedIn = await getCoffeeChatsOptInStatus(userId, channelId);
184+
185+
if (isOptedIn) {
186+
await say({
187+
text: `You are currently opted in to coffee chats.`,
188+
blocks: [
189+
{
190+
type: "section",
191+
text: {
192+
type: "mrkdwn",
193+
text: `☕ You are currently *opted in* to coffee chats in this channel.`,
194+
},
195+
},
196+
{
197+
type: "actions",
198+
elements: [
199+
{
200+
type: "button",
201+
text: {
202+
type: "plain_text",
203+
text: "⏸️ Pause Future Pairings",
204+
},
205+
style: "danger",
206+
action_id: "coffee_chat_opt_out",
207+
value: channelId,
208+
},
209+
],
210+
},
211+
],
212+
});
213+
} else {
214+
await say({
215+
text: `You are currently opted out of coffee chats.`,
216+
blocks: [
217+
{
218+
type: "section",
219+
text: {
220+
type: "mrkdwn",
221+
text: `You are currently *opted out* of coffee chats in this channel.`,
222+
},
223+
},
224+
{
225+
type: "actions",
226+
elements: [
227+
{
228+
type: "button",
229+
text: {
230+
type: "plain_text",
231+
text: "▶️ Resume Pairings",
232+
},
233+
style: "primary",
234+
action_id: "coffee_chat_opt_in",
235+
value: channelId,
236+
},
237+
],
238+
},
239+
],
240+
});
241+
}
242+
} catch (error) {
243+
await say(`❌ Error checking coffee chat status: ${error}`);
244+
}
245+
});
246+
86247
export default slackbot;

0 commit comments

Comments
 (0)