apptise-core
Version:
Core library for Apptise unified notification system
314 lines • 12.7 kB
JavaScript
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