501 lines
16 KiB
TypeScript
501 lines
16 KiB
TypeScript
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
// Load .env variables
|
|
const envPath = path.join(__dirname, "../.env");
|
|
if (fs.existsSync(envPath)) {
|
|
const envContent = fs.readFileSync(envPath, "utf8");
|
|
for (const line of envContent.split("\n")) {
|
|
const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)\s*$/);
|
|
if (match) {
|
|
const key = match[1];
|
|
let value = match[2].trim();
|
|
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
|
|
else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
const N8N_HOST = process.env.N8N_HOST || "https://n8n.gaboggamer.online";
|
|
const N8N_API_KEY = process.env.N8N_API_KEY;
|
|
const EMAIL_USER = process.env.EMAIL_USER;
|
|
const EMAIL_PASSWORD = process.env.EMAIL_PASSWORD;
|
|
const EMAIL_HOST = process.env.EMAIL_HOST;
|
|
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT || "465", 10);
|
|
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
const SUPABASE_SECRET_KEY = process.env.SUPABASE_SECRET_KEY;
|
|
|
|
if (!N8N_API_KEY) {
|
|
console.error("Error: N8N_API_KEY is not defined in .env");
|
|
process.exit(1);
|
|
}
|
|
|
|
async function n8nRequest(endpoint: string, method: string = "GET", body?: any) {
|
|
const response = await fetch(`${N8N_HOST}${endpoint}`, {
|
|
method,
|
|
headers: {
|
|
"X-N8N-API-KEY": N8N_API_KEY!,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new Error(`n8n API request failed: ${response.status} ${response.statusText} - ${text}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
async function getOrCreateCredential(name: string, type: string, data: any) {
|
|
try {
|
|
const credsList = await n8nRequest("/api/v1/credentials");
|
|
const existingCred = credsList.data.find((c: any) => c.name === name && c.type === type);
|
|
if (existingCred) {
|
|
console.log(`Reusing existing credential: ${name} (ID: ${existingCred.id})...`);
|
|
return existingCred.id;
|
|
}
|
|
|
|
const newCred = await n8nRequest("/api/v1/credentials", "POST", {
|
|
name,
|
|
type,
|
|
data,
|
|
});
|
|
console.log(`Created new credential: ${name} (ID: ${newCred.id})`);
|
|
return newCred.id;
|
|
} catch (err: any) {
|
|
console.error(`Error setting up credential ${name}:`, err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log("\n==============================================");
|
|
console.log("DEPLOYING EMAIL AUTOMATION FOR N8N");
|
|
console.log("==============================================");
|
|
|
|
// 1. Setup SMTP Credentials
|
|
const smtpCredId = await getOrCreateCredential("Semillero2_SMTP", "smtp", {
|
|
host: EMAIL_HOST,
|
|
port: EMAIL_PORT,
|
|
user: EMAIL_USER,
|
|
password: EMAIL_PASSWORD,
|
|
secure: true,
|
|
});
|
|
|
|
// 2. Setup IMAP Credentials
|
|
const imapCredId = await getOrCreateCredential("Semillero2_IMAP_V2", "imap", {
|
|
host: EMAIL_HOST,
|
|
port: 993,
|
|
user: EMAIL_USER,
|
|
password: EMAIL_PASSWORD,
|
|
secure: true,
|
|
});
|
|
|
|
// 3. Setup Supabase Credentials
|
|
const supabaseCredId = await getOrCreateCredential("Semillero2_Supabase", "supabaseApi", {
|
|
host: SUPABASE_URL,
|
|
serviceRole: SUPABASE_SECRET_KEY,
|
|
allowedHttpRequestDomains: "none",
|
|
});
|
|
|
|
// 4. Fetch Candidate Evaluation Workflow ID
|
|
const workflowsList = await n8nRequest("/api/v1/workflows");
|
|
const evalWf = workflowsList.data.find(
|
|
(w: any) => w.name === "Semillero2: End-to-End Candidate Evaluation"
|
|
);
|
|
if (!evalWf) {
|
|
console.error("Error: Could not find candidate evaluation workflow ID.");
|
|
process.exit(1);
|
|
}
|
|
const evalWfId = evalWf.id;
|
|
console.log(`Candidate Evaluation Workflow ID: ${evalWfId}`);
|
|
|
|
// 5. Deploy / Update IMAP Ingestion Workflow
|
|
const imapWf = workflowsList.data.find(
|
|
(w: any) => w.name === "Semillero2: Email Ingestion Listener"
|
|
);
|
|
|
|
const imapWfDefinition = {
|
|
name: "Semillero2: Email Ingestion Listener",
|
|
settings: {},
|
|
nodes: [
|
|
{
|
|
parameters: {
|
|
postProcessAction: "markAsRead",
|
|
format: "resolved",
|
|
downloadAttachments: true,
|
|
options: {
|
|
customEmailConfig: '["UNSEEN"]',
|
|
},
|
|
},
|
|
id: "w2-imap-id",
|
|
name: "Email Trigger (IMAP)",
|
|
type: "n8n-nodes-base.emailReadImap",
|
|
typeVersion: 2,
|
|
position: [100, 300],
|
|
credentials: {
|
|
imap: {
|
|
id: imapCredId,
|
|
name: "Semillero2_IMAP_V2",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
parameters: {
|
|
operation: "pdf",
|
|
binaryPropertyName: "attachment_0",
|
|
},
|
|
id: "w2-extract-id",
|
|
name: "Extract From PDF",
|
|
type: "n8n-nodes-base.extractFromFile",
|
|
typeVersion: 1,
|
|
position: [320, 300],
|
|
},
|
|
{
|
|
parameters: {
|
|
jsCode: `const item = $input.first().json;
|
|
const text = item.text || "";
|
|
const emailBody = $('Email Trigger (IMAP)').item.json.textHtml || $('Email Trigger (IMAP)').item.json.text || "";
|
|
const fromEmail = $('Email Trigger (IMAP)').item.json.from.value[0].address;
|
|
const fromName = $('Email Trigger (IMAP)').item.json.from.value[0].name || fromEmail.split('@')[0];
|
|
return [{
|
|
json: {
|
|
name: fromName,
|
|
email: fromEmail,
|
|
text: \`Email Body:\\n\${emailBody}\\n\\nResume Text:\\n\${text}\`
|
|
}
|
|
}];`,
|
|
},
|
|
id: "w2-format-id",
|
|
name: "Format Email Data",
|
|
type: "n8n-nodes-base.code",
|
|
typeVersion: 2,
|
|
position: [540, 300],
|
|
},
|
|
{
|
|
parameters: {
|
|
workflowId: evalWfId,
|
|
options: {},
|
|
},
|
|
id: "w2-exec-id",
|
|
name: "Execute Workflow 3",
|
|
type: "n8n-nodes-base.executeWorkflow",
|
|
typeVersion: 1,
|
|
position: [760, 300],
|
|
},
|
|
],
|
|
connections: {
|
|
"Email Trigger (IMAP)": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Extract From PDF",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
"Extract From PDF": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Format Email Data",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
"Format Email Data": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Execute Workflow 3",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
let imapDeployResult;
|
|
if (imapWf) {
|
|
if (imapWf.isArchived) {
|
|
console.log(`Deleting archived IMAP workflow (ID: ${imapWf.id})...`);
|
|
await n8nRequest(`/api/v1/workflows/${imapWf.id}`, "DELETE");
|
|
console.log("Creating new IMAP workflow...");
|
|
imapDeployResult = await n8nRequest("/api/v1/workflows", "POST", imapWfDefinition);
|
|
} else {
|
|
console.log(`Updating existing IMAP workflow (ID: ${imapWf.id})...`);
|
|
imapDeployResult = await n8nRequest(`/api/v1/workflows/${imapWf.id}`, "PUT", imapWfDefinition);
|
|
}
|
|
} else {
|
|
console.log("Creating new IMAP workflow...");
|
|
imapDeployResult = await n8nRequest("/api/v1/workflows", "POST", imapWfDefinition);
|
|
}
|
|
|
|
// Activate IMAP Ingestion Workflow
|
|
console.log(`Activating IMAP Ingestion workflow (ID: ${imapDeployResult.id})...`);
|
|
await n8nRequest(`/api/v1/workflows/${imapDeployResult.id}/activate`, "POST");
|
|
|
|
// 6. Deploy / Update Stage Change Email Notifier
|
|
const notifierWf = workflowsList.data.find(
|
|
(w: any) => w.name === "Semillero2: Stage Change Email Notifier"
|
|
);
|
|
|
|
const notifierWfDefinition = {
|
|
name: "Semillero2: Stage Change Email Notifier",
|
|
settings: {},
|
|
nodes: [
|
|
{
|
|
parameters: {
|
|
httpMethod: "POST",
|
|
path: "stage-changed",
|
|
responseMode: "responseNode",
|
|
options: {},
|
|
},
|
|
id: "notifier-webhook-id",
|
|
name: "Stage Changed Webhook",
|
|
type: "n8n-nodes-base.webhook",
|
|
typeVersion: 1.1,
|
|
position: [100, 300],
|
|
},
|
|
{
|
|
parameters: {
|
|
resource: "row",
|
|
operation: "get",
|
|
tableId: "candidates",
|
|
rowId: "={{ $('Stage Changed Webhook').item.json.body.new_record.candidate_id }}",
|
|
},
|
|
id: "notifier-get-cand-id",
|
|
name: "Get Candidate",
|
|
type: "n8n-nodes-base.supabase",
|
|
typeVersion: 1,
|
|
position: [320, 300],
|
|
credentials: {
|
|
supabaseApi: {
|
|
id: supabaseCredId,
|
|
name: "Semillero2_Supabase",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
parameters: {
|
|
resource: "row",
|
|
operation: "get",
|
|
tableId: "jobs",
|
|
rowId: "={{ $('Stage Changed Webhook').item.json.body.new_record.job_id }}",
|
|
},
|
|
id: "notifier-get-job-id",
|
|
name: "Get Job",
|
|
type: "n8n-nodes-base.supabase",
|
|
typeVersion: 1,
|
|
position: [540, 300],
|
|
credentials: {
|
|
supabaseApi: {
|
|
id: supabaseCredId,
|
|
name: "Semillero2_Supabase",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
parameters: {
|
|
jsCode: `const candidate = $('Get Candidate').first().json;
|
|
const candidateName = candidate.name;
|
|
const contactInfo = candidate.contact_info || {};
|
|
const candidateEmail = contactInfo.email || "unknown@example.com";
|
|
const jobTitle = $('Get Job').first().json.title;
|
|
const oldRecord = $('Stage Changed Webhook').first().json.body.old_record;
|
|
const oldStage = oldRecord ? oldRecord.stage : 'None';
|
|
const newStage = $('Stage Changed Webhook').first().json.body.new_record.stage;
|
|
|
|
let subject = '';
|
|
let html = '';
|
|
|
|
if (newStage === 'Screening') {
|
|
subject = \`Application Received: \${jobTitle}\`;
|
|
html = \`<p>Hello \${candidateName},</p>
|
|
<p>Thank you for applying to the <strong>\${jobTitle}</strong> position. We have received your CV and are currently reviewing it.</p>
|
|
<p>Best regards,<br/>The Recruitment Team</p>\`;
|
|
} else if (newStage === 'Technical') {
|
|
subject = \`Next Steps - Technical Interview: \${jobTitle}\`;
|
|
html = \`<p>Hello \${candidateName},</p>
|
|
<p>Great news! Your application for the <strong>\${jobTitle}</strong> position has progressed to the <strong>Technical Interview</strong> stage.</p>
|
|
<p>Our team will contact you shortly to schedule the interview. Please prepare by reviewing core concepts for the role.</p>
|
|
<p>Best regards,<br/>The Recruitment Team</p>\`;
|
|
} else if (newStage === 'Cultural') {
|
|
subject = \`Next Steps - Cultural Interview: \${jobTitle}\`;
|
|
html = \`<p>Hello \${candidateName},</p>
|
|
<p>We are pleased to invite you to the <strong>Cultural Fit Interview</strong> for the <strong>\${jobTitle}</strong> role.</p>
|
|
<p>This conversation will be a casual discussion to get to know you better and answer any questions you have about our culture and team.</p>
|
|
<p>Best regards,<br/>The Recruitment Team</p>\`;
|
|
} else if (newStage === 'Offer') {
|
|
subject = \`Job Offer - \${jobTitle}!\`;
|
|
html = \`<p>Hello \${candidateName},</p>
|
|
<p>Congratulations! We are thrilled to extend a <strong>Job Offer</strong> for the <strong>\${jobTitle}</strong> position.</p>
|
|
<p>We will send the official offer letter and details via email shortly. We look forward to having you join us!</p>
|
|
<p>Best regards,<br/>The Hiring Team</p>\`;
|
|
} else if (newStage === 'Hired') {
|
|
subject = \`Welcome to the Team!\`;
|
|
html = \`<p>Hello \${candidateName},</p>
|
|
<p>Welcome aboard! You have been officially <strong>Hired</strong> for the <strong>\${jobTitle}</strong> position.</p>
|
|
<p>Our onboarding team will reach out with the next steps for your first day.</p>
|
|
<p>Best regards,<br/>The Team</p>\`;
|
|
} else if (newStage === 'Rejected') {
|
|
subject = \`Application Update: \${jobTitle}\`;
|
|
html = \`<p>Hello \${candidateName},</p>
|
|
<p>Thank you for your interest in the <strong>\${jobTitle}</strong> position and for taking the time to interview with us.</p>
|
|
<p>Unfortunately, we have decided to proceed with other candidates whose profiles more closely align with our current needs.</p>
|
|
<p>We will keep your profile in our database for future opportunities.</p>
|
|
<p>Best regards,<br/>The Recruitment Team</p>\`;
|
|
} else {
|
|
subject = \`Application Status Update: \${jobTitle}\`;
|
|
html = \`<p>Hello \${candidateName},</p>
|
|
<p>The status of your application for the <strong>\${jobTitle}</strong> position has been updated to: <strong>\${newStage}</strong>.</p>
|
|
<p>Best regards,<br/>The Recruitment Team</p>\`;
|
|
}
|
|
|
|
return [{
|
|
json: {
|
|
toEmail: candidateEmail,
|
|
subject,
|
|
html
|
|
}
|
|
}];`,
|
|
},
|
|
id: "notifier-code-id",
|
|
name: "Format Notification Email",
|
|
type: "n8n-nodes-base.code",
|
|
typeVersion: 2,
|
|
position: [760, 300],
|
|
},
|
|
{
|
|
parameters: {
|
|
fromEmail: '"Recruitment Team" <robot@gaboggamer.online>',
|
|
toEmail: "={{ $json.toEmail }}",
|
|
subject: "={{ $json.subject }}",
|
|
html: "={{ $json.html }}",
|
|
options: {},
|
|
},
|
|
id: "notifier-send-id",
|
|
name: "Send Notification Email",
|
|
type: "n8n-nodes-base.emailSend",
|
|
typeVersion: 2.1,
|
|
position: [980, 300],
|
|
credentials: {
|
|
smtp: {
|
|
id: smtpCredId,
|
|
name: "Semillero2_SMTP",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
parameters: {
|
|
options: {},
|
|
},
|
|
id: "notifier-respond-id",
|
|
name: "Respond to Webhook",
|
|
type: "n8n-nodes-base.respondToWebhook",
|
|
typeVersion: 1.1,
|
|
position: [1200, 300],
|
|
},
|
|
],
|
|
connections: {
|
|
"Stage Changed Webhook": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Get Candidate",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
"Get Candidate": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Get Job",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
"Get Job": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Format Notification Email",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
"Format Notification Email": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Send Notification Email",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
"Send Notification Email": {
|
|
main: [
|
|
[
|
|
{
|
|
node: "Respond to Webhook",
|
|
type: "main",
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
let notifierDeployResult;
|
|
if (notifierWf) {
|
|
if (notifierWf.isArchived) {
|
|
console.log(`Deleting archived Notifier workflow (ID: ${notifierWf.id})...`);
|
|
await n8nRequest(`/api/v1/workflows/${notifierWf.id}`, "DELETE");
|
|
console.log("Creating new Notifier workflow...");
|
|
notifierDeployResult = await n8nRequest("/api/v1/workflows", "POST", notifierWfDefinition);
|
|
} else {
|
|
console.log(`Updating existing Notifier workflow (ID: ${notifierWf.id})...`);
|
|
notifierDeployResult = await n8nRequest(`/api/v1/workflows/${notifierWf.id}`, "PUT", notifierWfDefinition);
|
|
}
|
|
} else {
|
|
console.log("Creating new Notifier workflow...");
|
|
notifierDeployResult = await n8nRequest("/api/v1/workflows", "POST", notifierWfDefinition);
|
|
}
|
|
|
|
// Activate Notifier Workflow
|
|
console.log(`Activating Notifier workflow (ID: ${notifierDeployResult.id})...`);
|
|
await n8nRequest(`/api/v1/workflows/${notifierDeployResult.id}/activate`, "POST");
|
|
|
|
const notifierWebhookUrl = `${N8N_HOST}/webhook/stage-changed`;
|
|
console.log("\n==============================================");
|
|
console.log("EMAIL AUTOMATION SETUP COMPLETE");
|
|
console.log("==============================================");
|
|
console.log(`IMAP Workflow ID: ${imapDeployResult.id}`);
|
|
console.log(`Notifier Workflow ID: ${notifierDeployResult.id}`);
|
|
console.log(`Notifier Webhook URL: ${notifierWebhookUrl}`);
|
|
console.log("==============================================");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Email automation setup failed:", err);
|
|
process.exit(1);
|
|
});
|