()
| 710 | * Used by both the admin endpoint and the daily background job. |
| 711 | */ |
| 712 | export async function autoLinkUnmappedSlackUsers(): Promise<{ |
| 713 | linked: number; |
| 714 | chapters_joined: number; |
| 715 | organizations_assigned: number; |
| 716 | pending_org_prospects_set: number; |
| 717 | errors: number; |
| 718 | }> { |
| 719 | // Ensure all current workspace members have rows before attempting to link. |
| 720 | // Users who joined before the team_join listener was active have no row otherwise. |
| 721 | const syncResult = await syncSlackUsers(); |
| 722 | if (syncResult.errors.length > 0) { |
| 723 | logger.warn({ errors: syncResult.errors }, 'Slack user sync had errors; auto-link results may be incomplete'); |
| 724 | } |
| 725 | |
| 726 | // excludeOptedOut: false — opt-out applies to nudge notifications, not account linking. |
| 727 | // Linking is a system operation; it doesn't send any messages. |
| 728 | const unmappedSlack = await slackDb.getUnmappedUsers({ |
| 729 | excludeOptedOut: false, |
| 730 | excludeRecentlyNudged: false, |
| 731 | }); |
| 732 | |
| 733 | const aaoEmailToUserId = await buildAaoEmailToUserIdMap(); |
| 734 | const mappedWorkosUserIds = await slackDb.getMappedWorkosUserIds(); |
| 735 | |
| 736 | // Set pending_organization_id for unmapped Slack users whose email domain matches an org. |
| 737 | // This is a DB-only operation and is safe regardless of whether Slack is configured. |
| 738 | // It is idempotent and covers users who joined Slack before the listener was active. |
| 739 | const backfillResult = await slackDb.backfillPendingOrganizations(); |
| 740 | |
| 741 | let linked = 0; |
| 742 | let chaptersJoined = 0; |
| 743 | let orgsAssigned = 0; |
| 744 | let errors = 0; |
| 745 | |
| 746 | for (const slackUser of unmappedSlack) { |
| 747 | if (!slackUser.slack_email) continue; |
| 748 | |
| 749 | const workosUserId = aaoEmailToUserId.get(slackUser.slack_email.toLowerCase()); |
| 750 | if (!workosUserId || mappedWorkosUserIds.has(workosUserId)) continue; |
| 751 | |
| 752 | try { |
| 753 | await slackDb.mapUser({ |
| 754 | slack_user_id: slackUser.slack_user_id, |
| 755 | workos_user_id: workosUserId, |
| 756 | mapping_source: 'email_auto', |
| 757 | }); |
| 758 | linked++; |
| 759 | mappedWorkosUserIds.add(workosUserId); |
| 760 | // Clear cached admin status so Addie recognizes newly linked admins immediately. |
| 761 | invalidateAdminStatusCache(slackUser.slack_user_id); |
| 762 | |
| 763 | const chapterResult = await syncUserToChaptersFromSlackChannels(workosUserId, slackUser.slack_user_id); |
| 764 | chaptersJoined += chapterResult.chapters_joined; |
| 765 | |
| 766 | const orgResult = await checkAndAssignOrganizationByDomain(workosUserId); |
| 767 | if (orgResult?.assigned) orgsAssigned++; |
| 768 | } catch (err) { |
| 769 | logger.error({ err, slackUserId: slackUser.slack_user_id, email: slackUser.slack_email }, 'Failed to auto-link user'); |
no test coverage detected