import { WebClient } from "@slack/web-api"; export class SlackService { private client: WebClient; private vettingChannelId: string; constructor(botToken: string, vettingChannelId: string) { this.client = new WebClient(botToken); this.vettingChannelId = vettingChannelId; } /** * Invite user to workspace and channel (using proper admin and conversation scopes) */ async inviteUserToSlack( email: string, realName: string, ): Promise<{ success: boolean; userId?: string; status?: string; error?: string; }> { try { // First, check if user already exists in workspace const existingUser = await this.findUserByEmail(email); if (existingUser) { // User exists, invite them to the vetting channel try { await this.client.conversations.invite({ channel: this.vettingChannelId, users: existingUser, }); console.log( `Successfully invited existing user ${email} to vetting channel`, ); return { success: true, userId: existingUser, status: "existing_user_added_to_channel", }; } catch (error: any) { if (error.data?.error === "already_in_channel") { return { success: true, userId: existingUser, status: "user_already_in_channel", }; } throw error; } } // User doesn't exist, try to invite to workspace using admin API try { const inviteResponse = await this.client.admin.users.invite({ email: email, real_name: realName, channel_ids: [this.vettingChannelId], is_restricted: true, // Single-channel guest is_ultra_restricted: false, }); if (inviteResponse.ok && inviteResponse.user) { console.log( `Successfully invited ${email} to workspace as single-channel guest`, ); return { success: true, userId: inviteResponse.user.id, status: "new_user_invited_to_workspace", }; } else { throw new Error(`Admin invite failed: ${inviteResponse.error}`); } } catch (adminError: any) { console.log( `Admin API not available or failed: ${ adminError.data?.error || adminError.message }`, ); // Fall back to manual process return { success: true, status: "manual_invitation_required", error: `Admin API unavailable: ${ adminError.data?.error || adminError.message }`, }; } } catch (error: any) { console.error(`Failed to process invitation for ${email}:`, error); return { success: false, error: error.data?.error || error.message || "Unknown error occurred", }; } } /** * Find user in workspace by email */ private async findUserByEmail(email: string): Promise { try { const response = await this.client.users.lookupByEmail({ email }); return response.user?.id || null; } catch { return null; } } /** * Find user ID by username (display name or real name) */ async findUserIdByUsername(username: string): Promise { try { const cleanUsername = username.replace("@", "").toLowerCase(); // List all users and search for matching username const response = await this.client.users.list(); if (!response.members) { return null; } // Search for user by name or display_name const user = response.members.find((member: any) => { const name = member.name?.toLowerCase() || ""; const realName = member.real_name?.toLowerCase() || ""; const displayName = member.profile?.display_name?.toLowerCase() || ""; return ( name === cleanUsername || displayName === cleanUsername || realName.includes(cleanUsername) ); }); return user?.id || null; } catch (error) { console.error("Error looking up Slack user by username:", error); return null; } } /** * Open/get a DM channel with a user and return the channel ID * This creates or opens a DM conversation and returns the channel ID (starts with D) */ async openDMChannel(userId: string): Promise { try { const response = await this.client.conversations.open({ users: userId, }); if (response.ok && response.channel?.id) { console.log( `Opened DM channel for user ${userId}: ${response.channel.id}`, ); return response.channel.id; } return null; } catch (error: any) { console.error( "Error opening DM channel:", error.data?.error || error.message, ); return null; } } /** * Send a notification to the vetting channel about a new member */ async notifyNewMember( memberName: string, memberEmail: string, circle: string, contributionTier: string, invitationStatus: string = "manual_invitation_required", ): Promise { try { let statusMessage = ""; let actionMessage = ""; switch (invitationStatus) { case "existing_user_added_to_channel": statusMessage = "✅ Existing user automatically added to this channel."; actionMessage = "Ready for vetting!"; break; case "user_already_in_channel": statusMessage = "✅ User is already in this channel."; actionMessage = "Ready for vetting!"; break; case "new_user_invited_to_workspace": statusMessage = "🎉 User successfully invited to workspace as single-channel guest."; actionMessage = "Ready for vetting!"; break; case "manual_invitation_required": statusMessage = "📧 User needs to be manually invited to join Slack."; actionMessage = `Please vet this new member before inviting them to other channels.`; break; default: statusMessage = "⚠️ Invitation status unknown."; actionMessage = "Manual review required."; } await this.client.chat.postMessage({ channel: this.vettingChannelId, text: `New Ghost Guild member: ${memberName}`, blocks: [ { type: "header", text: { type: "plain_text", text: "New Ghost Guild Member Registration", }, }, { type: "section", fields: [ { type: "mrkdwn", text: `*Name:*\n${memberName}`, }, { type: "mrkdwn", text: `*Email:*\n${memberEmail}`, }, { type: "mrkdwn", text: `*Circle:*\n${circle}`, }, { type: "mrkdwn", text: `*Contribution:*\n$${contributionTier}/month`, }, ], }, { type: "section", text: { type: "mrkdwn", text: `*Status:* ${statusMessage}\n*Action:* ${actionMessage}`, }, }, { type: "divider", }, ], }); } catch (error) { console.error("Failed to send Slack notification:", error); // Don't throw - this is non-critical } } /** * Verify the Slack channel exists and bot has access */ async verifyChannelAccess(): Promise { try { const response = await this.client.conversations.info({ channel: this.vettingChannelId, }); return response.ok && !!response.channel; } catch { return false; } } } /** * Get configured Slack service instance */ export function getSlackService(): SlackService | null { const config = useRuntimeConfig(); if (!config.slackBotToken || !config.slackVettingChannelId) { console.warn( "Slack integration not configured - missing bot token or channel ID", ); return null; } return new SlackService(config.slackBotToken, config.slackVettingChannelId); }