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