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