You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
6.1 KiB
TypeScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
}
}
}