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