apptise-core
Version:
Core library for Apptise unified notification system
198 lines • 9.21 kB
JavaScript
import { NotificationPlugin } from '../base/plugin.js';
import { HttpClient } from '../utils/http-client.js';
import { ErrorType } from '../base/types.js';
/**
* ServerChan 通知插件
* 支持 Server酱 推送服务
*/
export class ServerChanPlugin extends NotificationPlugin {
registration = {
serviceId: 'serverchan',
protocols: ['serverchan', 'sc', 'schan'],
name: 'ServerChan',
description: 'Server酱推送服务插件',
version: '1.0.0',
};
// Service configuration constants
serviceConfig = {
// Default API domains
defaultDomain: 'sctapi.ftqq.com',
pushDomain: 'push.ft07.com',
// API endpoints
defaultApiPath: '.send',
pushApiPath: '/send',
// Request configuration
timeout: 30000, // 30 seconds
// Content type headers
jsonContentType: 'application/json;charset=utf-8',
formContentType: 'application/x-www-form-urlencoded',
};
/**
* 解析 ServerChan URL
* 支持格式:
* - serverchan://sendkey
* - sc://sendkey
* - schan://sendkey
* - serverchan://sendkey@custom.domain.com
*/
parseUrl(url) {
const parsedUrl = this.parseUrlBase(url);
if (!this.registration.protocols.includes(parsedUrl.protocol)) {
throw this.createError(ErrorType.INVALID_URL, `Unsupported protocol: ${parsedUrl.protocol}. Expected: ${this.registration.protocols.join(', ')}`);
}
// 从路径中提取 sendKey
const sendKey = parsedUrl.pathname?.replace(/^\//, '') ?? parsedUrl.hostname;
if (!sendKey) {
throw this.createError(ErrorType.INVALID_URL, 'SendKey is required in ServerChan URL');
}
// 检查是否有自定义域名
const customDomain = parsedUrl.searchParams.get('domain');
const serverChanConfig = {
sendKey,
};
if (customDomain) {
serverChanConfig.domain = customDomain;
}
return {
serviceId: this.registration.serviceId,
url,
config: serverChanConfig,
};
}
/**
* 发送通知到 ServerChan
*/
async send(config, message) {
const { result, duration } = await this.measureTime(async () => {
return this.safeExecute(async () => {
// 验证配置
if (!this.validateConfig(config)) {
throw this.createError(ErrorType.VALIDATION_ERROR, 'Invalid plugin configuration');
}
// 验证消息
if (!this.validateMessage(message)) {
throw this.createError(ErrorType.VALIDATION_ERROR, 'Invalid notification message');
}
const serverChanConfig = config.config;
const { sendKey, domain } = serverChanConfig;
// 构建请求 URL
let requestUrl;
if (domain) {
// 使用自定义域名
requestUrl = `https://${domain}${this.serviceConfig.pushApiPath}/${sendKey}${this.serviceConfig.defaultApiPath}`;
}
else if (String(sendKey).startsWith('sctp')) {
// 支持 sctp 类型的 sendkey,使用 push.ft07.com 域名
const match = sendKey.match(/^sctp(\d+)t/);
if (match) {
const serverId = match[1];
requestUrl = `https://${serverId}.${this.serviceConfig.pushDomain}${this.serviceConfig.pushApiPath}/${sendKey}${this.serviceConfig.defaultApiPath}`;
}
else {
// 如果匹配失败,使用默认的 push.ft07.com
requestUrl = `https://${this.serviceConfig.pushDomain}${this.serviceConfig.pushApiPath}/${sendKey}${this.serviceConfig.defaultApiPath}`;
}
}
else {
// 默认使用 sctapi.ftqq.com
requestUrl = `https://${this.serviceConfig.defaultDomain}/${sendKey}${this.serviceConfig.defaultApiPath}`;
}
// 构建请求数据
const requestData = {
title: message.title,
};
if (message.body) {
requestData.desp = message.body;
}
// 添加标签(如果有)
if (message.metadata?.tags) {
const tags = Array.isArray(message.metadata.tags)
? message.metadata.tags.join('|')
: String(message.metadata.tags);
requestData.tags = tags;
}
// 根据不同的 API 类型选择不同的请求格式
let response;
if (String(sendKey).startsWith('sctp') || requestUrl.includes('push.ft07.com')) {
// Log HTTP request details for equivalence testing
console.log(`[APPTISE_HTTP_REQUEST] Method: POST`);
console.log(`[APPTISE_HTTP_REQUEST] URL: ${requestUrl}`);
console.log(`[APPTISE_HTTP_REQUEST] Headers: ${JSON.stringify({
'Content-Type': this.serviceConfig.jsonContentType,
})}`);
console.log(`[APPTISE_HTTP_REQUEST] Data: ${JSON.stringify(requestData)}`);
console.log(`[APPTISE_HTTP_REQUEST] Timeout: ${this.serviceConfig.timeout}`);
// 对于 sctp 类型,使用 JSON 格式
response = await HttpClient.post(requestUrl, JSON.stringify(requestData), {
headers: {
'Content-Type': this.serviceConfig.jsonContentType,
},
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)}`);
}
else {
// 对于传统 API,使用 form 格式
const formData = new URLSearchParams();
Object.entries(requestData).forEach(([key, value]) => {
formData.append(key, value);
});
// Log HTTP request details for equivalence testing
console.log(`[APPTISE_HTTP_REQUEST] Method: POST`);
console.log(`[APPTISE_HTTP_REQUEST] URL: ${requestUrl}`);
console.log(`[APPTISE_HTTP_REQUEST] Headers: ${JSON.stringify({
'Content-Type': this.serviceConfig.formContentType,
})}`);
console.log(`[APPTISE_HTTP_REQUEST] Data: ${formData.toString()}`);
console.log(`[APPTISE_HTTP_REQUEST] Timeout: ${this.serviceConfig.timeout}`);
response = await HttpClient.post(requestUrl, formData, {
headers: {
'Content-Type': this.serviceConfig.formContentType,
},
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)}`);
}
// 检查响应
if (!response.ok) {
throw this.createError(ErrorType.NETWORK_ERROR, `HTTP ${response.status}: ${response.statusText}`);
}
const { data } = response;
// ServerChan API 返回格式检查
if (data.code !== 0) {
throw this.createError(ErrorType.SERVER_ERROR, data.message ?? 'ServerChan API error', undefined, { code: data.code, response: data });
}
return data;
}, ErrorType.NETWORK_ERROR);
});
return this.createSuccessResult(result, duration);
}
/**
* 验证 ServerChan 特定配置
*/
validateConfig(config) {
if (!super.validateConfig(config)) {
return false;
}
const serverChanConfig = config.config;
return (typeof serverChanConfig.sendKey === 'string' &&
serverChanConfig.sendKey.length > 0);
}
}
/**
* 创建 ServerChan 插件实例
*/
export function createServerChanPlugin() {
return new ServerChanPlugin();
}
/**
* 默认 ServerChan 插件实例
*/
export const serverChanPlugin = new ServerChanPlugin();
//# sourceMappingURL=serverchan.js.map