diff --git a/packages/core/src/infrastructure/bull/bull.types.ts b/packages/core/src/infrastructure/bull/bull.types.ts index 71e46f560..fe48f0f72 100644 --- a/packages/core/src/infrastructure/bull/bull.types.ts +++ b/packages/core/src/infrastructure/bull/bull.types.ts @@ -464,7 +464,7 @@ export const SlackBullJob = z.discriminatedUnion('name', [ name: z.literal('slack.joined'), data: z.object({ email: Student.shape.email, - slackId: Student.shape.slackId, + slackId: z.string().trim().min(1), }), }), z.object({ diff --git a/packages/core/src/modules/slack/events/slack-workspace-joined.ts b/packages/core/src/modules/slack/events/slack-workspace-joined.ts index 2f5889a55..3b1813c0c 100644 --- a/packages/core/src/modules/slack/events/slack-workspace-joined.ts +++ b/packages/core/src/modules/slack/events/slack-workspace-joined.ts @@ -2,6 +2,7 @@ import { db } from '@oyster/db'; import { type GetBullJobData } from '@/infrastructure/bull/bull.types'; import { getMemberByEmail } from '@/modules/member/queries/get-member-by-email'; +import { addDirectoryLinkToSlackProfile } from '@/modules/slack/slack-profile'; import { NotFoundError } from '@/shared/errors'; export async function onSlackWorkspaceJoined({ @@ -27,4 +28,9 @@ export async function onSlackWorkspaceJoined({ }) .where('id', '=', member.id) .execute(); + + await addDirectoryLinkToSlackProfile({ + id: member.id, + slackId, + }); } diff --git a/packages/core/src/modules/slack/slack-profile.ts b/packages/core/src/modules/slack/slack-profile.ts new file mode 100644 index 000000000..6a8a0fb94 --- /dev/null +++ b/packages/core/src/modules/slack/slack-profile.ts @@ -0,0 +1,70 @@ +import { db, type DB } from '@oyster/db'; + +import { reportException } from '@/modules/sentry/use-cases/report-exception'; +import { slack } from '@/modules/slack/instances'; +import { ENV } from '@/shared/env'; +import { RateLimiter } from '@/shared/utils/rate-limiter'; + +// Environment Variables + +const SLACK_MEMBER_DIRECTORY_FIELD_ID = process.env + .SLACK_MEMBER_DIRECTORY_FIELD_ID as string; + +// Core + +/** + * @see https://api.slack.com/apis/rate-limits#tier_t3 + */ +const updateProfileRateLimiter = new RateLimiter('slack:profile:update', { + rateLimit: 50, + rateLimitWindow: 60, +}); + +export async function addDirectoryLinkToSlackProfiles() { + const members = await db + .selectFrom('students') + .select(['email', 'id', 'slackId']) + .where('slackId', 'is not', null) + .orderBy('createdAt', 'desc') + .execute(); + + console.log(`Found ${members.length} members to update.`); + + for (const member of members) { + console.log('Updating Slack profile...', member.email); + + try { + await addDirectoryLinkToSlackProfile(member); + console.count('Updated Slack profile!'); + } catch (e) { + console.count('Failed to update Slack profile.'); + reportException(e); + } + } +} + +/** + * Adds a member's Member Directory profile link to their Slack profile. Throws + * an error if the profile update fails. + * + * @param member - The member whose Slack profile we'll update. + */ +export async function addDirectoryLinkToSlackProfile( + member: Pick +) { + await updateProfileRateLimiter.process(); + + const profile = { + fields: { + [SLACK_MEMBER_DIRECTORY_FIELD_ID]: { + value: new URL(`/directory/${member.id}`, ENV.STUDENT_PROFILE_URL), + }, + }, + }; + + await slack.users.profile.set({ + profile: JSON.stringify(profile), + token: ENV.SLACK_ADMIN_TOKEN, + user: member.slackId as string, + }); +}