UNPKG

apptise-core

Version:

Core library for Apptise unified notification system

251 lines 9.94 kB
import { NotificationPlugin } from '../base/plugin.js'; import { HttpClient } from '../utils/http-client.js'; import * as crypto from 'crypto'; // Phone number validation regex (matches Python IS_PHONE_NO) const IS_PHONE_NO = /^\+?(?<phone>[0-9\s)(+-]+)\s*$/; export class DingTalkPlugin extends NotificationPlugin { registration = { serviceId: 'dingtalk', protocols: ['dingtalk'], name: 'DingTalk', description: 'DingTalk robot notification service', version: '1.0.0', }; serviceConfig = { name: 'DingTalk', url: 'https://www.dingtalk.com/', setupUrl: 'https://github.com/caronc/apprise/wiki/Notify_dingtalk', apiUrl: 'https://oapi.dingtalk.com/robot/send', timeout: 10000, userAgent: 'Apptise/1.0.0 DingTalk Plugin', contentType: 'application/json', }; templates = [ '{schema}://{token}/', '{schema}://{token}/{targets}/', '{schema}://{secret}@{token}/', '{schema}://{secret}@{token}/{targets}/', ]; templateTokens = { token: { name: 'Token', type: 'string', private: true, required: true, regex: /^[a-z0-9]+$/i, }, secret: { name: 'Secret', type: 'string', private: true, regex: /^[a-z0-9]+$/i, }, target_phone_no: { name: 'Target Phone No', type: 'string', mapTo: 'targets', }, targets: { name: 'Targets', type: 'list:string', }, }; templateArgs = { to: { aliasOf: 'targets', }, token: { aliasOf: 'token', }, secret: { aliasOf: 'secret', }, }; validatePhoneNumber(phone) { const match = phone.match(IS_PHONE_NO); if (!match || !match.groups?.phone) { return null; } // Extract digits only const digits = match.groups.phone.replace(/[^0-9]/g, ''); // Check digit count (11-14 digits as per Python version) if (digits.length < 11 || digits.length > 14) { return null; } return digits; } getSignature(secret) { const timestamp = Math.round(Date.now()).toString(); const stringToSign = `${timestamp}\n${secret}`; const hmac = crypto.createHmac('sha256', secret); hmac.update(stringToSign, 'utf8'); const signature = encodeURIComponent(Buffer.from(hmac.digest()).toString('base64')); return { timestamp, signature }; } parseUrl(url) { try { const parsedUrl = new URL(url); // Validate protocol const protocol = parsedUrl.protocol.slice(0, -1); if (!this.registration.protocols.includes(protocol)) { throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`); } // Extract token from hostname const token = parsedUrl.hostname; if (!token) { throw new Error('DingTalk token is required'); } // Validate token format if (!this.templateTokens.token.regex?.test(token)) { throw new Error(`Invalid DingTalk API Token (${token}) was specified.`); } // Extract secret from username if provided let secret; if (parsedUrl.username) { secret = parsedUrl.username; if (!this.templateTokens.secret.regex?.test(secret)) { throw new Error(`Invalid DingTalk Secret (${secret}) was specified.`); } } // Extract targets from path const pathSegments = parsedUrl.pathname .split('/') .filter(segment => segment.length > 0); const targets = []; for (const segment of pathSegments) { const validPhone = this.validatePhoneNumber(decodeURIComponent(segment)); if (validPhone) { targets.push(validPhone); } else { console.warn(`Dropped invalid phone # (${segment}) specified.`); } } // Parse query parameters const params = new URLSearchParams(parsedUrl.search); // Support token parameter const tokenParam = params.get('token'); if (tokenParam && tokenParam.length > 0) { // Query parameter token overrides hostname token if (!this.templateTokens.token.regex?.test(tokenParam)) { throw new Error(`Invalid DingTalk API Token (${tokenParam}) was specified.`); } } // Support secret parameter const secretParam = params.get('secret'); if (secretParam && secretParam.length > 0) { secret = secretParam; if (!this.templateTokens.secret.regex?.test(secret)) { throw new Error(`Invalid DingTalk Secret (${secret}) was specified.`); } } // Support 'to' parameter for additional targets const toParam = params.get('to'); if (toParam && toParam.length > 0) { const additionalTargets = toParam.split(',').map(t => t.trim()); for (const target of additionalTargets) { const validPhone = this.validatePhoneNumber(target); if (validPhone) { targets.push(validPhone); } else { console.warn(`Dropped invalid phone # (${target}) specified.`); } } } return { serviceId: this.registration.serviceId, url, config: { token: tokenParam || token, secret, targets, }, }; } catch (error) { throw new Error(`Failed to parse DingTalk URL: ${error instanceof Error ? error.message : String(error)}`); } } async send(config, message) { const dingTalkConfig = config.config; const { token, secret, targets } = dingTalkConfig; try { // Build request URL const requestUrl = `${this.serviceConfig.apiUrl}?access_token=${token}`; // Prepare payload const payload = { msgtype: 'text', at: { atMobiles: targets || [], isAtAll: false, }, }; // Use text format to match Python implementation let content = message.body; if (message.title && message.title.trim().length > 0) { content = `${message.title}\r\n${message.body}`; } payload.text = { content: content, }; // Prepare headers const headers = { 'User-Agent': this.serviceConfig.userAgent, 'Content-Type': this.serviceConfig.contentType, }; // Add signature parameters if secret is provided const urlParams = new URLSearchParams(); if (secret) { const { timestamp, signature } = this.getSignature(secret); urlParams.set('timestamp', timestamp); urlParams.set('sign', signature); } const finalUrl = urlParams.toString() ? `${requestUrl}&${urlParams.toString()}` : requestUrl; const requestData = JSON.stringify(payload); const method = 'POST'; // Log HTTP request information (required for equivalence testing) console.log(`[APPTISE_HTTP_REQUEST] Method: ${method}`); console.log(`[APPTISE_HTTP_REQUEST] URL: ${finalUrl}`); console.log(`[APPTISE_HTTP_REQUEST] Headers: ${JSON.stringify(headers)}`); console.log(`[APPTISE_HTTP_REQUEST] Data: ${requestData}`); console.log(`[APPTISE_HTTP_REQUEST] Timeout: ${this.serviceConfig.timeout}`); // Send request const response = await HttpClient.post(finalUrl, requestData, { headers, timeout: this.serviceConfig.timeout, }); // Log HTTP response information (required 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.status === 200) { return { success: true, serviceId: this.registration.serviceId, }; } else { return { success: false, serviceId: this.registration.serviceId, error: `HTTP ${response.status}: ${response.statusText}`, }; } } catch (error) { // Log error information console.log(`[APPTISE_HTTP_ERROR] ${error instanceof Error ? error.message : String(error)}`); return { success: false, serviceId: this.registration.serviceId, error: `Failed to send DingTalk notification: ${error instanceof Error ? error.message : String(error)}`, }; } } } export const dingTalkPlugin = new DingTalkPlugin(); export default dingTalkPlugin; //# sourceMappingURL=dingtalk.js.map