UNPKG

apptise-core

Version:

Core library for Apptise unified notification system

198 lines 9.21 kB
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