|
|
import { REQUIRED_ENV } from '../server';
|
|
|
|
|
|
type WebhookConfig = {
|
|
|
url: string;
|
|
|
enabled: boolean;
|
|
|
webhook_by_events: boolean;
|
|
|
webhook_base64: boolean;
|
|
|
events: string[];
|
|
|
};
|
|
|
|
|
|
type WebhookResponse = {
|
|
|
webhook: {
|
|
|
instanceName: string;
|
|
|
webhook: {
|
|
|
url: string;
|
|
|
events: string[];
|
|
|
enabled: boolean;
|
|
|
};
|
|
|
};
|
|
|
};
|
|
|
|
|
|
export class WebhookManager {
|
|
|
private static readonly REQUIRED_EVENTS = [
|
|
|
'APPLICATION_STARTUP',
|
|
|
'MESSAGES_UPSERT',
|
|
|
'GROUPS_UPSERT',
|
|
|
'MESSAGES_UPDATE',
|
|
|
'MESSAGES_DELETE',
|
|
|
'PRESENCE_UPDATE',
|
|
|
'CONTACTS_UPDATE',
|
|
|
'CHATS_UPDATE'
|
|
|
];
|
|
|
|
|
|
private static validateConfig() {
|
|
|
const missing = REQUIRED_ENV.filter(v => !process.env[v]);
|
|
|
if (missing.length) {
|
|
|
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
|
|
|
}
|
|
|
|
|
|
if (!process.env.WEBHOOK_URL?.trim()) {
|
|
|
throw new Error('WEBHOOK_URL environment variable is required and cannot be empty');
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const url = new URL(process.env.WEBHOOK_URL);
|
|
|
|
|
|
// Enhanced internal Docker URL validation
|
|
|
if (process.env.WEBHOOK_URL.startsWith('http://srv-captain--')) {
|
|
|
if (!url.port) {
|
|
|
console.warn('⚠️ Internal Docker URL missing port number - this may cause connection issues');
|
|
|
// Auto-fix for common CapRover default port
|
|
|
if (process.env.PORT) {
|
|
|
process.env.WEBHOOK_URL = process.env.WEBHOOK_URL.replace(
|
|
|
'http://srv-captain--',
|
|
|
`http://srv-captain--:${process.env.PORT}/`
|
|
|
);
|
|
|
console.warn(`⚠️ Auto-corrected URL to: ${process.env.WEBHOOK_URL.substring(0, 30)}...`);
|
|
|
return; // Skip further validation for corrected URL
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Allow internal docker URLs in production
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
|
if (!['http:', 'https:', 'http://srv-captain--'].some(prefix =>
|
|
|
process.env.WEBHOOK_URL?.startsWith(prefix))) {
|
|
|
console.warn('Production WEBHOOK_URL should use http/https or internal docker URL');
|
|
|
}
|
|
|
} else if (!['http:', 'https:'].includes(url.protocol)) {
|
|
|
throw new Error('WEBHOOK_URL must use http or https protocol');
|
|
|
}
|
|
|
} catch (e) {
|
|
|
throw new Error(`Invalid WEBHOOK_URL: ${e.message}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private static getConfig(): { webhook: WebhookConfig } {
|
|
|
return {
|
|
|
webhook: {
|
|
|
url: process.env.WEBHOOK_URL!,
|
|
|
enabled: true,
|
|
|
webhook_by_events: true,
|
|
|
webhook_base64: true,
|
|
|
events: this.REQUIRED_EVENTS,
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
private static getApiUrl(): string {
|
|
|
return `${process.env.EVOLUTION_API_URL}/webhook/set/${process.env.EVOLUTION_API_INSTANCE}`;
|
|
|
}
|
|
|
|
|
|
private static getHeaders(): HeadersInit {
|
|
|
return {
|
|
|
apikey: process.env.EVOLUTION_API_KEY!,
|
|
|
'Content-Type': 'application/json',
|
|
|
};
|
|
|
}
|
|
|
|
|
|
static async registerWebhook(): Promise<WebhookResponse> {
|
|
|
this.validateConfig();
|
|
|
|
|
|
// First test if our own endpoint is reachable
|
|
|
const endpointTest = await this.testWebhookEndpoint();
|
|
|
if (!endpointTest) {
|
|
|
throw new Error('Webhook endpoint test failed - check your server logs');
|
|
|
}
|
|
|
|
|
|
const config = this.getConfig();
|
|
|
const apiUrl = this.getApiUrl();
|
|
|
|
|
|
const logSafeUrl = (url: string) => url ? `${url.substring(0, 20)}...` : 'invalid-url';
|
|
|
|
|
|
console.log('ℹ️ Attempting to register webhook:', {
|
|
|
apiUrl,
|
|
|
config: {
|
|
|
...config,
|
|
|
webhook: {
|
|
|
...config.webhook,
|
|
|
url: logSafeUrl(config.webhook.url),
|
|
|
events: config.webhook.events
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const response = await fetch(apiUrl, {
|
|
|
method: 'POST',
|
|
|
headers: this.getHeaders(),
|
|
|
body: JSON.stringify(config),
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
let errorDetails = response.statusText;
|
|
|
try {
|
|
|
const errorBody = await response.text();
|
|
|
errorDetails += ` - ${errorBody}`;
|
|
|
} catch { }
|
|
|
throw new Error(`Failed to register webhook: ${errorDetails}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (!data?.enabled) {
|
|
|
throw new Error('Webhook registration failed - not enabled in response');
|
|
|
}
|
|
|
|
|
|
console.log('✅ Webhook successfully registered:', {
|
|
|
url: data.url,
|
|
|
events: data.events,
|
|
|
id: data.id
|
|
|
});
|
|
|
|
|
|
return data;
|
|
|
}
|
|
|
|
|
|
static async verifyWebhook(): Promise<boolean> {
|
|
|
try {
|
|
|
const url = `${process.env.EVOLUTION_API_URL}/webhook/find/${process.env.EVOLUTION_API_INSTANCE}`;
|
|
|
console.log('ℹ️ Verifying webhook at:', url);
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
method: 'GET',
|
|
|
headers: this.getHeaders(),
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
const body = await response.text();
|
|
|
console.error('❌ Webhook verification failed:', {
|
|
|
status: response.status,
|
|
|
statusText: response.statusText,
|
|
|
body
|
|
|
});
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
console.log('ℹ️ Webhook verification response:', data);
|
|
|
|
|
|
const isEnabled = data?.enabled === true;
|
|
|
if (!isEnabled) {
|
|
|
console.error('❌ Webhook not enabled in verification response');
|
|
|
}
|
|
|
return isEnabled;
|
|
|
} catch (error) {
|
|
|
console.error('❌ Webhook verification error:', error instanceof Error ? error.stack : error);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
static async testWebhookEndpoint(): Promise<boolean> {
|
|
|
try {
|
|
|
if (!process.env.WEBHOOK_URL) {
|
|
|
throw new Error('WEBHOOK_URL is not set');
|
|
|
}
|
|
|
|
|
|
// Skip self-test in production if using internal URL
|
|
|
if (process.env.NODE_ENV === 'production' &&
|
|
|
process.env.WEBHOOK_URL.startsWith('http://srv-captain--')) {
|
|
|
console.log('ℹ️ Skipping self-test for internal production URL');
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
console.log('ℹ️ Testing webhook endpoint:', process.env.WEBHOOK_URL);
|
|
|
|
|
|
const testPayload = {
|
|
|
test: true,
|
|
|
timestamp: new Date().toISOString()
|
|
|
};
|
|
|
|
|
|
const response = await fetch(process.env.WEBHOOK_URL, {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json',
|
|
|
},
|
|
|
body: JSON.stringify(testPayload),
|
|
|
timeout: 5000 // Add timeout to prevent hanging
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
const body = await response.text();
|
|
|
console.error('❌ Webhook test failed:', {
|
|
|
status: response.status,
|
|
|
statusText: response.statusText,
|
|
|
body
|
|
|
});
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
console.log('✅ Webhook endpoint test successful');
|
|
|
return true;
|
|
|
} catch (error) {
|
|
|
console.error('❌ Webhook test error:', error instanceof Error ? error.stack : error);
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
}
|