UNPKG

apptise-core

Version:

Core library for Apptise unified notification system

327 lines 13.5 kB
import { NotificationPlugin } from '../base/plugin.js'; import { HttpClient } from '../utils/http-client.js'; import { ErrorType } from '../base/types.js'; /** * PushDeer notification plugin * * Supports the following URL formats (templates): * - {schema}://{pushkey} * - {schema}://{host}/{pushkey} * - {schema}://{host}:{port}/{pushkey} * * Where schema can be 'pushdeer', 'pushdeers' (secure), or 'pd' (short form) * * @example * ```typescript * const plugin = new PushDeerPlugin(); * const config = plugin.parseUrl('pushdeer://your-pushkey'); * const result = await plugin.send(config, { * title: 'Test Title', * body: 'Test message content' * }); * ``` */ export class PushDeerPlugin extends NotificationPlugin { // Service registration information registration = { serviceId: 'pushdeer', protocols: ['pushdeer', 'pushdeers'], name: 'PushDeer', description: 'Open source message push service', version: '1.0.0', }; // Service configuration constants serviceConfig = { // Default hostname for PushDeer API defaultHostname: 'api2.pushdeer.com', // Default ports defaultPort: 80, defaultSecurePort: 443, // API endpoints apiPath: '/message/push', // Request configuration timeout: 10000, // 10 seconds userAgent: 'Apptise/1.0.0 PushDeer Plugin', // Message limits maxTitleLength: 500, maxBodyLength: 10000, }; // URL templates supported by PushDeer templates = [ '{schema}://{pushkey}', '{schema}://{host}/{pushkey}', '{schema}://{host}:{port}/{pushkey}', ]; // Template tokens (URL path parameters) templateTokens = { pushkey: { name: 'Pushkey', type: 'string', required: true, private: true, regex: ['^[a-z0-9]+$', 'i'], }, host: { name: 'Hostname', type: 'string', required: false, }, port: { name: 'Port', type: 'int', required: false, min: 1, max: 65535, }, }; // Template args (URL query parameters) templateArgs = { type: { name: 'Message Type', type: 'string', default: 'text', enum: ['text', 'markdown', 'image'], }, }; /** * Parse PushDeer URL and extract configuration * * Supports templates: * - {schema}://{pushkey} * - {schema}://{host}/{pushkey} * - {schema}://{host}:{port}/{pushkey} * * @param url - The PushDeer URL to parse * @returns Plugin configuration * @throws {Error} When URL format is invalid */ parseUrl(url) { const parsedUrl = this.parseUrlBase(url); // Validate protocol if (!this.registration.protocols.includes(parsedUrl.protocol)) { throw this.createError(ErrorType.INVALID_URL, `Unsupported protocol: ${parsedUrl.protocol}. Supported protocols: ${this.registration.protocols.join(', ')}`); } let pushkey; let host = null; let port = null; let secure = false; // Determine if secure protocol (if protocol ends with 's', then assume secure flag is set) // This matches Python's URLBase.post_process_parse_url_results logic secure = parsedUrl.protocol.endsWith('s'); // Parse URL based on templates const fullPath = parsedUrl.pathname || ''; const pathSegments = fullPath.split('/').filter(segment => segment.length > 0); if (pathSegments.length === 0) { // Template: {schema}://{pushkey} // The pushkey is in the hostname position pushkey = parsedUrl.hostname || ''; if (!pushkey) { throw this.createError(ErrorType.INVALID_URL, 'PushDeer pushkey is required'); } } else if (pathSegments.length === 1) { // Template: {schema}://{host}/{pushkey} or {schema}://{host}:{port}/{pushkey} host = parsedUrl.hostname || null; port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : null; pushkey = pathSegments[0]; if (!host) { throw this.createError(ErrorType.INVALID_URL, 'Host is required when pushkey is in path'); } } else { throw this.createError(ErrorType.INVALID_URL, 'Invalid URL format. Expected: {schema}://{pushkey} or {schema}://{host}[:{port}]/{pushkey}'); } // Validate pushkey format (case-insensitive alphanumeric) const pushkeyRegex = new RegExp(this.templateTokens.pushkey.regex[0], this.templateTokens.pushkey.regex[1]); if (!pushkeyRegex.test(pushkey)) { throw this.createError(ErrorType.INVALID_URL, 'Invalid pushkey format. Only alphanumeric characters are allowed'); } // Determine API endpoint let apiEndpoint; if (host) { // Custom host specified const protocol = secure || port === this.serviceConfig.defaultSecurePort ? 'https' : 'http'; const defaultPort = secure ? this.serviceConfig.defaultSecurePort : this.serviceConfig.defaultPort; const portSuffix = port && port !== defaultPort ? `:${port}` : ''; apiEndpoint = `${protocol}://${host}${portSuffix}`; } else { // Use default hostname with protocol based on secure flag const protocol = secure ? 'https' : 'http'; apiEndpoint = `${protocol}://${this.serviceConfig.defaultHostname}`; } // Extract query parameters for additional options const messageType = parsedUrl.searchParams.get('type') || this.templateArgs.type.default; // Validate message type if (!this.templateArgs.type.enum.includes(messageType)) { throw this.createError(ErrorType.INVALID_URL, `Invalid message type: ${messageType}. Supported types: ${this.templateArgs.type.enum.join(', ')}`); } const config = { pushkey, apiEndpoint, messageType, host, port, secure, }; return { serviceId: this.registration.serviceId, url, config, }; } /** * Send notification via PushDeer 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 PushDeer plugin configuration'); } // Validate message if (!this.validateMessage(message)) { throw this.createError(ErrorType.VALIDATION_ERROR, 'Invalid notification message'); } const { pushkey, apiEndpoint, messageType, host, port, secure } = config.config; // Build notify URL following PushDeer API format // Format: {schema}://{host}:{port}/message/push?pushkey={pushkey} let notifyUrl; if (host) { const schema = secure ? 'https' : 'http'; const defaultPort = secure ? this.serviceConfig.defaultSecurePort : this.serviceConfig.defaultPort; const portSuffix = port && port !== defaultPort ? `:${port}` : ''; notifyUrl = `${schema}://${host}${portSuffix}${this.serviceConfig.apiPath}?pushkey=${pushkey}`; } else { // Use default hostname with protocol based on secure flag const schema = secure ? 'https' : 'http'; notifyUrl = `${schema}://${this.serviceConfig.defaultHostname}${this.serviceConfig.apiPath}?pushkey=${pushkey}`; } // Prepare request data as URL-encoded form data const requestData = new URLSearchParams({ text: message.title || 'Notification', desp: message.body || '', type: messageType, }); // 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({ 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.serviceConfig.userAgent, })}`); console.log(`[APPTISE_HTTP_REQUEST] Data: ${requestData.toString()}`); console.log(`[APPTISE_HTTP_REQUEST] Timeout: ${this.serviceConfig.timeout}`); // Send HTTP request const response = await HttpClient.post(notifyUrl, requestData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.serviceConfig.userAgent, }, 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, `PushDeer API error: HTTP ${response.status} ${response.statusText}`); } const responseData = response.data; // Check PushDeer API response format if (responseData && typeof responseData === 'object') { // PushDeer typically returns { code: number, content: string, error: string } if (responseData.code !== undefined && responseData.code !== 0) { throw this.createError(ErrorType.SERVER_ERROR, `PushDeer API error: ${responseData.error || responseData.content || 'Unknown error'}`); } } return responseData; }, ErrorType.NETWORK_ERROR); }); return this.createSuccessResult(result, duration); } /** * Validate PushDeer plugin configuration * * @param config - Plugin configuration to validate * @returns True if configuration is valid */ validateConfig(config) { if (!super.validateConfig(config)) { return false; } const { pushkey, apiEndpoint, messageType, host, port, secure } = config.config; // Validate pushkey if (!pushkey || typeof pushkey !== 'string' || pushkey.trim().length === 0) { return false; } // Validate pushkey format (case-insensitive alphanumeric) const pushkeyRegex = new RegExp(this.templateTokens.pushkey.regex[0], this.templateTokens.pushkey.regex[1]); if (!pushkeyRegex.test(pushkey)) { return false; } // Validate API endpoint if (!apiEndpoint || typeof apiEndpoint !== 'string') { return false; } try { new URL(apiEndpoint); } catch { return false; } // Validate message type if (!messageType || typeof messageType !== 'string') { return false; } if (!this.templateArgs.type.enum.includes(messageType)) { return false; } // Validate host (optional) if (host !== null && (typeof host !== 'string' || host.trim().length === 0)) { return false; } // Validate port (optional) if (port !== null && (typeof port !== 'number' || port < this.templateTokens.port.min || port > this.templateTokens.port.max)) { return false; } // Validate secure flag if (typeof secure !== 'boolean') { return false; } return true; } /** * Validate notification message for PushDeer * * @param message - Notification message to validate * @returns True if message is valid */ validateMessage(message) { if (!super.validateMessage(message)) { return false; } // PushDeer requires at least title or body if (!message.title && !message.body) { return false; } // Check message length limits if (message.title && message.title.length > this.serviceConfig.maxTitleLength) { return false; } if (message.body && message.body.length > this.serviceConfig.maxBodyLength) { return false; } return true; } } // Export singleton instance export const pushDeerPlugin = new PushDeerPlugin(); //# sourceMappingURL=pushdeer.js.map