push-all-in-one
Version:
Push All In One!支持 Server酱(以及 Server 酱³)、自定义邮件、钉钉机器人、企业微信机器人、企业微信应用、pushplus、iGot 、Qmsg、息知、PushDeer、Discord、OneBot、Telegram 等多种推送方式
1 lines • 148 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","names":["colors: any","Debugger","customEmailConfigSchema: CustomEmailConfigSchema","customEmailOptionSchema: CustomEmailOptionSchema","Debugger","HttpsProxyAgent","SocksProxyAgent","error","crypto","Debugger","dingtalkConfigSchema: DingtalkConfigSchema","dingtalkOptionSchema: DingtalkOptionSchema","Debugger","discordConfigSchema: DiscordConfigSchema","discordOptionSchema: DiscordOptionSchema","Debugger","feishuConfigSchema: FeishuConfigSchema","feishuOptionSchema: FeishuOptionSchema","Debugger","iGotConfigSchema: IGotConfigSchema","iGotOptionSchema: IGotOptionSchema","Debugger","ntfyConfigSchema: NtfyConfigSchema","ntfyOptionSchema: NtfyOptionSchema","headers: any","Debugger","oneBotConfigSchema: OneBotConfigSchema","oneBotOptionSchema: OneBotOptionSchema","Debugger","pushDeerConfigSchema: PushDeerConfigSchema","pushDeerOptionSchema: PushDeerOptionSchema","Debugger","pushPlusConfigSchema: PushPlusConfigSchema","pushPlusOptionSchema: PushPlusOptionSchema","Debugger","qmsgConfigSchema: QmsgConfigSchema","qmsgOptionSchema: QmsgOptionSchema","Debugger","serverChanTurboConfigSchema: ServerChanTurboConfigSchema","serverChanTurboOptionSchema: ServerChanTurboOptionSchema","Debugger","serverChanV3ConfigSchema: ServerChanV3ConfigSchema","serverChanV3OptionSchema: ServerChanV3OptionSchema","Debugger","telegramConfigSchema: TelegramConfigSchema","telegramOptionSchema: TelegramOptionSchema","Debugger","wechatAppConfigSchema: WechatAppConfigSchema","wechatAppOptionSchema: WechatAppOptionSchema","Debugger","wechatRobotConfigSchema: WechatRobotConfigSchema","wechatRobotOptionSchema: WechatRobotOptionSchema","Debugger","xiZhiConfigSchema: XiZhiConfigSchema","xiZhiOptionSchema: XiZhiOptionSchema","wxPusherConfigSchema: WxPusherConfigSchema","wxPusherOptionSchema: WxPusherOptionSchema"],"sources":["../src/utils/helper.ts","../src/utils/validate.ts","../src/push/custom-email.ts","../src/utils/ajax.ts","../src/utils/crypto.ts","../src/push/dingtalk.ts","../src/push/discord.ts","../src/push/feishu.ts","../src/push/i-got.ts","../src/push/ntfy.ts","../src/push/one-bot.ts","../src/push/push-deer.ts","../src/push/push-plus.ts","../src/push/qmsg.ts","../src/push/server-chan-turbo.ts","../src/push/server-chan-v3.ts","../src/push/telegram.ts","../src/push/wechat-app.ts","../src/push/wechat-robot.ts","../src/push/xi-zhi.ts","../src/push/wx-pusher.ts","../src/one.ts"],"sourcesContent":["let colors: any\n\nif (globalThis.process && typeof globalThis.process.on === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n colors = require('@colors/colors')\n } catch {\n import('@colors/colors').then((value) => {\n colors = value.default\n }).catch(console.error)\n }\n}\n\nexport function warn(text: any): void {\n if (colors) {\n text = colors.yellow(text)\n }\n console.warn(text)\n}\n\nexport function error(text: any): void {\n if (colors) {\n text = colors.red(text)\n }\n console.error(text)\n}\n\nexport const logger = {\n warn,\n error,\n}\n\n/**\n * 检测是否为 http/https 开头的 url\n * @param url\n * @returns\n */\nexport const isHttpURL = (url: string): boolean => /^(https?:\\/\\/)/.test(url)\n\n/**\n * 检测是否为 socks/socks5 开头的 url\n * @param url\n * @returns\n */\nexport const isSocksUrl = (url: string): boolean => /^(socks5?:\\/\\/)/.test(url)\n\n/**\n * 判断是否为 null 或 undefined\n * @param value\n * @returns\n */\nexport function isNil(value: unknown): boolean {\n return value === null || value === undefined\n}\n\n/**\n * 判断是否不为 null 且不为 undefined\n * @param value\n * @returns\n */\nexport function isNotNil(value: unknown): boolean {\n return !isNil(value)\n}\n\n/**\n * 判断是否为 null 或 undefined 或 空字符串\n * @param value\n * @returns\n */\nexport function isEmpty(value: unknown): boolean {\n return value === null || value === undefined || value === ''\n}\n/**\n * 判断是否不为 null 且不为 undefined 且不为 空字符串\n * @param value\n * @returns\n */\nexport function isNotEmpty(value: unknown): boolean {\n return !isEmpty(value)\n}\n\n/**\n * 数组去重\n *\n * @author CaoMeiYouRen\n * @date 2025-03-05\n * @export\n * @template T\n * @param arr\n */\nexport function uniq<T>(arr: T[]): T[] {\n return Array.from(new Set(arr))\n}\n","import { isEmpty } from './helper'\nimport { Config, ConfigSchema } from '@/interfaces/schema'\n\n/**\n * 验证配置是否符合 Schema 规则,如果不符合则抛出错误\n *\n * @author CaoMeiYouRen\n * @date 2024-11-17\n * @export\n * @template T\n * @param config\n * @param schema\n */\nexport function validate<T = Config>(config: T, schema: ConfigSchema<T>): void {\n Object.keys(schema).forEach((key) => {\n const item = schema[key]\n const value = config[key]\n if (!item.required && isEmpty(value)) {\n return\n }\n if (item.required && isEmpty(value)) {\n throw new Error(`\"${key}\" 字段是必须的!`)\n }\n if (item.type === 'select') {\n const { options } = item as any\n if (!options.map((e) => e.value).includes(value)) {\n throw new Error(`\"${key}\" 字段必须是以下选项之一:${options.map((e) => e.value).join(',')}`)\n }\n return\n }\n if (item.type === 'string') {\n if (typeof value !== 'string') {\n throw new Error(`\"${key}\" 字段必须是字符串!`)\n }\n return\n }\n if (item.type === 'number') {\n if (typeof value !== 'number') {\n throw new Error(`\"${key}\" 字段必须是数字!`)\n }\n return\n }\n if (item.type === 'boolean') {\n if (typeof value !== 'boolean') {\n throw new Error(`\"${key}\" 字段必须是布尔值!`)\n }\n return\n }\n if (item.type === 'array') {\n if (!Array.isArray(value)) {\n throw new Error(`\"${key}\" 字段必须是数组!`)\n }\n return\n }\n if (item.type === 'object') {\n if (typeof value !== 'object') {\n throw new Error(`\"${key}\" 字段必须是对象!`)\n }\n return\n }\n throw new Error(`\"${key}\" 字段类型不支持!`)\n })\n}\n","import debug from 'debug'\nimport nodemailer from 'nodemailer'\nimport SMTPTransport from 'nodemailer/lib/smtp-transport'\nimport Mail from 'nodemailer/lib/mailer'\nimport { Send } from '@/interfaces/send'\nimport { SendResponse } from '@/interfaces/response'\nimport { ConfigSchema, OptionSchema } from '@/interfaces/schema'\nimport { validate } from '@/utils/validate'\n\nconst Debugger = debug('push:custom-email')\n\nexport type CustomEmailType = 'text' | 'html'\nexport interface CustomEmailConfig {\n /**\n * 邮件类型\n */\n EMAIL_TYPE: CustomEmailType\n /**\n * 收件邮箱\n */\n EMAIL_TO_ADDRESS: string\n /**\n * 发件邮箱\n */\n EMAIL_AUTH_USER: string\n /**\n * 发件授权码(或密码)\n */\n EMAIL_AUTH_PASS: string\n /**\n * 发件域名\n */\n EMAIL_HOST: string\n /**\n * 发件端口\n */\n EMAIL_PORT: number\n}\n\nexport type CustomEmailConfigSchema = ConfigSchema<CustomEmailConfig>\n\nexport const customEmailConfigSchema: CustomEmailConfigSchema = {\n EMAIL_TYPE: {\n type: 'select',\n title: '邮件类型',\n description: '邮件类型',\n required: true,\n default: 'text',\n options: [\n {\n label: '文本',\n value: 'text',\n },\n {\n label: 'HTML',\n value: 'html',\n },\n ],\n },\n EMAIL_TO_ADDRESS: {\n type: 'string',\n title: '收件邮箱',\n description: '收件邮箱',\n required: true,\n default: '',\n },\n EMAIL_AUTH_USER: {\n type: 'string',\n title: '发件邮箱',\n description: '发件邮箱',\n required: true,\n default: '',\n },\n EMAIL_AUTH_PASS: {\n type: 'string',\n title: '发件授权码(或密码)',\n description: '发件授权码(或密码)',\n required: true,\n default: '',\n },\n EMAIL_HOST: {\n type: 'string',\n title: '发件域名',\n description: '发件域名',\n required: true,\n default: '',\n },\n EMAIL_PORT: {\n type: 'number',\n title: '发件端口',\n description: '发件端口',\n required: true,\n default: 465,\n },\n} as const\n\nexport type CustomEmailOption = Mail.Options\n\ntype OptionalCustomEmailOption = Pick<CustomEmailOption, 'to' | 'from' | 'subject' | 'text' | 'html'>\n\n/**\n * 由于 CustomEmailOption 的配置太多,所以不提供完整的 Schema,只提供部分配置 schema。\n * 如需使用完整的配置,请查看官方文档\n */\nexport type CustomEmailOptionSchema = OptionSchema<{\n [K in keyof OptionalCustomEmailOption]: string\n}>\n\nexport const customEmailOptionSchema: CustomEmailOptionSchema = {\n to: {\n type: 'string',\n title: '收件邮箱',\n description: '收件邮箱',\n required: false,\n default: '',\n },\n from: {\n type: 'string',\n title: '发件邮箱',\n description: '发件邮箱',\n required: false,\n default: '',\n },\n subject: {\n type: 'string',\n title: '邮件主题',\n description: '邮件主题',\n required: false,\n default: '',\n },\n text: {\n type: 'string',\n title: '邮件内容',\n description: '邮件内容',\n required: false,\n default: '',\n },\n html: {\n type: 'string',\n title: '邮件内容',\n description: '邮件内容',\n required: false,\n default: '',\n },\n} as const\n\n/**\n * 自定义邮件。官方文档: https://github.com/nodemailer/nodemailer\n *\n * @author CaoMeiYouRen\n * @date 2023-03-12\n * @export\n * @class CustomEmail\n */\nexport class CustomEmail implements Send {\n\n // 命名空间\n static readonly namespace = '自定义邮件'\n\n static readonly configSchema = customEmailConfigSchema\n\n static readonly optionSchema = customEmailOptionSchema\n\n private config: CustomEmailConfig\n\n private transporter: nodemailer.Transporter<SMTPTransport.SentMessageInfo, SMTPTransport.Options>\n\n constructor(config: CustomEmailConfig) {\n this.config = config\n Debugger('CustomEmailConfig: %o', config)\n // 根据 configSchema 验证 config\n validate(config, CustomEmail.configSchema)\n const { EMAIL_AUTH_USER, EMAIL_AUTH_PASS, EMAIL_HOST, EMAIL_PORT } = this.config\n this.transporter = nodemailer.createTransport({\n host: EMAIL_HOST,\n port: Number(EMAIL_PORT),\n auth: {\n user: EMAIL_AUTH_USER,\n pass: EMAIL_AUTH_PASS,\n },\n })\n }\n\n /**\n * 释放资源(需要支持 Symbol.dispose)\n *\n * @author CaoMeiYouRen\n * @date 2024-11-08\n */\n [Symbol.dispose](): void {\n if (this.transporter) {\n this.transporter.close()\n }\n }\n\n /**\n *\n * @author CaoMeiYouRen\n * @date 2024-11-08\n * @param title 消息的标题\n * @param [desp] 消息的内容,支持 html\n * @param [option] 额外选项\n */\n async send(title: string, desp?: string, option?: CustomEmailOption): Promise<SendResponse<SMTPTransport.SentMessageInfo>> {\n Debugger('title: \"%s\", desp: \"%s\", option: %o', title, desp, option)\n const { EMAIL_TYPE, EMAIL_TO_ADDRESS, EMAIL_AUTH_USER } = this.config\n if (!await this.transporter.verify()) {\n throw new Error('自定义邮件的发件配置无效')\n }\n const { to: _to, ...args } = option || {}\n const from = EMAIL_AUTH_USER\n const to = _to || EMAIL_TO_ADDRESS\n const type = EMAIL_TYPE\n const response = await this.transporter.sendMail({\n from,\n to,\n subject: title,\n [type]: desp,\n ...args,\n })\n if (typeof Symbol.dispose === 'undefined') { // 如果不支持 Symbol.dispose ,则手动释放\n this.transporter.close()\n }\n Debugger('CustomEmail Response: %o', response)\n if (response.response?.includes('250 OK')) {\n return {\n status: 200,\n statusText: 'OK',\n data: response,\n headers: {},\n }\n }\n return {\n status: 500,\n statusText: 'Internal Server Error',\n data: response,\n headers: {},\n }\n }\n\n}\n","import axios, { AxiosResponse, Method, AxiosRequestHeaders } from 'axios'\nimport debug from 'debug'\nimport { HttpsProxyAgent } from 'https-proxy-agent'\nimport { SocksProxyAgent } from 'socks-proxy-agent'\nimport { isHttpURL, isSocksUrl, logger } from './helper'\n\nconst Debugger = debug('push:ajax')\n\ninterface AjaxConfig {\n url: string\n query?: Record<string, unknown>\n data?: Record<string, unknown> | string | Buffer | ArrayBuffer\n method?: Method\n headers?: Record<string, unknown>\n baseURL?: string\n proxyUrl?: string\n}\n\n/**\n * axios 接口封装\n *\n * @author CaoMeiYouRen\n * @date 2021-02-27\n * @export\n * @param config\n * @returns\n */\nexport async function ajax<T = any>(config: AjaxConfig): Promise<AxiosResponse<T>> {\n try {\n Debugger('ajax config: %O', config)\n const { url, query = {}, method = 'GET', baseURL = '', proxyUrl } = config\n const headers = (config.headers || {}) as AxiosRequestHeaders\n let { data = {} } = config\n\n if (headers['Content-Type'] === 'application/x-www-form-urlencoded' && typeof data === 'object') {\n data = new URLSearchParams(data as Record<string, string>).toString()\n }\n\n let httpAgent = null\n Debugger('NO_PROXY: %s', process.env.NO_PROXY)\n if (process.env.NO_PROXY !== 'true') {\n Debugger('HTTP_PROXY: %s', process.env.HTTP_PROXY)\n Debugger('HTTPS_PROXY: %s', process.env.HTTPS_PROXY)\n Debugger('SOCKS_PROXY: %s', process.env.SOCKS_PROXY)\n if (isHttpURL(proxyUrl)) {\n httpAgent = new HttpsProxyAgent(proxyUrl)\n } else if (isSocksUrl(proxyUrl)) {\n httpAgent = new SocksProxyAgent(proxyUrl)\n } else if (process.env.HTTPS_PROXY) {\n httpAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY)\n } else if (process.env.HTTP_PROXY) {\n httpAgent = new HttpsProxyAgent(process.env.HTTP_PROXY)\n } else if (process.env.SOCKS_PROXY) {\n httpAgent = new SocksProxyAgent(process.env.SOCKS_PROXY)\n }\n }\n const response = await axios(url, {\n baseURL,\n method,\n headers,\n params: query,\n data,\n timeout: 60000,\n httpAgent,\n httpsAgent: httpAgent,\n proxy: false,\n })\n Debugger('response data: %O', response.data)\n return response\n } catch (error) {\n if (error?.response) {\n logger.error(error.response)\n return error.response\n }\n throw error\n }\n}\n","import crypto from 'crypto'\n\n/**\n * 生成钉钉签名\n *\n * @author CaoMeiYouRen\n * @date 2024-10-30\n * @export\n * @param timestamp\n * @param suiteTicket\n * @param suiteSecret\n */\nexport function generateSignature(timestamp: string | number, suiteTicket: string, suiteSecret: crypto.BinaryLike | crypto.KeyObject): string {\n // 创建要签名的字符串\n const stringToSign = `${timestamp}\\n${suiteTicket}`\n\n // 创建 HMAC 实例\n const hmac = crypto.createHmac('sha256', suiteSecret)\n\n // 更新 HMAC 实例的数据\n hmac.update(stringToSign, 'utf8')\n\n // 计算 HMAC 签名并进行 Base64 编码\n const signature = hmac.digest('base64')\n\n return signature\n}\n\nexport function base64Encode(str: string): string {\n return Buffer.from(str).toString('base64')\n}\n\nexport function rfc2047Encode(str: string): string {\n return `=?utf-8?B?${base64Encode(str)}?=`\n}\n","import { AxiosResponse } from 'axios'\nimport debug from 'debug'\nimport { Markdown } from './dingtalk/markdown'\nimport { Text } from './dingtalk/text'\nimport { Link } from './dingtalk/link'\nimport { FeedCard } from './dingtalk/feed-card'\nimport { ActionCard, IndependentJump, OverallJump } from './dingtalk/action-card'\nimport { Send } from '@/interfaces/send'\nimport { warn } from '@/utils/helper'\nimport { ajax } from '@/utils/ajax'\nimport { generateSignature } from '@/utils/crypto'\nimport { SendResponse } from '@/interfaces/response'\nimport { ConfigSchema, OptionSchema } from '@/interfaces/schema'\nimport { validate } from '@/utils/validate'\n\nconst Debugger = debug('push:dingtalk')\n\nexport type DingtalkMsgType = 'text' | 'markdown' | 'link' | 'actionCard' | 'feedCard'\n\nexport interface DingtalkConfig {\n /**\n * 钉钉机器人 access_token。官方文档:https://developers.dingtalk.com/document/app/custom-robot-access\n */\n DINGTALK_ACCESS_TOKEN: string\n /**\n * 加签安全秘钥(HmacSHA256)\n */\n DINGTALK_SECRET?: string\n}\n\nexport type DingtalkConfigSchema = ConfigSchema<DingtalkConfig>\n\nexport const dingtalkConfigSchema: DingtalkConfigSchema = {\n DINGTALK_ACCESS_TOKEN: {\n type: 'string',\n title: '钉钉机器人 access_token',\n description: '钉钉机器人 access_token',\n required: true,\n default: '',\n },\n DINGTALK_SECRET: {\n type: 'string',\n title: '加签安全秘钥(HmacSHA256)',\n required: false,\n default: '',\n },\n} as const\n\nexport type DingtalkOption = Partial<(Text | Markdown | Link | FeedCard | ActionCard)>\n\ntype TempDingtalkOption = {\n msgtype?: DingtalkOption['msgtype']\n text?: Partial<Text['text']>\n markdown?: Partial<Markdown['markdown']>\n link?: Partial<Link['link']>\n actionCard?: Partial<{\n // 首屏会话透出的展示内容\n title: string\n // markdown 格式的消息内容\n text: string\n // 0:按钮竖直排列;1:按钮横向排列\n btnOrientation?: '0' | '1'\n }> & Partial<OverallJump> & Partial<IndependentJump>\n feedCard?: Partial<FeedCard['feedCard']>\n\n at?: Text['at']\n [key: string]: any\n}\n\nexport type DingtalkOptionSchema = OptionSchema<TempDingtalkOption>\n\nexport const dingtalkOptionSchema: DingtalkOptionSchema = {\n msgtype: {\n type: 'select',\n title: '消息类型',\n description: '消息类型',\n required: false,\n default: 'text',\n options: [\n {\n label: '文本',\n value: 'text',\n },\n {\n label: 'Markdown',\n value: 'markdown',\n },\n {\n label: '链接',\n value: 'link',\n },\n {\n label: '按钮',\n value: 'actionCard',\n },\n {\n label: 'FeedCard',\n value: 'feedCard',\n },\n ],\n },\n text: {\n type: 'object',\n title: '文本',\n description: '文本',\n required: false,\n default: {},\n },\n markdown: {\n type: 'object',\n title: 'Markdown',\n description: 'Markdown',\n required: false,\n default: {},\n },\n link: {\n type: 'object',\n title: '链接',\n description: '链接',\n required: false,\n default: {},\n },\n actionCard: {\n type: 'object',\n title: '动作卡片',\n description: '动作卡片',\n required: false,\n default: {},\n },\n feedCard: {\n type: 'object',\n title: '订阅卡片',\n description: '订阅卡片',\n required: false,\n default: {},\n },\n} as const\n\nexport interface DingtalkResponse {\n errcode: number\n errmsg: string\n}\n\n/**\n * 钉钉机器人推送\n * 在 [dingtalk-robot-sdk](https://github.com/ineo6/dingtalk-robot-sdk) 的基础上重构了一下,用法几乎完全一致。\n * @author CaoMeiYouRen\n * @date 2021-02-27\n * @export\n * @class Dingtalk\n */\nexport class Dingtalk implements Send {\n\n static readonly namespace = '钉钉'\n\n static readonly configSchema = dingtalkConfigSchema\n\n static readonly optionSchema = dingtalkOptionSchema\n\n private ACCESS_TOKEN: string\n /**\n * 加签安全秘钥(HmacSHA256)\n *\n * @private\n */\n private SECRET?: string\n private webhook: string = 'https://oapi.dingtalk.com/robot/send'\n\n /**\n * 参考文档 [钉钉开放平台 - 自定义机器人接入](https://developers.dingtalk.com/document/app/custom-robot-access)\n * @author CaoMeiYouRen\n * @date 2024-11-08\n * @param config\n */\n constructor(config: DingtalkConfig) {\n const { DINGTALK_ACCESS_TOKEN, DINGTALK_SECRET } = config\n this.ACCESS_TOKEN = DINGTALK_ACCESS_TOKEN\n this.SECRET = DINGTALK_SECRET\n Debugger('DINGTALK_ACCESS_TOKEN: %s , DINGTALK_SECRET: %s', this.ACCESS_TOKEN, this.SECRET)\n // 根据 configSchema 验证 config\n validate(config, Dingtalk.configSchema)\n if (!this.SECRET) {\n warn('未提供 DINGTALK_SECRET !')\n }\n }\n\n private getSign(timeStamp: number): string {\n let signStr = ''\n if (this.SECRET) {\n signStr = generateSignature(timeStamp, this.SECRET, this.SECRET)\n Debugger('Sign string is %s, result is %s', `${timeStamp}\\n${this.SECRET}`, signStr)\n }\n return signStr\n }\n\n private async push(data: DingtalkOption): Promise<AxiosResponse<DingtalkResponse>> {\n const timestamp = Date.now()\n const sign = this.getSign(timestamp)\n const result = await ajax({\n url: this.webhook,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n query: {\n timestamp,\n sign,\n access_token: this.ACCESS_TOKEN,\n },\n data,\n })\n Debugger('Result is %s, %s。', result.data.errcode, result.data.errmsg)\n if (result.data.errcode === 310000) {\n console.error('Send Failed:', result.data)\n Debugger('Please check safe config : %O', result.data)\n }\n return result\n }\n\n /**\n *\n *\n * @author CaoMeiYouRen\n * @date 2024-11-08\n * @param title 消息的标题\n * @param [desp] 消息的内容,支持 Markdown\n * @returns\n */\n async send(title: string, desp?: string, option?: DingtalkOption): Promise<SendResponse<DingtalkResponse>> {\n Debugger('title: \"%s\", desp: \"%s\", option: %O', title, desp, option)\n switch (option.msgtype) {\n case 'text':\n return this.push({\n msgtype: 'text',\n text: {\n content: `${title}${desp ? `\\n${desp}` : ''}`,\n },\n ...option,\n })\n case 'markdown':\n return this.push({\n msgtype: 'markdown',\n markdown: {\n title,\n text: `# ${title}${desp ? `\\n\\n${desp}` : ''}`,\n },\n ...option,\n })\n case 'link':\n return this.push({\n msgtype: 'link',\n link: {\n title,\n text: desp || '',\n picUrl: option?.link?.picUrl || '',\n messageUrl: option.link?.messageUrl || '',\n },\n ...option,\n })\n case 'actionCard':\n return this.push({\n msgtype: 'actionCard',\n actionCard: {\n title,\n text: desp || '',\n btnOrientation: option?.actionCard?.btnOrientation || '0',\n btns: (option?.actionCard as any)?.btns,\n singleTitle: (option?.actionCard as any)?.singleTitle,\n singleURL: (option?.actionCard as any)?.singleURL,\n },\n ...option,\n })\n case 'feedCard':\n return this.push({\n msgtype: 'feedCard',\n feedCard: {\n links: option?.feedCard?.links || [],\n },\n ...option,\n })\n default:\n throw new Error('msgtype is required!')\n }\n }\n\n}\n","import debug from 'debug'\nimport { Send } from '@/interfaces/send'\nimport { ajax } from '@/utils/ajax'\nimport { SendResponse } from '@/interfaces/response'\nimport { ConfigSchema, OptionSchema } from '@/interfaces/schema'\nimport { validate } from '@/utils/validate'\n\nconst Debugger = debug('push:discord')\n\nexport interface DiscordConfig {\n /**\n * Webhook Url 可在服务器设置 -> 整合 -> Webhook -> 创建 Webhook 中获取\n */\n DISCORD_WEBHOOK: string\n\n /**\n * 代理地址\n */\n PROXY_URL?: string\n}\n\nexport type DiscordConfigSchema = ConfigSchema<DiscordConfig>\n\nexport const discordConfigSchema: DiscordConfigSchema = {\n DISCORD_WEBHOOK: {\n type: 'string',\n title: 'Webhook Url',\n description: 'Webhook Url 可在服务器设置 -> 整合 -> Webhook -> 创建 Webhook 中获取',\n required: true,\n },\n PROXY_URL: {\n type: 'string',\n title: '代理地址',\n description: '代理地址',\n required: false,\n },\n} as const\n\n/**\n * Discord 额外选项\n * 由于参数过多,因此请参考官方文档进行配置。\n * @link https://discord.com/developers/docs/resources/webhook#execute-webhook\n */\nexport type DiscordOption = {\n /**\n * 机器人显示的名称\n */\n username?: string\n /**\n * 机器人头像的 Url\n */\n avatar_url?: string\n [key: string]: any\n}\n\nexport type DiscordOptionSchema = OptionSchema<DiscordOption>\n\nexport const discordOptionSchema: DiscordOptionSchema = {\n username: {\n type: 'string',\n title: '机器人显示的名称',\n description: '机器人显示的名称',\n required: false,\n },\n avatar_url: {\n type: 'string',\n title: '机器人头像的 Url',\n description: '机器人头像的 Url',\n required: false,\n },\n} as const\n\nexport interface DiscordResponse { }\n\n/**\n * Discord Webhook 推送\n *\n * @author CaoMeiYouRen\n * @date 2023-09-17\n * @export\n * @class Discord\n */\nexport class Discord implements Send {\n\n static readonly namespace = 'Discord'\n static readonly configSchema = discordConfigSchema\n static readonly optionSchema = discordOptionSchema\n\n /**\n * Webhook Url 可在服务器设置 -> 整合 -> Webhook -> 创建 Webhook 中获取\n *\n * @author CaoMeiYouRen\n * @date 2023-09-17\n * @private\n */\n private DISCORD_WEBHOOK: string\n\n proxyUrl: string\n\n /**\n * 创建 Discord 实例\n * @author CaoMeiYouRen\n * @date 2024-11-08\n * @param config 配置\n */\n constructor(config: DiscordConfig) {\n const { DISCORD_WEBHOOK, PROXY_URL } = config\n Debugger('DISCORD_WEBHOOK: %s, PROXY_URL: %s', DISCORD_WEBHOOK, PROXY_URL)\n this.DISCORD_WEBHOOK = DISCORD_WEBHOOK\n if (PROXY_URL) {\n this.proxyUrl = PROXY_URL\n }\n // 根据 configSchema 验证 config\n validate(config, Discord.configSchema)\n }\n\n /**\n * 发送消息\n *\n * @author CaoMeiYouRen\n * @date 2024-11-08\n * @param title 消息的标题\n * @param [desp] 消息的描述。最多 2000 个字符\n * @param [option] 额外选项\n */\n async send(title: string, desp?: string, option?: DiscordOption): Promise<SendResponse<DiscordResponse>> {\n Debugger('title: \"%s\", desp: \"%s\", option: %o', title, desp, option)\n const { username, avatar_url, ...args } = option || {}\n const proxyUrl = this.proxyUrl\n const content = `${title}${desp ? `\\n${desp}` : ''}`\n return ajax({\n url: this.DISCORD_WEBHOOK,\n method: 'POST',\n proxyUrl,\n data: {\n username,\n content,\n avatar_url,\n ...args,\n },\n })\n }\n\n}\n","import debug from 'debug'\nimport { Send } from '@/interfaces/send'\nimport { ajax } from '@/utils/ajax'\nimport { SendResponse } from '@/interfaces/response'\nimport { ConfigSchema, OptionSchema } from '@/interfaces/schema'\nimport { validate } from '@/utils/validate'\n\nconst Debugger = debug('push:feishu')\n\nexport interface FeishuConfig {\n /**\n * 飞书应用 ID。官方文档:https://open.feishu.cn/document/server-docs/api-call-guide/terminology#b047be0c\n */\n FEISHU_APP_ID: string\n /**\n * 飞书应用密钥。官方文档:https://open.feishu.cn/document/server-docs/api-call-guide/terminology#1b5fb6cd\n */\n FEISHU_APP_SECRET: string\n}\n\nexport type FeishuConfigSchema = ConfigSchema<FeishuConfig>\n\nexport const feishuConfigSchema: FeishuConfigSchema = {\n FEISHU_APP_ID: {\n type: 'string',\n title: '飞书应用 ID',\n description: '飞书应用 ID',\n required: true,\n default: '',\n },\n FEISHU_APP_SECRET: {\n type: 'string',\n title: '飞书应用密钥',\n description: '飞书应用密钥',\n required: true,\n default: '',\n },\n}\n\nexport type FeishuOption = {\n // 用户 ID 类型\n receive_id_type: 'open_id' | 'union_id' | 'user_id' | 'email' | 'chat_id'\n // 消息接收者的 ID,ID 类型与查询参数 receive_id_type 的取值一致。\n receive_id: string\n // 消息类型。\n msg_type: 'text' | 'post' | 'image' | 'file' | 'audio' | 'media' | 'sticker' | 'interactive' | 'share_chat' | 'share_user' | 'system'\n // 消息内容,JSON 结构序列化后的字符串。该参数的取值与 msg_type 对应,例如 msg_type 取值为 text,则该参数需要传入文本类型的内容。\n content?: string\n // 自定义设置的唯一字符串序列,用于在发送消息时请求去重。持有相同 uuid 的请求,在 1 小时内至多成功发送一条消息。\n uuid?: string\n}\n\nexport type FeishuOptionSchema = OptionSchema<FeishuOption>\n\nexport const feishuOptionSchema: FeishuOptionSchema = {\n receive_id_type: {\n type: 'select',\n title: '用户 ID 类型',\n description: '用户 ID 类型',\n required: true,\n options: [\n {\n label: 'open_id',\n value: 'open_id',\n },\n {\n label: 'union_id',\n value: 'union_id',\n },\n {\n label: 'user_id',\n value: 'user_id',\n },\n {\n label: 'email',\n value: 'email',\n },\n {\n label: 'chat_id',\n value: 'chat_id',\n },\n ],\n },\n receive_id: {\n type: 'string',\n title: '消息接收者的 ID',\n description: '消息接收者的 ID,ID 类型与查询参数 receive_id_type 的取值一致。',\n required: true,\n },\n msg_type: {\n type: 'select',\n title: '消息类型',\n description: '消息类型',\n required: true,\n options: [\n {\n label: '文本',\n value: 'text',\n },\n {\n label: '富文本',\n value: 'post',\n },\n {\n label: '图片',\n value: 'image',\n },\n {\n label: '文件',\n value: 'file',\n },\n {\n label: '语音',\n value: 'audio',\n },\n {\n label: '视频',\n value: 'media',\n },\n {\n label: '表情包',\n value: 'sticker',\n },\n {\n label: '卡片',\n value: 'interactive',\n },\n {\n label: '分享群名片',\n value: 'share_chat',\n },\n {\n label: '分享个人名片',\n value: 'share_user',\n },\n {\n label: '系统消息',\n value: 'system',\n },\n ],\n },\n content: {\n type: 'string',\n title: '消息内容',\n description: '消息内容,JSON 结构序列化后的字符串。该参数的取值与 msg_type 对应,例如 msg_type 取值为 text,则该参数需要传入文本类型的内容。',\n required: false,\n },\n uuid: {\n type: 'string',\n title: '自定义设置的唯一字符串序列',\n description: '自定义设置的唯一字符串序列,用于在发送消息时请求去重。持有相同 uuid 的请求,在 1 小时内至多成功发送一条消息。',\n required: false,\n },\n}\n\n/**\n * 飞书。官方文档:https://open.feishu.cn/document/home/index\n *\n * @author CaoMeiYouRen\n * @date 2025-02-10\n * @export\n * @class Feishu\n */\nexport class Feishu implements Send {\n\n static readonly namespace = '飞书'\n\n static readonly configSchema = feishuConfigSchema\n\n static readonly optionSchema = feishuOptionSchema\n\n private readonly config: FeishuConfig\n\n /**\n * accessToken 的过期时间(时间戳)\n */\n private expiresTime: number\n\n private accessToken: string\n\n constructor(config: FeishuConfig) {\n this.config = config\n // 根据 configSchema 验证 config\n validate(config, Feishu.configSchema)\n }\n\n private async getAccessToken() {\n const { FEISHU_APP_ID, FEISHU_APP_SECRET } = this.config\n const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal'\n const data = {\n app_id: FEISHU_APP_ID,\n app_secret: FEISHU_APP_SECRET,\n }\n const result = await ajax({\n url,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json; charset=utf-8',\n },\n data,\n })\n const { code, msg, tenant_access_token, expire } = result.data\n if (code !== 0) { // 出错返回码,为0表示成功,非0表示调用失败\n throw new Error(msg || '获取 tenant_access_token 失败!')\n }\n this.expiresTime = Date.now() + expire * 1000\n Debugger('获取 tenant_access_token 成功: %s', tenant_access_token)\n return tenant_access_token as string\n }\n\n async send(title: string, desp?: string, option?: FeishuOption): Promise<SendResponse> {\n Debugger('title: \"%s\", desp: \"%s\", option: %O', title, desp, option)\n if (!this.accessToken || Date.now() >= this.expiresTime) {\n this.accessToken = await this.getAccessToken()\n }\n const { receive_id_type = 'open_id', receive_id, msg_type = 'text', content, uuid } = option\n const data = { receive_id, msg_type, content, uuid }\n if (!data.content) {\n switch (msg_type) {\n case 'text':\n data.content = JSON.stringify({\n text: `${title}${desp ? `\\n${desp}` : ''}`,\n })\n break\n case 'post':\n data.content = JSON.stringify({\n post: {\n zh_cn: {\n title,\n content: [\n [\n {\n tag: 'text',\n text: desp,\n },\n ],\n ],\n },\n },\n })\n break\n default:\n throw new Error('msg_type is required!')\n }\n }\n const result = await ajax({\n url: 'https://open.feishu.cn/open-apis/im/v1/messages',\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json; charset=utf-8',\n Authorization: `Bearer ${this.accessToken}`,\n },\n data,\n query: {\n receive_id_type: receive_id_type || 'open_id',\n },\n })\n return result\n }\n\n}\n","import debug from 'debug'\nimport { Send } from '@/interfaces/send'\nimport { ajax } from '@/utils/ajax'\nimport { SendResponse } from '@/interfaces/response'\nimport { ConfigSchema, OptionSchema } from '@/interfaces/schema'\nimport { validate } from '@/utils/validate'\n\nconst Debugger = debug('push:i-got')\n\nexport interface IGotConfig {\n /**\n * 微信搜索小程序“iGot”获取推送key\n */\n I_GOT_KEY: string\n}\n\nexport type IGotConfigSchema = ConfigSchema<IGotConfig>\n\nexport const iGotConfigSchema: IGotConfigSchema = {\n I_GOT_KEY: {\n type: 'string',\n title: 'iGot 推送key',\n description: 'iGot 推送key',\n required: true,\n default: '',\n },\n} as const\n\nexport interface IGotOption {\n /**\n * 链接; 点开消息后会主动跳转至此地址\n */\n url?: string\n /**\n * 是否自动复制; 为1自动复制\n */\n automaticallyCopy?: number\n /**\n * 紧急消息,为1表示紧急。此消息将置顶在小程序内, 同时会在推送的消息内做一定的特殊标识\n */\n urgent?: number\n /**\n * 需要自动复制的文本内容\n */\n copy?: string\n /**\n * 主题; 订阅链接下有效;对推送内容分类,用户可选择性订阅\n */\n topic?: string\n [key: string]: any\n}\n\nexport type IGotOptionSchema = OptionSchema<IGotOption>\n\nexport const iGotOptionSchema: IGotOptionSchema = {\n url: {\n type: 'string',\n title: '链接',\n description: '链接; 点开消息后会主动跳转至此地址',\n required: false,\n default: '',\n },\n automaticallyCopy: {\n type: 'number',\n title: '是否自动复制',\n description: '是否自动复制; 为1自动复制',\n required: false,\n default: 0,\n },\n urgent: {\n type: 'number',\n title: '紧急消息',\n description: '紧急消息,为1表示紧急。此消息将置顶在小程序内, 同时会在推送的消息内做一定的特殊标识',\n required: false,\n default: 0,\n },\n copy: {\n type: 'string',\n title: '需要自动复制的文本内容',\n description: '需要自动复制的文本内容',\n required: false,\n default: '',\n },\n topic: {\n type: 'string',\n title: '主题',\n description: '主题; 订阅链接下有效;对推送内容分类,用户可选择性订阅',\n required: false,\n default: '',\n },\n} as const\n\nexport interface IGotResponse {\n /**\n * 状态码; 0为正常\n */\n ret: number\n /**\n * 响应结果\n */\n data: {\n /**\n * 消息记录,后期开放其他接口用\n * */\n id: string\n }\n /**\n * 结果描述\n */\n errMsg: string\n}\n\n/**\n * iGot 推送,官方文档:http://hellyw.com\n *\n * @author CaoMeiYouRen\n * @date 2021-03-03\n * @export\n * @class IGot\n */\nexport class IGot implements Send {\n\n static readonly namespace = 'iGot'\n static readonly configSchema = iGotConfigSchema\n static readonly optionSchema = iGotOptionSchema\n /**\n * 微信搜索小程序“iGot”获取推送key\n *\n * @private\n */\n private I_GOT_KEY: string\n /**\n * @author CaoMeiYouRen\n * @date 2024-11-08\n * @param config 微信搜索小程序“iGot”获取推送key\n */\n constructor(config: IGotConfig) {\n const { I_GOT_KEY } = config\n this.I_GOT_KEY = I_GOT_KEY\n Debugger('set I_GOT_KEY: \"%s\"', I_GOT_KEY)\n // 根据 configSchema 验证 config\n validate(config, IGot.configSchema)\n }\n\n /**\n *\n *\n * @author CaoMeiYouRen\n * @date 2024-11-08\n * @param title 消息标题\n * @param [desp] 消息正文\n * @param [option] 额外选项\n * @returns\n */\n send(title: string, desp?: string, option?: IGotOption): Promise<SendResponse<IGotResponse>> {\n Debugger('title: \"%s\", desp: \"%s\", option: \"%o\"', title, desp, option)\n return ajax({\n url: `https://push.hellyw.com/${this.I_GOT_KEY}`,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n data: {\n title,\n content: desp || title,\n automaticallyCopy: 0, // 关闭自动复制\n ...option,\n },\n })\n }\n\n}\n","import debug from 'debug'\nimport { Send } from '@/interfaces/send'\nimport { ajax } from '@/utils/ajax'\nimport { SendResponse } from '@/interfaces/response'\nimport { ConfigSchema, OptionSchema } from '@/interfaces/schema'\nimport { validate } from '@/utils/validate'\nimport { rfc2047Encode } from '@/utils/crypto'\n\nconst Debugger = debug('push:ntfy')\n\nexport interface NtfyConfig {\n /**\n * 推送地址\n */\n NTFY_URL: string\n\n /**\n * 主题\n * 用于区分不同的推送目标。\n * 主题本质上是一个密码,所以请选择不容易猜到的东西。\n * 例如:`my-topic`\n */\n NTFY_TOPIC: string\n\n /**\n * 认证参数。\n * 支持 Basic Auth、Bearer Token。\n * Basic Auth 示例:\"Basic dGVzdDpwYXNz\"\n * Bearer Token 示例:\"Bearer tk_...\"\n */\n NTFY_AUTH?: string\n}\n\nexport type NtfyConfigSchema = ConfigSchema<NtfyConfig>\nexport const ntfyConfigSchema: NtfyConfigSchema = {\n NTFY_URL: {\n type: 'string',\n title: '推送地址',\n description: '推送地址',\n required: true,\n default: '',\n },\n NTFY_TOPIC: {\n type: 'string',\n title: '主题',\n description: '主题',\n required: true,\n default: '',\n },\n NTFY_AUTH: {\n type: 'string',\n title: '认证参数',\n description: '支持 Basic Auth、Bearer Token。\\n'\n + 'Basic Auth 示例:\"Basic dGVzdDpwYXNz\"\\n'\n + 'Bearer Token 示例:\"Bearer tk_...\"',\n required: false,\n default: '',\n },\n} as const\n\nexport interface NtfyOption {\n /**\n * 通知中显示的标题\n */\n title?: string\n /**\n * 通知中显示的消息正文\n */\n message?: string\n /**\n * 消息正文\n */\n body?: string\n /**\n * 消息优先级(1-5,1最低,5最高)\n */\n priority?: number\n /**\n * 标签列表(逗号分隔),支持Emoji短代码\n */\n tags?: string\n /**\n * 启用Markdown格式化(设为`true`或`yes`)\n */\n markdown?: boolean\n /**\n * 延迟发送时间(支持时间戳、自然语言如`tomorrow 10am`)\n */\n delay?: string\n /**\n * 点击通知时打开的URL\n */\n click?: string\n /**\n * 附加文件的URL\n */\n attach?: string\n /**\n * 附件的显示文件名\n */\n filename?: string\n /**\n * 通知图标的URL(仅支持JPEG/PNG)\n */\n icon?: string\n /**\n * 定义通知的操作按钮(JSON或简写格式)\n */\n actions?: string\n /**\n * 设为`no`禁止服务器缓存消息\n */\n cache?: boolean\n /**\n * 设为`no`禁止转发到Firebase(仅影响Android推送)\n */\n firebase?: boolean\n /**\n * 设为`1`启用UnifiedPush模式(用于Matrix网关)\n */\n unifiedPush?: boolean\n /**\n * 将通知转发到指定邮箱\n */\n email?: string\n /**\n * 发送语音呼叫(需验证手机号,仅限认证用户)\n */\n call?: string\n /**\n * 设为`text/markdown`启用Markdown\n */\n contentType?: string\n /**\n * 直接上传文件作为附件(需设置`X-Filename`)\n */\n file?: File\n}\n\nexport type NtfyOptionSchema = OptionSchema<NtfyOption>\n\nexport const ntfyOptionSchema: NtfyOptionSchema = {\n title: {\n type: 'string',\n title: '标题',\n description: '标题',\n required: false,\n default: '',\n },\n body: {\n type: 'string',\n title: '消息正文',\n description: '消息正文',\n required: false,\n default: '',\n },\n priority: {\n type: 'number',\n title: '消息优先级',\n description: '消息优先级(1-5,1最低,5最高)',\n required: false,\n default: 3,\n },\n tags: {\n type: 'string',\n title: '标签列表',\n description: '标签列表(逗号分隔),支持Emoji短代码',\n required: false,\n default: '',\n },\n markdown: {\n type: 'boolean',\n title: '启用Markdown格式',\n description: '启用Markdown格式(设为`true`或`yes`)',\n required: false,\n default: false,\n },\n delay: {\n type: 'string',\n title: '延迟发送时间',\n description: '延迟发送时间(支持时间戳、自然语言如`tomorrow 10am`)',\n required: false,\n default: '',\n },\n click: {\n type: 'string',\n title: '点击通知时打开的URL',\n description: '点击通知时打开的URL',\n required: false,\n default: '',\n },\n attach: {\n type: 'string',\n title: '附加文件的URL',\n description: '附加文件的URL',\n required: false,\n default: '',\n },\n filename: {\n type: 'string',\n title: '附件的显示文件名',\n description: '附件的显示文件名',\n required: false,\n default: '',\n },\n icon: {\n type: 'string',\n title: '通知图标的URL',\n description: '通知图标的URL(仅支持JPEG/PNG)',\n required: false,\n default: '',\n },\n actions: {\n type: 'string',\n title: '定义通知的操作按钮',\n description: '定义通知的操作按钮(JSON或简写格式)',\n required: false,\n default: '',\n },\n cache: {\n type: 'boolean',\n title: '禁止服务器缓存消息',\n description: '设为`no`禁止服务器缓存消息',\n required: false,\n default: false,\n },\n firebase: {\n type: 'boolean',\n title: '禁止转发到Firebase',\n description: '设为`no`禁止转发到Firebase(仅影响Android推送)',\n required: false,\n default: false,\n },\n unifiedPush: {\n type: 'boolean',\n title: '启用UnifiedPush模式',\n description: '设为`1`启用UnifiedPush模式(用于Matrix网关)',\n required: false,\n default: false,\n },\n email: {\n type: 'string',\n title: '邮箱',\n description: '将通知转发到指定邮箱',\n required: false,\n default: '',\n },\n call: {\n type: 'string',\n title: '发送语音呼叫',\n description: '发送语音呼叫(需验证手机号,仅限认证用户)',\n required: false,\n default: '',\n },\n contentType: {\n type: 'string',\n title: '编码格式',\n description: '设为`text/markdown`启用Markdown',\n required: false,\n default: '',\n },\n file: {\n type: 'object',\n title: '附件',\n description: '直接上传文件作为附件(需设置`X-Filename`)',\n required: false,\n },\n} as const\n\nexport interface NtfyResponse {\n /**\n * 消息ID\n */\n id: string\n /**\n * 消息发布时间(Unix时间戳)\n */\n time: number\n /**\n * 消息过期时间(Unix时间戳)\n */\n expires: number\n /**\n * 事件类型\n */\n event: string\n /**\n * 主题\n */\n topic: string\n /**\n * 消息内容\n */\n message: string\n}\n\n/**\n * ntfy推送。\n * 官方文档:https://ntfy.sh/docs/publish/\n *\n * @author CaoMeiYouRen\n * @date 2025-02-11\n * @export\n * @class Ntfy\n */\nexport class Ntfy implements Send {\n\n static readonly namespace = 'ntfy'\n static readonly configSchema = ntfyConfigSchema\n static readonly optionSchema = ntfyOptionSchema\n /**\n * 推送地址\n */\n private NTFY_URL: string\n /**\n * 认证参数。\n * 支持 Basic Auth、Bearer Token。\n * Basic Auth 示例:\"Basic dGVzdDpwYXNz\"\n * Bearer Token 示例:\"Bearer tk_...\"\n */\n private NTFY_AUTH?: string\n\n /**\n * 主题\n * 用于区分不同的推送目标。\n * 主题本质上是一个密码,所以请选择不容易猜到的东西。\n * 例如:`my-topic`\n */\n private NTFY_TOPIC: string\n\n constructor(config: NtfyConfig) {\n const { NTFY_URL, NTFY_AUTH, NTFY_TOPIC } = config\n this.NTFY_URL = NTFY_URL\n this.NTFY_TOPIC = NTFY_TOPIC\n this.NTFY_AUTH = NTFY_AUTH\n Debugger('set NTFY_URL: \"%s\", NTFY_TOPIC: \"%s\", NTFY_AUTH: \"%s\"', NTFY_URL, NTFY_TOPIC, NTFY_AUTH)\n // 根据 configSchema 验证 config\n validate(config, Ntfy.configSchema)\n }\n\n async send(title: string, desp: string, option?: NtfyOption): Promise<SendResponse<NtfyResponse>> {\n Debugger('option: \"%o\"', option)\n const { message, body, priority, tags, markdown, delay, click, attach, filename, icon, actions, cache, firebase, unifiedPush, email, call, contentType, file } = option || {}\n const headers: any = {}\n if (this.NTFY_AUTH) {\n headers['Authorization'] = this.NTFY_AUTH\n }\n if (contentType) {\n headers['Content-Type'] = contentType\n }\n const xTitle = title || option.title\n if (xTitle) {\n headers['X-Title'] = rfc2047Encode(xTitle)\n }\n if (message) {\n headers['X-Message'] = rfc2047Encode(message)\n }\n if (priority) {\n headers['X-Priority'] = priority.toString()\n }\n if (tags) {\n headers['X-Tags'] = tags\n }\n if (markdown) {\n headers['X-Markdown'] = markdown.toString()\n }\n if (delay) {\n headers['X-Delay'] = delay\n }\n if (click) {\n headers['X-Click'] = click\n }\n if (attach) {\n headers['X-Attach'] = attach\n }\n if (filename) {\n headers['X-Filename'] = filename\n }\n if (icon) {\n headers['X-Icon'] = icon\n }\n if (actions) {\n headers['X-Actions'] = actions\n }\n if (cache) {\n headers['X-Cache'] = cache ? 'yes' : 'no'\n }\n if (firebase) {\n headers['X-Firebase'] = firebase ? 'yes' : 'no'\n }\n if (unifiedPush) {\n headers['X-UnifiedPush'] = unifiedPush ? '1' : '0'\n }\n if (email) {\n headers['X-Email'] = email\n }\n if (call) {\n headers['X-Call'] = call\n }\n if (file) {\n headers['X-Filename'] = file.name\n headers['Content-Type'] = 'application/octet-stream'\n headers['Content-Length'] = file.size\n headers['Content-Disposition'] = `attachment; filename=\"${file.name}\"`\n }\n Debugger('headers: \"%o\"', headers)\n const data = desp || body || message\n Debugger('data: \"%s\"', data)\n const url = new URL(this.NTFY_TOPIC, this.NTFY_URL).toString()\n const response = await ajax({\n url,\n