UNPKG

apptise-core

Version:

Core library for Apptise unified notification system

314 lines 12.7 kB
import { NotificationPlugin } from '../base/plugin.js'; import { HttpClient } from '../utils/http-client.js'; import { ErrorType, NotificationLevel } from '../base/types.js'; /** * IFTTT notification plugin * * Supports the following URL formats (templates): * - {schema}://{webhook_id}/{events} * * Where schema is 'ifttt' * * @example * ```typescript * const plugin = new IftttPlugin(); * const config = plugin.parseUrl('ifttt://a3nHB7gA9TfBQSqJAHklod/event1/event2'); * const result = await plugin.send(config, { * title: 'Test Title', * body: 'Test message content' * }); * ``` */ export class IftttPlugin extends NotificationPlugin { // Service registration information registration = { serviceId: 'ifttt', protocols: ['ifttt'], name: 'IFTTT', description: 'If This Then That webhook notifications', version: '1.0.0', }; // Service configuration constants serviceConfig = { // Service information name: 'IFTTT', url: 'https://ifttt.com/', setupUrl: 'https://github.com/caronc/apprise/wiki/Notify_ifttt', // API configuration baseUrl: 'https://maker.ifttt.com/trigger', // Request configuration timeout: 10000, // 10 seconds userAgent: 'python-requests/2.32.3', // Match Python version's User-Agent // Content type jsonContentType: 'application/json', // Default IFTTT keys for mapping notification data defaultTitleKey: 'value1', defaultBodyKey: 'value2', defaultTypeKey: 'value3', }; // URL templates supported by IFTTT templates = [ '{schema}://{webhook_id}/{events}', ]; // Template tokens (URL path parameters) templateTokens = { webhook_id: { name: 'Webhook ID', type: 'string', required: true, private: true, }, events: { name: 'Events', type: 'list:string', required: true, }, }; // Template arguments (query parameters) templateArgs = { to: { name: 'Events', type: 'list:string', alias_of: 'events', }, }; /** * Parse IFTTT URL and extract configuration * * @param url - IFTTT URL to parse * @returns Parsed plugin configuration */ parseUrl(url) { try { const parsed = new URL(url); // Validate protocol const protocol = parsed.protocol.slice(0, -1); // Remove trailing ':' if (!this.registration.protocols.includes(protocol)) { throw this.createError(ErrorType.VALIDATION_ERROR, `Unsupported protocol: ${protocol}`); } // Extract webhook_id from hostname or username let webhookId = ''; let events = []; if (parsed.username) { // Format: ifttt://webhook_id@host/event1/event2 webhookId = decodeURIComponent(parsed.username); // Host becomes first event if present if (parsed.hostname) { events.push(decodeURIComponent(parsed.hostname)); } } else { // Format: ifttt://webhook_id/event1/event2 webhookId = decodeURIComponent(parsed.hostname); } // Validate webhook_id if (!webhookId || !/^[A-Z0-9_-]+$/i.test(webhookId)) { throw this.createError(ErrorType.VALIDATION_ERROR, `Invalid IFTTT Webhook ID: ${webhookId}`); } // Extract events from path const pathParts = parsed.pathname .split('/') .filter(part => part.length > 0) .map(part => decodeURIComponent(part)); events.push(...pathParts); // Extract events from 'to' query parameter const toParam = parsed.searchParams.get('to'); if (toParam) { const toEvents = toParam.split(',').map(e => e.trim()).filter(e => e.length > 0); events.push(...toEvents); } // Validate events if (events.length === 0) { throw this.createError(ErrorType.VALIDATION_ERROR, 'You must specify at least one event to trigger'); } // Extract add_tokens (custom key-value pairs with + prefix) const addTokens = {}; for (const [key, value] of parsed.searchParams.entries()) { if (key.startsWith('+')) { const tokenKey = key.substring(1); addTokens[tokenKey] = value; } } // Extract del_tokens (tokens to remove with - prefix) const delTokens = []; for (const [key] of parsed.searchParams.entries()) { if (key.startsWith('-')) { const tokenKey = key.substring(1); delTokens.push(tokenKey); } } return { serviceId: this.registration.serviceId, url, config: { webhook_id: webhookId, events, add_tokens: addTokens, del_tokens: delTokens, }, }; } catch (error) { if (error instanceof Error && error.message.includes('Invalid URL')) { throw this.createError(ErrorType.VALIDATION_ERROR, `Invalid IFTTT URL format: ${url}`); } throw error; } } /** * Send notification via IFTTT webhook * * @param config - Plugin configuration * @param message - Notification message * @returns Notification result */ async send(config, message) { const { result, duration } = await this.measureTime(async () => { return this.safeExecute(async () => { // Validate configuration if (!this.validateConfig(config)) { throw this.createError(ErrorType.VALIDATION_ERROR, 'Invalid IFTTT plugin configuration'); } // Validate message if (!this.validateMessage(message)) { throw this.createError(ErrorType.VALIDATION_ERROR, 'Invalid notification message'); } const { webhook_id, events, add_tokens, del_tokens } = config.config; // Process each event const results = []; let hasError = false; for (const event of events) { try { // Build notify URL for this event const notifyUrl = `${this.serviceConfig.baseUrl}/${event}/with/key/${webhook_id}`; // Prepare JSON payload const payload = { [this.serviceConfig.defaultTitleKey]: message.title || 'Apptise Notification', [this.serviceConfig.defaultBodyKey]: message.body || '', [this.serviceConfig.defaultTypeKey]: this.mapNotificationLevelToType(message.level || NotificationLevel.INFO), }; // Add custom tokens Object.assign(payload, add_tokens); // Remove tokens flagged for deletion (convert keys to lowercase as IFTTT expects) const finalPayload = {}; for (const [key, value] of Object.entries(payload)) { const lowerKey = key.toLowerCase(); if (!del_tokens.includes(key) && !del_tokens.includes(lowerKey)) { finalPayload[lowerKey] = value; } } const requestData = JSON.stringify(finalPayload); const headers = { 'Content-Type': this.serviceConfig.jsonContentType, 'User-Agent': this.serviceConfig.userAgent, }; // Log HTTP request details for equivalence testing console.log(`[APPTISE_HTTP_REQUEST] Method: POST`); console.log(`[APPTISE_HTTP_REQUEST] URL: ${notifyUrl}`); console.log(`[APPTISE_HTTP_REQUEST] Headers: ${JSON.stringify(headers)}`); console.log(`[APPTISE_HTTP_REQUEST] Data: ${requestData}`); console.log(`[APPTISE_HTTP_REQUEST] Timeout: ${this.serviceConfig.timeout}`); // Send HTTP request const response = await HttpClient.post(notifyUrl, requestData, { headers, timeout: this.serviceConfig.timeout, }); // Log HTTP response details for equivalence testing console.log(`[APPTISE_HTTP_RESPONSE] Status: ${response.status}`); console.log(`[APPTISE_HTTP_RESPONSE] Headers: ${JSON.stringify(response.headers)}`); console.log(`[APPTISE_HTTP_RESPONSE] Content: ${JSON.stringify(response.data)}`); // Handle response if (!response.ok) { console.warn(`Failed to send IFTTT notification to ${event}: HTTP ${response.status} ${response.statusText}`); hasError = true; } else { console.log(`Sent IFTTT notification to ${event}`); results.push({ event, success: true }); } } catch (error) { console.warn(`A Connection error occurred sending IFTTT:${event} notification: ${error instanceof Error ? error.message : String(error)}`); hasError = true; } } if (hasError && results.length === 0) { throw this.createError(ErrorType.NETWORK_ERROR, 'Failed to send notifications to all IFTTT events'); } return { events: results, hasError }; }, ErrorType.NETWORK_ERROR); }); return this.createSuccessResult(result, duration); } /** * Validate IFTTT plugin configuration * * @param config - Plugin configuration to validate * @returns True if configuration is valid */ validateConfig(config) { if (!config || !config.config) { return false; } const { webhook_id, events } = config.config; // Validate webhook_id if (!webhook_id || typeof webhook_id !== 'string' || !/^[A-Z0-9_-]+$/i.test(webhook_id)) { return false; } // Validate events if (!events || !Array.isArray(events) || events.length === 0) { return false; } // Validate each event name for (const event of events) { if (!event || typeof event !== 'string') { return false; } } return true; } /** * Validate notification message * * @param message - Notification message to validate * @returns True if message is valid */ validateMessage(message) { if (!message) { return false; } // At least body should be provided return typeof message.body === 'string' && message.body.length > 0; } /** * Map notification level to IFTTT type string * * @param level - Notification level * @returns IFTTT type string */ mapNotificationLevelToType(level) { switch (level) { case NotificationLevel.SUCCESS: return 'success'; case NotificationLevel.WARNING: return 'warning'; case NotificationLevel.FAILURE: return 'failure'; case NotificationLevel.INFO: default: return 'info'; } } } /** * Create a new IFTTT plugin instance * * @returns New IFTTT plugin instance */ export function createIftttPlugin() { return new IftttPlugin(); } // Export default plugin instance export const iftttPlugin = createIftttPlugin(); export default iftttPlugin; //# sourceMappingURL=ifttt.js.map