Spaces:
Paused
Paused
| import type { WebClient } from "@slack/web-api"; | |
| import { createSlackWebClient } from "./client.js"; | |
| export type SlackUserLookup = { | |
| id: string; | |
| name: string; | |
| displayName?: string; | |
| realName?: string; | |
| email?: string; | |
| deleted: boolean; | |
| isBot: boolean; | |
| isAppUser: boolean; | |
| }; | |
| export type SlackUserResolution = { | |
| input: string; | |
| resolved: boolean; | |
| id?: string; | |
| name?: string; | |
| email?: string; | |
| deleted?: boolean; | |
| isBot?: boolean; | |
| note?: string; | |
| }; | |
| type SlackListUsersResponse = { | |
| members?: Array<{ | |
| id?: string; | |
| name?: string; | |
| deleted?: boolean; | |
| is_bot?: boolean; | |
| is_app_user?: boolean; | |
| real_name?: string; | |
| profile?: { | |
| display_name?: string; | |
| real_name?: string; | |
| email?: string; | |
| }; | |
| }>; | |
| response_metadata?: { next_cursor?: string }; | |
| }; | |
| function parseSlackUserInput(raw: string): { id?: string; name?: string; email?: string } { | |
| const trimmed = raw.trim(); | |
| if (!trimmed) { | |
| return {}; | |
| } | |
| const mention = trimmed.match(/^<@([A-Z0-9]+)>$/i); | |
| if (mention) { | |
| return { id: mention[1]?.toUpperCase() }; | |
| } | |
| const prefixed = trimmed.replace(/^(slack:|user:)/i, ""); | |
| if (/^[A-Z][A-Z0-9]+$/i.test(prefixed)) { | |
| return { id: prefixed.toUpperCase() }; | |
| } | |
| if (trimmed.includes("@") && !trimmed.startsWith("@")) { | |
| return { email: trimmed.toLowerCase() }; | |
| } | |
| const name = trimmed.replace(/^@/, "").trim(); | |
| return name ? { name } : {}; | |
| } | |
| async function listSlackUsers(client: WebClient): Promise<SlackUserLookup[]> { | |
| const users: SlackUserLookup[] = []; | |
| let cursor: string | undefined; | |
| do { | |
| const res = (await client.users.list({ | |
| limit: 200, | |
| cursor, | |
| })) as SlackListUsersResponse; | |
| for (const member of res.members ?? []) { | |
| const id = member.id?.trim(); | |
| const name = member.name?.trim(); | |
| if (!id || !name) { | |
| continue; | |
| } | |
| const profile = member.profile ?? {}; | |
| users.push({ | |
| id, | |
| name, | |
| displayName: profile.display_name?.trim() || undefined, | |
| realName: profile.real_name?.trim() || member.real_name?.trim() || undefined, | |
| email: profile.email?.trim()?.toLowerCase() || undefined, | |
| deleted: Boolean(member.deleted), | |
| isBot: Boolean(member.is_bot), | |
| isAppUser: Boolean(member.is_app_user), | |
| }); | |
| } | |
| const next = res.response_metadata?.next_cursor?.trim(); | |
| cursor = next ? next : undefined; | |
| } while (cursor); | |
| return users; | |
| } | |
| function scoreSlackUser(user: SlackUserLookup, match: { name?: string; email?: string }): number { | |
| let score = 0; | |
| if (!user.deleted) { | |
| score += 3; | |
| } | |
| if (!user.isBot && !user.isAppUser) { | |
| score += 2; | |
| } | |
| if (match.email && user.email === match.email) { | |
| score += 5; | |
| } | |
| if (match.name) { | |
| const target = match.name.toLowerCase(); | |
| const candidates = [user.name, user.displayName, user.realName] | |
| .map((value) => value?.toLowerCase()) | |
| .filter(Boolean) as string[]; | |
| if (candidates.some((value) => value === target)) { | |
| score += 2; | |
| } | |
| } | |
| return score; | |
| } | |
| export async function resolveSlackUserAllowlist(params: { | |
| token: string; | |
| entries: string[]; | |
| client?: WebClient; | |
| }): Promise<SlackUserResolution[]> { | |
| const client = params.client ?? createSlackWebClient(params.token); | |
| const users = await listSlackUsers(client); | |
| const results: SlackUserResolution[] = []; | |
| for (const input of params.entries) { | |
| const parsed = parseSlackUserInput(input); | |
| if (parsed.id) { | |
| const match = users.find((user) => user.id === parsed.id); | |
| results.push({ | |
| input, | |
| resolved: true, | |
| id: parsed.id, | |
| name: match?.displayName ?? match?.realName ?? match?.name, | |
| email: match?.email, | |
| deleted: match?.deleted, | |
| isBot: match?.isBot, | |
| }); | |
| continue; | |
| } | |
| if (parsed.email) { | |
| const matches = users.filter((user) => user.email === parsed.email); | |
| if (matches.length > 0) { | |
| const scored = matches | |
| .map((user) => ({ user, score: scoreSlackUser(user, parsed) })) | |
| .toSorted((a, b) => b.score - a.score); | |
| const best = scored[0]?.user ?? matches[0]; | |
| results.push({ | |
| input, | |
| resolved: true, | |
| id: best.id, | |
| name: best.displayName ?? best.realName ?? best.name, | |
| email: best.email, | |
| deleted: best.deleted, | |
| isBot: best.isBot, | |
| note: matches.length > 1 ? "multiple matches; chose best" : undefined, | |
| }); | |
| continue; | |
| } | |
| } | |
| if (parsed.name) { | |
| const target = parsed.name.toLowerCase(); | |
| const matches = users.filter((user) => { | |
| const candidates = [user.name, user.displayName, user.realName] | |
| .map((value) => value?.toLowerCase()) | |
| .filter(Boolean) as string[]; | |
| return candidates.includes(target); | |
| }); | |
| if (matches.length > 0) { | |
| const scored = matches | |
| .map((user) => ({ user, score: scoreSlackUser(user, parsed) })) | |
| .toSorted((a, b) => b.score - a.score); | |
| const best = scored[0]?.user ?? matches[0]; | |
| results.push({ | |
| input, | |
| resolved: true, | |
| id: best.id, | |
| name: best.displayName ?? best.realName ?? best.name, | |
| email: best.email, | |
| deleted: best.deleted, | |
| isBot: best.isBot, | |
| note: matches.length > 1 ? "multiple matches; chose best" : undefined, | |
| }); | |
| continue; | |
| } | |
| } | |
| results.push({ input, resolved: false }); | |
| } | |
| return results; | |
| } | |