clienttarget-python / src /slack /slack-service.ts
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",
});
}