820 lines
24 KiB
TypeScript
820 lines
24 KiB
TypeScript
import {type NtfyNotification, NtfyPriority, NtfyTag} from '@plunk/types';
|
|
import signale from 'signale';
|
|
|
|
/**
|
|
* Service for sending notifications via ntfy.sh
|
|
* Supports configurable ntfy server URL via NTFY_URL environment variable
|
|
*/
|
|
export class NtfyService {
|
|
private static ntfyUrl: string | null = null;
|
|
private static isConfigured = false;
|
|
|
|
/**
|
|
* Send a notification to ntfy
|
|
* @param notification - The notification details
|
|
* @returns Promise<void>
|
|
*/
|
|
public static async send(notification: NtfyNotification): Promise<void> {
|
|
this.initialize();
|
|
|
|
if (!this.ntfyUrl) {
|
|
// Silently skip if not configured
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'text/plain',
|
|
'Title': notification.title,
|
|
};
|
|
|
|
if (notification.priority) {
|
|
headers.Priority = notification.priority.toString();
|
|
}
|
|
|
|
if (notification.tags && notification.tags.length > 0) {
|
|
headers.Tags = notification.tags.join(',');
|
|
}
|
|
|
|
const response = await fetch(this.ntfyUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body: notification.message,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
signale.success(`[NTFY] Sent notification: ${notification.title}`);
|
|
} catch (error) {
|
|
signale.error('[NTFY] Failed to send notification:', error);
|
|
// Don't throw - notifications should not break the main flow
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a high-priority notification
|
|
*/
|
|
public static async sendHigh(title: string, message: string, tags?: NtfyTag[]): Promise<void> {
|
|
await this.send({
|
|
title,
|
|
message,
|
|
priority: NtfyPriority.HIGH,
|
|
tags,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a max-priority urgent notification
|
|
*/
|
|
public static async sendUrgent(title: string, message: string, tags?: NtfyTag[]): Promise<void> {
|
|
await this.send({
|
|
title,
|
|
message,
|
|
priority: NtfyPriority.MAX,
|
|
tags,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a low-priority informational notification
|
|
*/
|
|
public static async sendInfo(title: string, message: string, tags?: NtfyTag[]): Promise<void> {
|
|
await this.send({
|
|
title,
|
|
message,
|
|
priority: NtfyPriority.LOW,
|
|
tags,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a default priority notification
|
|
*/
|
|
public static async sendDefault(title: string, message: string, tags?: NtfyTag[]): Promise<void> {
|
|
await this.send({
|
|
title,
|
|
message,
|
|
priority: NtfyPriority.DEFAULT,
|
|
tags,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about a new project creation
|
|
*/
|
|
public static async notifyProjectCreated(projectName: string, projectId: string, userId: string): Promise<void> {
|
|
await this.sendDefault(
|
|
'New Project Created',
|
|
`Project "${projectName}" (${projectId}) was created by user ${userId}`,
|
|
[NtfyTag.ROCKET, NtfyTag.SUCCESS],
|
|
);
|
|
}
|
|
|
|
// ===== Event-specific notification helpers =====
|
|
|
|
/**
|
|
* Notify about subscription started
|
|
*/
|
|
public static async notifySubscriptionStarted(
|
|
projectName: string,
|
|
projectId: string,
|
|
subscriptionId: string,
|
|
): Promise<void> {
|
|
await this.sendHigh(
|
|
'Subscription Started',
|
|
`Project "${projectName}" (${projectId}) started a paid subscription (${subscriptionId})`,
|
|
[NtfyTag.MONEY, NtfyTag.SUCCESS],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about subscription cancelled
|
|
*/
|
|
public static async notifySubscriptionCancelled(
|
|
projectName: string,
|
|
projectId: string,
|
|
subscriptionId: string,
|
|
): Promise<void> {
|
|
await this.sendHigh(
|
|
'Subscription Cancelled',
|
|
`Project "${projectName}" (${projectId}) cancelled their subscription (${subscriptionId})`,
|
|
[NtfyTag.WARNING, NtfyTag.MONEY],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about project disabled due to security risk
|
|
*/
|
|
public static async notifyProjectDisabledForSecurity(
|
|
projectName: string,
|
|
projectId: string,
|
|
violations: string[],
|
|
): Promise<void> {
|
|
const violationText = violations.join(', ');
|
|
await this.sendUrgent(
|
|
'Project Disabled - Security Risk',
|
|
`Project "${projectName}" (${projectId}) was automatically disabled due to: ${violationText}`,
|
|
[NtfyTag.SHIELD, NtfyTag.ERROR, NtfyTag.SKULL],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about payment failure
|
|
*/
|
|
public static async notifyPaymentFailed(projectName: string, projectId: string): Promise<void> {
|
|
await this.sendHigh('Payment Failed', `Payment failed for project "${projectName}" (${projectId})`, [
|
|
NtfyTag.WARNING,
|
|
NtfyTag.MONEY,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Notify about project disabled due to payment failure
|
|
*/
|
|
public static async notifyProjectDisabledForPayment(projectName: string, projectId: string): Promise<void> {
|
|
await this.sendUrgent(
|
|
'Project Disabled - Payment Failed',
|
|
`Project "${projectName}" (${projectId}) was automatically disabled due to a failed recurring payment`,
|
|
[NtfyTag.WARNING, NtfyTag.MONEY, NtfyTag.ERROR],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about successful invoice payment - MIN priority (routine)
|
|
*/
|
|
public static async notifyInvoicePaid(projectName: string, projectId: string): Promise<void> {
|
|
await this.send({
|
|
title: 'Invoice Paid',
|
|
message: `Invoice paid for project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.MIN,
|
|
tags: [NtfyTag.MONEY, NtfyTag.SUCCESS],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about subscription update - LOW priority (minor changes)
|
|
*/
|
|
public static async notifySubscriptionUpdated(projectName: string, projectId: string): Promise<void> {
|
|
await this.send({
|
|
title: 'Subscription Updated',
|
|
message: `Subscription updated for project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.MONEY, NtfyTag.INFO],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about project deletion - DEFAULT priority
|
|
*/
|
|
public static async notifyProjectDeleted(projectName: string, projectId: string, userId: string): Promise<void> {
|
|
await this.sendDefault('Project Deleted', `Project "${projectName}" (${projectId}) was deleted by user ${userId}`, [
|
|
NtfyTag.WARNING,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Notify about security warning (non-critical)
|
|
*/
|
|
public static async notifySecurityWarning(projectName: string, projectId: string, warnings: string[]): Promise<void> {
|
|
// Import redis at runtime to avoid circular dependencies
|
|
const {redis} = await import('../database/redis.js');
|
|
|
|
const cacheKey = `ntfy:security:warning:${projectId}`;
|
|
const ttl = 3600; // 1 hour
|
|
|
|
// Use SETNX to atomically check and set the flag (prevents race conditions)
|
|
const wasSet = await redis.set(cacheKey, '1', 'EX', ttl, 'NX');
|
|
if (!wasSet) {
|
|
return;
|
|
}
|
|
|
|
const warningText = warnings.join(', ');
|
|
await this.sendDefault(
|
|
'Security Warning',
|
|
`Project "${projectName}" (${projectId}) has security warnings: ${warningText}`,
|
|
[NtfyTag.WARNING, NtfyTag.SHIELD],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about email bounce - LOW priority (high volume)
|
|
* Rate-limited to only send notification every 20 bounces with latest 20 bounce details
|
|
*/
|
|
public static async notifyEmailBounce(
|
|
projectName: string,
|
|
projectId: string,
|
|
recipientEmail: string,
|
|
bounceType?: string,
|
|
): Promise<void> {
|
|
// Import redis at runtime to avoid circular dependencies
|
|
const {redis} = await import('../database/redis.js');
|
|
|
|
// Use Redis counters to track bounce count globally
|
|
const globalCountKey = `ntfy:bounce:count`;
|
|
const bounceListKey = `ntfy:bounce:latest`;
|
|
|
|
// Increment global counter
|
|
const globalCount = await redis.incr(globalCountKey);
|
|
|
|
// Store this bounce event in a list (keep latest 20)
|
|
const bounceEvent = JSON.stringify({
|
|
projectName,
|
|
projectId,
|
|
recipientEmail,
|
|
bounceType: bounceType || 'Unknown',
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
await redis.lpush(bounceListKey, bounceEvent);
|
|
await redis.ltrim(bounceListKey, 0, 19); // Keep only latest 20
|
|
|
|
// Set expiry on first increment (24 hour rolling window)
|
|
if (globalCount === 1) {
|
|
await redis.expire(globalCountKey, 86400);
|
|
await redis.expire(bounceListKey, 86400);
|
|
}
|
|
|
|
// Only send notification every 20 bounces
|
|
if (globalCount % 20 === 0) {
|
|
// Get the latest 20 bounces
|
|
const latestBounces = await redis.lrange(bounceListKey, 0, 19);
|
|
|
|
// Parse and format the bounce events
|
|
const bounceDetails = latestBounces
|
|
.map(bounce => {
|
|
try {
|
|
const parsed = JSON.parse(bounce);
|
|
return ` • ${parsed.recipientEmail} (${parsed.bounceType}) - ${parsed.projectName}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
|
|
await this.send({
|
|
title: 'Email Bounces',
|
|
message: `20 email bounces detected (total: ${globalCount})\n\nLatest 20 bounces:\n${bounceDetails}`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.WARNING],
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify about email complaint - HIGH priority (reputation risk)
|
|
*/
|
|
public static async notifyEmailComplaint(
|
|
projectName: string,
|
|
projectId: string,
|
|
recipientEmail: string,
|
|
): Promise<void> {
|
|
await this.sendHigh(
|
|
'Email Complaint',
|
|
`Email complaint received from ${recipientEmail} in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.WARNING, NtfyTag.ERROR],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about new user account created via signup - LOW priority
|
|
*/
|
|
public static async notifyUserSignup(userEmail: string, userId: string): Promise<void> {
|
|
await this.send({
|
|
title: 'New User Signup',
|
|
message: `New user registered: ${userEmail} (${userId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.ROCKET, NtfyTag.SUCCESS],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about failed signup attempt with invalid email - LOW priority
|
|
*/
|
|
public static async notifyFailedSignupAttempt(email: string, reasons: string[]): Promise<void> {
|
|
const reasonText = reasons.join(', ');
|
|
await this.send({
|
|
title: 'Failed Signup - Invalid Email',
|
|
message: `Signup attempt blocked for email: ${email}\nReasons: ${reasonText}`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.WARNING, NtfyTag.SHIELD],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about new user account created via OAuth - LOW priority
|
|
*/
|
|
public static async notifyUserOAuthSignup(
|
|
userEmail: string,
|
|
userId: string,
|
|
provider: 'GitHub' | 'Google',
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: `New User - ${provider} OAuth`,
|
|
message: `New user registered via ${provider}: ${userEmail} (${userId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.ROCKET, NtfyTag.SUCCESS],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about campaign created (draft) - LOW priority
|
|
*/
|
|
public static async notifyCampaignCreated(
|
|
campaignName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: 'Campaign Created',
|
|
message: `Campaign "${campaignName}" created in project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.ROCKET],
|
|
});
|
|
}
|
|
|
|
// ===== Campaign event notifications =====
|
|
|
|
/**
|
|
* Notify about campaign scheduled - DEFAULT priority
|
|
*/
|
|
public static async notifyCampaignScheduled(
|
|
campaignName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
scheduledFor: Date,
|
|
recipientCount: number,
|
|
): Promise<void> {
|
|
await this.sendDefault(
|
|
'Campaign Scheduled',
|
|
`Campaign "${campaignName}" scheduled to send to ${recipientCount} recipients at ${scheduledFor.toISOString()} in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.ROCKET, NtfyTag.INFO],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about campaign sending started - LOW priority (informational)
|
|
*/
|
|
public static async notifyCampaignSendingStarted(
|
|
campaignName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
recipientCount: number,
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: 'Campaign Sending Started',
|
|
message: `Campaign "${campaignName}" started sending to ${recipientCount} recipients in project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.ROCKET, NtfyTag.CHART],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about campaign send completed - DEFAULT priority
|
|
*/
|
|
public static async notifyCampaignSendCompleted(
|
|
campaignName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
recipientCount: number,
|
|
): Promise<void> {
|
|
await this.sendDefault(
|
|
'Campaign Send Completed',
|
|
`Campaign "${campaignName}" successfully sent to ${recipientCount} recipients in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.ROCKET, NtfyTag.SUCCESS],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about campaign cancelled - LOW priority
|
|
*/
|
|
public static async notifyCampaignCancelled(
|
|
campaignName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: 'Campaign Cancelled',
|
|
message: `Campaign "${campaignName}" was cancelled in project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.WARNING],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about campaign deleted - LOW priority
|
|
*/
|
|
public static async notifyCampaignDeleted(
|
|
campaignName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: 'Campaign Deleted',
|
|
message: `Campaign "${campaignName}" was deleted from project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.INFO],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about workflow created - LOW priority
|
|
*/
|
|
public static async notifyWorkflowCreated(
|
|
workflowName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: 'Workflow Created',
|
|
message: `Workflow "${workflowName}" created in project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.ROCKET],
|
|
});
|
|
}
|
|
|
|
// ===== Workflow event notifications =====
|
|
|
|
/**
|
|
* Notify about workflow enabled
|
|
*/
|
|
public static async notifyWorkflowEnabled(
|
|
workflowName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.sendDefault(
|
|
'Workflow Enabled',
|
|
`Workflow "${workflowName}" is now active in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.SUCCESS],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about workflow disabled
|
|
*/
|
|
public static async notifyWorkflowDisabled(
|
|
workflowName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.sendDefault(
|
|
'Workflow Disabled',
|
|
`Workflow "${workflowName}" has been disabled in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.WARNING],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about workflow deleted - LOW priority
|
|
*/
|
|
public static async notifyWorkflowDeleted(
|
|
workflowName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: 'Workflow Deleted',
|
|
message: `Workflow "${workflowName}" was deleted from project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.INFO],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about workflow execution failed
|
|
*/
|
|
public static async notifyWorkflowExecutionFailed(
|
|
workflowName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
contactEmail: string,
|
|
error: string,
|
|
): Promise<void> {
|
|
await this.sendHigh(
|
|
'Workflow Execution Failed',
|
|
`Workflow "${workflowName}" failed for contact ${contactEmail} in project "${projectName}" (${projectId}). Error: ${error}`,
|
|
[NtfyTag.ERROR, NtfyTag.WARNING],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about domain added
|
|
*/
|
|
public static async notifyDomainAdded(domain: string, projectName: string, projectId: string): Promise<void> {
|
|
await this.sendDefault(
|
|
'Domain Added',
|
|
`Domain "${domain}" added for verification in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.INFO],
|
|
);
|
|
}
|
|
|
|
// ===== Domain verification notifications =====
|
|
|
|
/**
|
|
* Notify about domain verified
|
|
*/
|
|
public static async notifyDomainVerified(domain: string, projectName: string, projectId: string): Promise<void> {
|
|
await this.sendDefault(
|
|
'Domain Verified',
|
|
`Domain "${domain}" is now verified and ready for use in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.SUCCESS, NtfyTag.SHIELD],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about domain verification failed
|
|
*/
|
|
public static async notifyDomainVerificationFailed(
|
|
domain: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
): Promise<void> {
|
|
await this.sendHigh(
|
|
'Domain Verification Failed',
|
|
`Domain "${domain}" failed verification in project "${projectName}" (${projectId}). Email sending may be affected.`,
|
|
[NtfyTag.WARNING, NtfyTag.SHIELD],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about domain removed
|
|
*/
|
|
public static async notifyDomainRemoved(domain: string, projectName: string, projectId: string): Promise<void> {
|
|
await this.sendInfo(
|
|
'Domain Removed',
|
|
`Domain "${domain}" was removed from project "${projectName}" (${projectId})`,
|
|
[NtfyTag.INFO],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about billing limit approaching (80% threshold)
|
|
*/
|
|
public static async notifyBillingLimitApproaching(
|
|
projectName: string,
|
|
projectId: string,
|
|
usage: number,
|
|
limit: number,
|
|
percentage: number,
|
|
sourceType: string,
|
|
): Promise<void> {
|
|
// Import redis at runtime to avoid circular dependencies
|
|
const {redis} = await import('../database/redis.js');
|
|
|
|
const cacheKey = `ntfy:billing:warning:${projectId}:${sourceType}`;
|
|
const ttl = 86400; // 24 hours
|
|
|
|
// Use SETNX to atomically check and set the flag (prevents race conditions)
|
|
const wasSet = await redis.set(cacheKey, '1', 'EX', ttl, 'NX');
|
|
if (!wasSet) {
|
|
return;
|
|
}
|
|
|
|
await this.sendDefault(
|
|
'Billing Limit Warning',
|
|
`Email usage at ${Math.round(percentage)}% (${usage}/${limit}) for ${sourceType} in project "${projectName}" (${projectId})`,
|
|
[NtfyTag.WARNING, NtfyTag.MONEY, NtfyTag.CHART],
|
|
);
|
|
}
|
|
|
|
// ===== Billing and usage limit notifications =====
|
|
|
|
/**
|
|
* Notify about billing limit exceeded - MAX priority (blocks operations)
|
|
*/
|
|
public static async notifyBillingLimitExceeded(
|
|
projectName: string,
|
|
projectId: string,
|
|
usage: number,
|
|
limit: number,
|
|
sourceType: string,
|
|
): Promise<void> {
|
|
// Import redis at runtime to avoid circular dependencies
|
|
const {redis} = await import('../database/redis.js');
|
|
|
|
const cacheKey = `ntfy:billing:exceeded:${projectId}:${sourceType}`;
|
|
const ttl = 86400; // 24 hours
|
|
|
|
// Use SETNX to atomically check and set the flag (prevents race conditions)
|
|
const wasSet = await redis.set(cacheKey, '1', 'EX', ttl, 'NX');
|
|
if (!wasSet) {
|
|
return;
|
|
}
|
|
|
|
await this.sendUrgent(
|
|
'Billing Limit Exceeded',
|
|
`Email usage limit reached (${usage}/${limit}) for ${sourceType} in project "${projectName}" (${projectId}). Further emails are blocked.`,
|
|
[NtfyTag.ERROR, NtfyTag.MONEY, NtfyTag.SKULL],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about API keys regenerated
|
|
*/
|
|
public static async notifyApiKeysRegenerated(projectName: string, projectId: string, userId: string): Promise<void> {
|
|
await this.sendHigh(
|
|
'API Keys Regenerated',
|
|
`API keys for project "${projectName}" (${projectId}) were regenerated by user ${userId}`,
|
|
[NtfyTag.SHIELD, NtfyTag.WARNING],
|
|
);
|
|
}
|
|
|
|
// ===== API key notifications =====
|
|
|
|
/**
|
|
* Notify about contact import started - MIN priority (routine, high volume)
|
|
*/
|
|
public static async notifyContactImportStarted(
|
|
projectName: string,
|
|
projectId: string,
|
|
filename: string,
|
|
totalRows: number,
|
|
): Promise<void> {
|
|
await this.send({
|
|
title: 'Contact Import Started',
|
|
message: `Importing ${totalRows} contacts from "${filename}" into project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.MIN,
|
|
tags: [NtfyTag.ROCKET],
|
|
});
|
|
}
|
|
|
|
// ===== Contact import notifications =====
|
|
|
|
/**
|
|
* Notify about contact import completed
|
|
*/
|
|
public static async notifyContactImportCompleted(
|
|
projectName: string,
|
|
projectId: string,
|
|
filename: string,
|
|
successCount: number,
|
|
createdCount: number,
|
|
updatedCount: number,
|
|
failureCount: number,
|
|
): Promise<void> {
|
|
await this.sendDefault(
|
|
'Contact Import Completed',
|
|
`Import "${filename}" completed for project "${projectName}" (${projectId}). Success: ${successCount} (${createdCount} new, ${updatedCount} updated), Errors: ${failureCount}`,
|
|
[NtfyTag.SUCCESS, NtfyTag.CHART],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about contact import failed
|
|
*/
|
|
public static async notifyContactImportFailed(
|
|
projectName: string,
|
|
projectId: string,
|
|
filename: string,
|
|
error: string,
|
|
): Promise<void> {
|
|
await this.sendHigh(
|
|
'Contact Import Failed',
|
|
`Contact import "${filename}" failed for project "${projectName}" (${projectId}). Error: ${error}`,
|
|
[NtfyTag.ERROR],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Notify about segment created - MIN priority
|
|
*/
|
|
public static async notifySegmentCreated(segmentName: string, projectName: string, projectId: string): Promise<void> {
|
|
await this.send({
|
|
title: 'Segment Created',
|
|
message: `Segment "${segmentName}" created in project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.MIN,
|
|
tags: [NtfyTag.INFO],
|
|
});
|
|
}
|
|
|
|
// ===== Segment notifications =====
|
|
|
|
/**
|
|
* Notify about segment membership computed - LOW priority
|
|
*/
|
|
public static async notifySegmentMembershipComputed(
|
|
segmentName: string,
|
|
projectName: string,
|
|
projectId: string,
|
|
memberCount: number,
|
|
added?: number,
|
|
removed?: number,
|
|
): Promise<void> {
|
|
let message = `Segment "${segmentName}" in project "${projectName}" (${projectId}) now has ${memberCount} members`;
|
|
|
|
if (added !== undefined && removed !== undefined) {
|
|
const changes: string[] = [];
|
|
if (added > 0) changes.push(`+${added} added`);
|
|
if (removed > 0) changes.push(`-${removed} removed`);
|
|
if (changes.length > 0) {
|
|
message += ` (${changes.join(', ')})`;
|
|
}
|
|
}
|
|
|
|
await this.send({
|
|
title: 'Segment Membership Updated',
|
|
message,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.CHART],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about bundled segment membership updates - LOW priority
|
|
* Used when multiple segments are updated in a single processing cycle
|
|
*/
|
|
public static async notifySegmentMembershipBundled(
|
|
projectName: string,
|
|
projectId: string,
|
|
segmentCount: number,
|
|
totalAdded: number,
|
|
totalRemoved: number,
|
|
): Promise<void> {
|
|
const changes: string[] = [];
|
|
if (totalAdded > 0) changes.push(`+${totalAdded} added`);
|
|
if (totalRemoved > 0) changes.push(`-${totalRemoved} removed`);
|
|
|
|
const changesText = changes.length > 0 ? ` (${changes.join(', ')})` : '';
|
|
const message = `${segmentCount} segment${segmentCount > 1 ? 's' : ''} updated in project "${projectName}" (${projectId})${changesText}`;
|
|
|
|
await this.send({
|
|
title: 'Segment Memberships Updated',
|
|
message,
|
|
priority: NtfyPriority.LOW,
|
|
tags: [NtfyTag.CHART],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Notify about segment deleted - MIN priority
|
|
*/
|
|
public static async notifySegmentDeleted(segmentName: string, projectName: string, projectId: string): Promise<void> {
|
|
await this.send({
|
|
title: 'Segment Deleted',
|
|
message: `Segment "${segmentName}" was deleted from project "${projectName}" (${projectId})`,
|
|
priority: NtfyPriority.MIN,
|
|
tags: [NtfyTag.INFO],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize the ntfy service with the configured URL
|
|
*/
|
|
private static initialize(): void {
|
|
if (this.isConfigured) {
|
|
return;
|
|
}
|
|
|
|
this.ntfyUrl = process.env.NTFY_URL || null;
|
|
this.isConfigured = true;
|
|
|
|
if (!this.ntfyUrl) {
|
|
signale.warn('[NTFY] NTFY_URL not configured - notifications will be skipped');
|
|
} else {
|
|
signale.info(`[NTFY] Initialized with URL: ${this.ntfyUrl}`);
|
|
}
|
|
}
|
|
}
|