UNPKG

apptise-core

Version:

Core library for Apptise unified notification system

323 lines 12.8 kB
import { NotificationPlugin } from '../base/plugin.js'; import { HttpClient } from '../utils/http-client.js'; import { ErrorType, NotificationLevel } from '../base/types.js'; /** * Line Notification Plugin * * Sends notifications via Line Messaging API * API Docs: https://developers.line.biz/en/reference/messaging-api/ */ export class LinePlugin extends NotificationPlugin { /** * Plugin registration information */ registration = { serviceId: 'line', protocols: ['line'], name: 'Line', description: 'Line Messaging API notifications', version: '1.0.0', }; /** * Service configuration constants */ serviceConfig = { // Line API endpoint notifyUrl: 'https://api.line.me/v2/bot/message/push', // Service limits maxBodyLength: 5000, titleMaxLength: 0, // Line doesn't support titles // HTTP configuration timeout: 10000, // 10 seconds userAgent: 'Apprise', }; /** * URL templates supported by this plugin */ templates = [ '{schema}://{token}/{targets}', ]; /** * Template tokens definition */ templateTokens = { token: { name: 'Access Token', type: 'string', required: true, private: true, }, targets: { name: 'Targets', type: 'list:string', required: true, }, }; /** * Template arguments definition */ templateArgs = { image: { name: 'Include Image', type: 'bool', default: true, mapTo: 'include_image', }, to: { aliasOf: 'targets', }, }; /** * Parse Line notification URL * * Supported formats: * - line://token/target1/target2 * - line://token/target?image=no * - line://?token=token&to=target1,target2 * * @param url - The notification URL to parse * @returns Parsed plugin configuration */ parseUrl(url) { const urlObj = new URL(url); // Validate protocol if (!this.registration.protocols.includes(urlObj.protocol.replace(':', ''))) { throw this.createError(ErrorType.VALIDATION_ERROR, `Invalid protocol: ${urlObj.protocol}`); } let token; let targets = []; let includeImage = true; // Parse token from URL const tokenFromQuery = urlObj.searchParams.get('token'); if (tokenFromQuery) { // Token specified in query parameters token = decodeURIComponent(tokenFromQuery); } else { // Token specified in hostname/path token = decodeURIComponent(urlObj.hostname); // Line Long Lived Tokens may include forward slashes // Parse path segments to find token ending with '=' const pathSegments = urlObj.pathname.split('/').filter(Boolean); if (!token.endsWith('=') && pathSegments.length > 0) { for (let i = 0; i < pathSegments.length; i++) { const segment = decodeURIComponent(pathSegments[i]); if (segment.endsWith('=')) { // Found token end, reconstruct full token token += '/' + pathSegments.slice(0, i + 1).map(s => decodeURIComponent(s)).join('/'); targets = pathSegments.slice(i + 1).map(s => decodeURIComponent(s)); break; } } // If no segment ends with '=', treat all path segments as targets if (!token.endsWith('=')) { targets = pathSegments.map(s => decodeURIComponent(s)); } } else { // Token is complete, all path segments are targets targets = pathSegments.map(s => decodeURIComponent(s)); } } // Parse targets from query parameters const toParam = urlObj.searchParams.get('to'); if (toParam) { const additionalTargets = decodeURIComponent(toParam) .split(/[\s\t\r\n,#\/\\]+/) .filter(Boolean); targets.push(...additionalTargets); } // Parse image parameter const imageParam = urlObj.searchParams.get('image'); if (imageParam !== null) { includeImage = imageParam.toLowerCase() !== 'no' && imageParam.toLowerCase() !== 'false'; } // Validate token if (!token || token.trim().length === 0) { throw this.createError(ErrorType.VALIDATION_ERROR, 'Access token is required'); } // Validate targets if (targets.length === 0) { throw this.createError(ErrorType.VALIDATION_ERROR, 'At least one target is required'); } const config = { token: token.trim(), targets, includeImage, }; return { serviceId: this.registration.serviceId, url, config, }; } /** * Get image URL based on notification level * * @param level - Notification level * @returns Image URL or null */ getImageUrl(level) { // Match Python version's image URL logic const baseUrl = 'https://github.com/caronc/apprise/raw/master/apprise/assets/themes/default'; switch (level) { case NotificationLevel.INFO: return `${baseUrl}/apprise-info-128x128.png`; case NotificationLevel.SUCCESS: return `${baseUrl}/apprise-success-128x128.png`; case NotificationLevel.WARNING: return `${baseUrl}/apprise-warning-128x128.png`; case NotificationLevel.FAILURE: return `${baseUrl}/apprise-failure-128x128.png`; default: return `${baseUrl}/apprise-info-128x128.png`; } } /** * Send notification via Line Messaging API * * @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 Line plugin configuration'); } // Validate message if (!this.validateMessage(message)) { throw this.createError(ErrorType.VALIDATION_ERROR, 'Invalid notification message'); } const { token, targets, includeImage } = config.config; if (targets.length === 0) { throw this.createError(ErrorType.VALIDATION_ERROR, 'No Line targets to notify'); } let hasError = false; const results = []; // Send notification to each target for (const target of targets) { try { // Prepare request headers const headers = { 'User-Agent': this.serviceConfig.userAgent, 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }; // Build message text - combine title and body like Python version let messageText = message.body || ''; if (message.title && message.title.trim()) { messageText = `${message.title}\r\n${message.body}`; } // Get image URL if available const imageUrl = this.getImageUrl(message.level || NotificationLevel.INFO); const messageObj = { type: 'text', text: messageText, sender: { name: 'Apprise', }, }; // Add iconUrl if image is available if (imageUrl) { messageObj.sender.iconUrl = imageUrl; } // Prepare message payload const payload = { to: target, messages: [messageObj], }; // Add image URL if configured // Note: Line API doesn't support custom icons in the same way as Python version // This is a placeholder for future image support implementation // Log HTTP request details for equivalence testing console.log(`[APPTISE_HTTP_REQUEST] Method: POST`); console.log(`[APPTISE_HTTP_REQUEST] URL: ${this.serviceConfig.notifyUrl}`); console.log(`[APPTISE_HTTP_REQUEST] Headers: ${JSON.stringify(headers)}`); console.log(`[APPTISE_HTTP_REQUEST] Data: ${JSON.stringify(payload)}`); console.log(`[APPTISE_HTTP_REQUEST] Timeout: ${this.serviceConfig.timeout}`); // Send HTTP request const response = await HttpClient.post(this.serviceConfig.notifyUrl, JSON.stringify(payload), { 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) { throw this.createError(ErrorType.NETWORK_ERROR, `Line API error: HTTP ${response.status} ${response.statusText}`); } results.push(response.data); } catch (error) { hasError = true; console.error(`Failed to send Line notification to ${target}:`, error); continue; } } if (hasError && results.length === 0) { throw this.createError(ErrorType.NETWORK_ERROR, 'Failed to send notifications to all targets'); } return results; }, ErrorType.NETWORK_ERROR); }); return this.createSuccessResult(result, duration); } /** * Validate Line plugin configuration * * @param config - Plugin configuration to validate * @returns True if configuration is valid */ validateConfig(config) { if (!super.validateConfig(config)) { return false; } const { token, targets, includeImage } = config.config; // Validate token if (!token || typeof token !== 'string' || token.trim().length === 0) { return false; } // Validate targets if (!Array.isArray(targets) || targets.length === 0) { return false; } for (const target of targets) { if (!target || typeof target !== 'string' || target.trim().length === 0) { return false; } } // Validate includeImage flag if (typeof includeImage !== 'boolean') { return false; } return true; } /** * Validate notification message for Line * * @param message - Notification message to validate * @returns True if message is valid */ validateMessage(message) { if (!super.validateMessage(message)) { return false; } // Line requires at least a body if (!message.body || message.body.trim().length === 0) { return false; } // Check message length limits if (message.body.length > this.serviceConfig.maxBodyLength) { return false; } return true; } } // Export singleton instance export const linePlugin = new LinePlugin(); //# sourceMappingURL=line.js.map