Spaces:
Running
Running
iDevBuddy
feat: Add Slack Events integration, Dockerfiles, and Hugging Face deployment config
5f138d4 | /** | |
| * Slack Service β 3-Layer Data Delivery | |
| * | |
| * Layer 1: Daily Digest (1 rich message per day β summary table) | |
| * Layer 2: Real-time Alerts (only HOT leads 85+ β immediate) | |
| * Layer 3: Commands (/leads, /discover, /status, /pause, /quota) | |
| * | |
| * NOT Slack blast β organized, formatted, actionable. | |
| */ | |
| import axios from "axios"; | |
| import { getEnv } from "../shared/config/env"; | |
| import { getSupabaseClient } from "../shared/supabase/client"; | |
| import { logger } from "../shared/utils/logger"; | |
| // βββ Slack API helper ββββββββββββββββββββββββββββββββββββββββ | |
| async function postMessage(channelId: string, blocks: unknown[], text: string): Promise<void> { | |
| const env = getEnv(); | |
| try { | |
| await axios.post("https://slack.com/api/chat.postMessage", { | |
| channel: channelId, | |
| text, | |
| blocks, | |
| }, { | |
| headers: { Authorization: `Bearer ${env.SLACK_BOT_TOKEN}` }, | |
| timeout: 5_000, | |
| }); | |
| } catch (err) { | |
| logger.warn({ err }, "Slack post failed β non-critical"); | |
| } | |
| } | |
| // βββ LAYER 1: Daily Digest βββββββββββββββββββββββββββββββββββ | |
| export async function sendDailyDigest(runSummary: { | |
| territory: string; | |
| industry: string; | |
| companiesSearched: number; | |
| leadsQualified: number; | |
| hotLeads: number; | |
| warmLeads: number; | |
| nurtureLeads: number; | |
| tokensUsed: number; | |
| durationMinutes: number; | |
| }): Promise<void> { | |
| const env = getEnv(); | |
| const db = getSupabaseClient(); | |
| // Fetch today's qualified leads | |
| const today = new Date(); | |
| today.setHours(0, 0, 0, 0); | |
| const { data: leads } = await db | |
| .from("lead_scores") | |
| .select(` | |
| total_score, tier, | |
| companies (name, domain, industry, employee_count, city, service_match), | |
| contacts (full_name, title, email, email_verified, linkedin_personal_url) | |
| `) | |
| .gte("created_at", today.toISOString()) | |
| .order("total_score", { ascending: false }) | |
| .limit(20); | |
| // Build lead table | |
| const leadRows = (leads ?? []).map((lead: any, i: number) => { | |
| const emoji = lead.tier === "hot" ? "π₯" : lead.tier === "warm" ? "β " : "π"; | |
| const company = lead.companies; | |
| const contact = lead.contacts; | |
| const emailIcon = contact?.email_verified ? "π§β" : contact?.email ? "π§" : "β"; | |
| const linkedinIcon = contact?.linkedin_personal_url ? "πΌβ" : "β"; | |
| return `${emoji} *${company?.name ?? "Unknown"}* β ${lead.total_score}/100 ${lead.tier.toUpperCase()}\n` + | |
| ` ${company?.industry ?? "?"} Β· ${company?.employee_count ?? "?"} emp Β· ${company?.city ?? "?"}\n` + | |
| ` ${contact?.full_name ?? "?"} (${contact?.title ?? "?"})\n` + | |
| ` ${emailIcon} ${linkedinIcon} Β· Match: ${company?.service_match ?? "β"}`; | |
| }).join("\n\n"); | |
| const blocks = [ | |
| // Header | |
| { | |
| type: "header", | |
| text: { type: "plain_text", text: `π Daily Lead Report β ${formatDate(new Date())}` }, | |
| }, | |
| // Summary stats | |
| { | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: `*Territory:* ${runSummary.territory} β ${runSummary.industry}\n` + | |
| `*Searched:* ${runSummary.companiesSearched} companies\n` + | |
| `*Qualified:* ${runSummary.leadsQualified} leads ` + | |
| `(π₯ ${runSummary.hotLeads} hot Β· β ${runSummary.warmLeads} warm Β· π ${runSummary.nurtureLeads} nurture)\n` + | |
| `*Duration:* ${runSummary.durationMinutes} min Β· *Tokens:* ${runSummary.tokensUsed.toLocaleString()}`, | |
| }, | |
| }, | |
| { type: "divider" }, | |
| // Lead list | |
| { | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: leadRows || "_No qualified leads found today_", | |
| }, | |
| }, | |
| { type: "divider" }, | |
| // Actions | |
| { | |
| type: "context", | |
| elements: [ | |
| { | |
| type: "mrkdwn", | |
| text: "Type `/leads` for full details Β· `/discover region:UK` for manual run Β· `/status` for system status", | |
| }, | |
| ], | |
| }, | |
| ]; | |
| await postMessage(env.SLACK_ALERT_CHANNEL_ID, blocks, | |
| `Daily Report: ${runSummary.leadsQualified} leads found`); | |
| } | |
| // βββ LAYER 2: Hot Lead Alert (85+ only) ββββββββββββββββββββββ | |
| export async function sendHotLeadAlert(lead: { | |
| companyName: string; | |
| domain: string; | |
| industry: string; | |
| employeeCount: number | null; | |
| city: string | null; | |
| score: number; | |
| tier: string; | |
| contactName: string; | |
| contactTitle: string; | |
| email: string | null; | |
| emailVerified: boolean; | |
| linkedinPersonal: string | null; | |
| linkedinCompany: string | null; | |
| serviceMatch: string | null; | |
| outreachAngle: string; | |
| painPoints: string[]; | |
| socialProfiles: Record<string, string | null>; | |
| }): Promise<void> { | |
| const env = getEnv(); | |
| const emoji = lead.score >= 90 ? "π₯π₯π₯" : lead.score >= 85 ? "π₯π₯" : "π₯"; | |
| // Contact channels summary | |
| const channels: string[] = []; | |
| if (lead.email && lead.emailVerified) channels.push(`π§ ${lead.email} β`); | |
| else if (lead.email) channels.push(`π§ ${lead.email} (unverified)`); | |
| if (lead.linkedinPersonal) channels.push(`πΌ <${lead.linkedinPersonal}|LinkedIn>`); | |
| if (lead.linkedinCompany) channels.push(`π’ <${lead.linkedinCompany}|Company LI>`); | |
| if (lead.socialProfiles?.instagram) channels.push(`π· <${lead.socialProfiles.instagram}|Instagram>`); | |
| if (lead.socialProfiles?.facebook) channels.push(`π₯ <${lead.socialProfiles.facebook}|Facebook>`); | |
| const blocks = [ | |
| { | |
| type: "header", | |
| text: { type: "plain_text", text: `${emoji} HOT LEAD β ${lead.companyName}` }, | |
| }, | |
| { | |
| type: "section", | |
| fields: [ | |
| { type: "mrkdwn", text: `*Score:*\n${lead.score}/100 β ${lead.tier.toUpperCase()}` }, | |
| { type: "mrkdwn", text: `*Industry:*\n${lead.industry}` }, | |
| { type: "mrkdwn", text: `*Employees:*\n${lead.employeeCount ?? "Unknown"}` }, | |
| { type: "mrkdwn", text: `*Location:*\n${lead.city ?? "Unknown"}` }, | |
| { type: "mrkdwn", text: `*Service Match:*\n${lead.serviceMatch ?? "General"}` }, | |
| { type: "mrkdwn", text: `*Domain:*\n${lead.domain}` }, | |
| ], | |
| }, | |
| { type: "divider" }, | |
| { | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: `*π€ Decision Maker:*\n${lead.contactName} β ${lead.contactTitle}\n\n` + | |
| `*π± Channels:*\n${channels.join("\n") || "None found"}\n\n` + | |
| `*π― Outreach Angle:*\n_"${lead.outreachAngle}"_\n\n` + | |
| `*π’ Pain Points:*\n${lead.painPoints.map(p => `β’ ${p}`).join("\n")}`, | |
| }, | |
| }, | |
| ]; | |
| await postMessage(env.SLACK_ALERT_CHANNEL_ID, blocks, | |
| `π₯ HOT LEAD: ${lead.companyName} β Score ${lead.score}`); | |
| } | |
| // βββ LAYER 2: Run Progress Updates ββββββββββββββββββββββββββ | |
| export async function sendRunStarted(territory: string, industry: string, quota: number): Promise<void> { | |
| const env = getEnv(); | |
| await postMessage(env.SLACK_ALERT_CHANNEL_ID, [ | |
| { | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: `π *Daily run started*\n` + | |
| `Territory: ${territory} β ${industry}\n` + | |
| `Quota: ${quota} leads\n` + | |
| `Estimated: ~90 min`, | |
| }, | |
| }, | |
| ], `Run started: ${territory} ${industry}`); | |
| } | |
| export async function sendRunProgress(qualified: number, quota: number, searched: number): Promise<void> { | |
| const env = getEnv(); | |
| const progress = Math.round((qualified / quota) * 100); | |
| const bar = "β".repeat(Math.round(progress / 10)) + "β".repeat(10 - Math.round(progress / 10)); | |
| await postMessage(env.SLACK_ALERT_CHANNEL_ID, [ | |
| { | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: `π *Progress:* ${qualified}/${quota} qualified [${bar}] ${progress}%\n` + | |
| `Searched: ${searched} companies`, | |
| }, | |
| }, | |
| ], `Progress: ${qualified}/${quota}`); | |
| } | |
| // βββ LAYER 3: Clarifying Questions ββββββββββββββββββββββββββ | |
| export async function sendClarifyingQuestions( | |
| userMessage: string, | |
| questions: { question: string; options: string[] }[] | |
| ): Promise<void> { | |
| const env = getEnv(); | |
| const blocks: unknown[] = [ | |
| { | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: `π€ *Got it: "${userMessage}"*\nMujhe kuch clarify karna hai:`, | |
| }, | |
| }, | |
| ]; | |
| for (const q of questions) { | |
| blocks.push({ | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: `*${q.question}*\n${q.options.map((o, i) => `${i + 1}. ${o}`).join("\n")}`, | |
| }, | |
| }); | |
| } | |
| blocks.push({ | |
| type: "context", | |
| elements: [{ | |
| type: "mrkdwn", | |
| text: "Just reply with numbers (e.g., `1 2 3`) or type your own answer", | |
| }], | |
| }); | |
| await postMessage(env.SLACK_ALERT_CHANNEL_ID, blocks, | |
| "Clarifying questions for manual discovery"); | |
| } | |
| // βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function formatDate(date: Date): string { | |
| return date.toLocaleDateString("en-US", { | |
| weekday: "long", | |
| year: "numeric", | |
| month: "long", | |
| day: "numeric", | |
| }); | |
| } | |