UNPKG

pushoo

Version:

Instant Messaging Pushing SDK

588 lines (550 loc) 15.8 kB
import axios from 'axios'; import { marked } from 'marked'; import markdownToTxt from 'markdown-to-txt'; export interface NoticeOptions { /** * bark通知方式的参数配置 */ bark?: { /** * url 用于点击通知后跳转的地址 */ url?: string; }; /** * IFTTT通知方式的参数配置 */ ifttt?: { value1?: string; value2?: string; value3?: string; }; /** * Discord通知方式的参数配置 */ discord?: { userName?: string; avatarUrl?: string; }; /** * WxPusher通知方式的参数配置 */ wxpusher?: { uids?: string[]; url?: string; verifyPay?: boolean; }; /** * QMsg酱通知方式的参数配置 */ qmsg?: { qq?: string; url?: string; group?: boolean; bot?: string; }; dingtalk?: { /** * 消息类型,目前支持 text、markdown。不设置,默认为 text。 */ msgtype?: string; }; } export interface CommonOptions { token: string; title?: string; content: string; /** * 扩展选项 */ options?: NoticeOptions; } export type ChannelType = | 'qmsg' | 'serverchan' | 'serverchain' | 'pushplus' | 'pushplushxtrip' | 'dingtalk' | 'wecom' | 'bark' | 'gocqhttp' | 'atri' | 'pushdeer' | 'igot' | 'telegram' | 'feishu' | 'ifttt' | 'wecombot' | 'discord' | 'wxpusher'; function checkParameters(options: any, requires: string[] = []) { requires.forEach((require) => { if (!options[require]) { throw new Error(`${require} is required`); } }); } function getHtml(content: string) { return marked.parse(content); } function getTxt(content: string) { return markdownToTxt(content); } function getTitle(content: string) { return getTxt(content).split('\n')[0]; } function removeUrlAndIp(content: string) { const urlRegex = /(https?:\/\/[^\s]+)/g; const ipRegex = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g; // 邮箱正则表达式来自 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation const mailRegExp = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/g; return content .replace(urlRegex, '') .replace(ipRegex, '') .replace(mailRegExp, ''); } /** * https://qmsg.zendee.cn/ */ async function noticeQmsg(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = options?.options?.qmsg?.url || 'https://qmsg.zendee.cn'; let msg = getTxt(options.content); if (options.title) { msg = `${options.title}\n${msg}`; } // 移除网址和 IP 以避免 Qmsg 酱被 Tencent 封号 msg = removeUrlAndIp(msg); const param = new URLSearchParams({ msg }); const qq = options?.options?.qmsg?.qq || false; if (qq) { param.append('qq', qq); } const bot = options?.options?.qmsg?.bot || false; if (bot) { param.append('bot', bot); } const group = options?.options?.qmsg?.group || false; const response = await axios.post(`${url}/${group ? 'group' : 'send'}/${options.token}`, param.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); return response.data; } /** * https://github.com/Tianli0/push-bot-api/ */ async function noticeAtri(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = 'http://pushoo.tianli0.top/'; let message = getTxt(options.content); if (options.title) { message = `${options.title}\n${message}`; } const param = new URLSearchParams({ user_id: options.token, message, }); const response = await axios.post(url, param.toString(), { headers: { 'X-Requested-By': 'pushoo' }, }); return response.data; } /** * Turbo: https://sct.ftqq.com/ * V3: https://sc3.ft07.com/ */ async function noticeServerChan(options: CommonOptions) { checkParameters(options, ['token', 'content']); let url: string; let param: URLSearchParams; if (options.token.startsWith('sctp')) { url = `https://${options.token.match(/^sctp(\d+)t/)[1]}.push.ft07.com/send`; param = new URLSearchParams({ title: options.title || getTitle(options.content), desp: options.content, }); } else if (options.token.substring(0, 3).toLowerCase() === 'sct') { url = 'https://sctapi.ftqq.com'; param = new URLSearchParams({ title: options.title || getTitle(options.content), desp: options.content, }); } else { url = 'https://sc.ftqq.com'; param = new URLSearchParams({ text: options.title || getTitle(options.content), desp: options.content, }); } const response = await axios.post(`${url}/${options.token}.send`, param.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); return response.data; } /** * https://www.pushplus.plus/ */ async function noticePushPlus(options: CommonOptions) { checkParameters(options, ['token', 'content']); const ppApiUrl = 'http://www.pushplus.plus/send'; const ppApiParam = { token: options.token, title: options.title || getTitle(options.content), content: options.content, template: 'markdown', }; const response = await axios.post(ppApiUrl, ppApiParam); return response.data; } /** * https://pushplus.hxtrip.com/ */ async function noticePushPlusHxtrip(options: CommonOptions) { checkParameters(options, ['token', 'content']); const ppApiUrl = 'http://pushplus.hxtrip.com/send'; const ppApiParam = { token: options.token, title: options.title || getTitle(options.content), content: getHtml(options.content), template: 'html', }; const response = await axios.post(ppApiUrl, ppApiParam); return response.data; } /** * 文档: https://open.dingtalk.com/document/group/custom-robot-access * 教程: https://blog.ljcbaby.top/article/Twikoo-DingTalk/ */ async function noticeDingTalk(options: CommonOptions) { checkParameters(options, ['token', 'content']); let url = 'https://oapi.dingtalk.com/robot/send?access_token='; if (options.token.substring(0, 4).toLowerCase() === 'http') { url = options.token; } else { url += options.token; } const msgtype = options.options?.dingtalk?.msgtype || 'text'; const content = msgtype === 'text' ? (options.title ? `${options.title}\n` : '') + getTxt(options.content) : options.content; const msgBody = { msgtype, }; if (msgtype === 'text') { msgBody[msgtype] = { content }; } else if (msgtype === 'markdown') { msgBody[msgtype] = { title: options.title || getTitle(options.content), text: content }; } const response = await axios.post(url, msgBody); return response.data; } /** * 文档: https://developer.work.weixin.qq.com/document/path/90236 * 教程: https://sct.ftqq.com/forward */ async function noticeWeCom(options: CommonOptions) { checkParameters(options, ['token', 'content']); const [corpid, corpsecret, agentid, touser = '@all'] = options.token.split('#'); checkParameters( { corpid, corpsecret, agentid, }, ['corpid', 'corpsecret', 'agentid'], ); // 获取 Access Token let accessToken; try { const accessTokenRes = await axios.get( `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpid}&corpsecret=${corpsecret}`, ); accessToken = accessTokenRes.data.access_token; } catch (e) { console.error('获取企业微信 access token 失败,请检查 token', e); return {}; } // 发送消息 const url = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accessToken}`; let content = getTxt(options.content); if (options.title) { content = `${options.title}\n${content}`; } const param = { touser, msgtype: 'text', agentid, text: { content }, }; const response = await axios.post(url, param); return response.data; } /** * https://github.com/Finb/Bark */ async function noticeBark(options: CommonOptions) { checkParameters(options, ['token', 'content']); let url = 'https://api.day.app/'; if (options.token.substring(0, 4).toLowerCase() === 'http') { url = options.token; } else { url += options.token; } if (!url.endsWith('/')) url += '/'; const title = encodeURIComponent(options.title || getTitle(options.content)); const content = encodeURIComponent(getTxt(options.content)); const params = new URLSearchParams({ url: options?.options?.bark?.url || '', }); const response = await axios.get(`${url}${title}/${content}/`, { params }); return response.data; } /** * 文档: https://docs.go-cqhttp.org/api/ * 教程: https://twikoo.js.org/QQ_API.html */ async function noticeGoCqhttp(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = options.token; let message = getTxt(options.content); if (options.title) { message = `${options.title}\n${message}`; } const param = new URLSearchParams({ message }); const response = await axios.post(url, param.toString()); return response.data; } async function noticePushdeer(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = 'https://api2.pushdeer.com/message/push'; const response = await axios.post(url, { pushkey: options.token, text: options.title || getTitle(options.content), desp: options.content, }); return response.data; } async function noticeIgot(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = `https://push.hellyw.com/${options.token}`; const response = await axios.post(url, { title: options.title || getTitle(options.content), content: getTxt(options.content), }); return response.data; } /** * 文档: https://core.telegram.org/method/messages.sendMessage * 教程: https://core.telegram.org/bots#3-how-do-i-create-a-bot */ async function noticeTelegram(options: CommonOptions) { checkParameters(options, ['token', 'content']); const [tgToken, chatId] = options.token.split('#'); checkParameters( { tgToken, chatId, }, ['tgToken', 'chatId'], ); let text = options.content.replace(/([*_])/g, '\\$1'); // * 和 _ 似乎需要转义,否则会抛出 400 Bad Request 以及消息显示不正常 if (options.title) { text = `${options.title}\n\n${text}`; } const response = await axios.post(`https://api.telegram.org/bot${tgToken}/sendMessage`, { text, chat_id: chatId, parse_mode: 'Markdown', }); return response.data; } /** * https://www.feishu.cn/hc/zh-CN/articles/360024984973 */ async function noticeFeishu(options: CommonOptions) { checkParameters(options, ['token', 'content']); const v1 = 'https://open.feishu.cn/open-apis/bot/hook/'; const v2 = 'https://open.feishu.cn/open-apis/bot/v2/hook/'; let url; let params; if (options.token.substring(0, 4).toLowerCase() === 'http') { url = options.token; } else { url = v2 + options.token; } if (url.substring(0, v1.length) === v1) { params = { title: options.title || getTitle(options.content), text: getTxt(options.content), }; } else { let text = getTxt(options.content); if (options.title) { text = `${options.title}\n${text}`; } params = { msg_type: 'text', content: { text }, }; } const response = await axios.post(url, params); return response.data; } /** * https://ifttt.com/maker_webhooks * http://ift.tt/webhooks_faq */ async function noticeIfttt(options: CommonOptions) { checkParameters(options, ['token', 'content']); const [token, eventName] = options.token.split('#'); checkParameters( { token, eventName, }, ['token', 'eventName'], ); const url = `https://maker.ifttt.com/trigger/${eventName}/with/key/${token}`; const response = await axios.post( url, { value1: options.options?.ifttt?.value1 || getTxt(options.title), value2: options.options?.ifttt?.value2 || getTxt(options.content), value3: options.options?.ifttt?.value3, }, { headers: { 'Content-Type': 'application/json' }, }, ); return response.data; } /** * 文档: https://developer.work.weixin.qq.com/document/path/91770 * 教程: https://developer.work.weixin.qq.com/tutorial/detail/54 */ async function noticeWecombot(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${options.token}`; const content = getTxt(options.content); const response = await axios.post( url, { msgtype: 'text', text: { content, }, }, { headers: { 'Content-Type': 'application/json' }, }, ); return response.data; } /** * 文档:https://discord.com/developers/docs/resources/webhook#execute-webhook */ async function noticeDiscord(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = options.token.startsWith('https://') ? options.token : `https://discord.com/api/webhooks/${options.token.replace(/#/, '/')}`; const response = await axios.post( url, { content: options.content, username: options.options?.discord?.userName, avatar_url: options.options?.discord?.avatarUrl, }, { headers: { 'Content-Type': 'application/json' }, }, ); return `Delivered successfully, code ${response.status}.`; } /** * WXPusher 推送 * 教程:https://wxpusher.zjiecode.com/admin/ * 文档: https://wxpusher.zjiecode.com/docs/#/ */ async function noticeWxPusher(options: CommonOptions) { checkParameters(options, ['token', 'content']); const url = 'http://wxpusher.zjiecode.com/api/send/message'; const [appToken, topicIds] = options.token.split('#'); checkParameters({ appToken, topicIds }, ['appToken', 'topicIds']); const response = await axios.post( url, { appToken, content: options.content, summary: options.title || getTitle(options.content), contentType: 3, topicIds: topicIds.split(',').map((id) => Number(id)), uids: options?.options?.wxpusher?.uids || [], url: options?.options?.wxpusher?.url || '', verifyPayload: options?.options?.wxpusher?.verifyPay || false, }, { headers: { 'Content-Type': 'application/json', }, }, ); return response.data; } async function notice(channel: ChannelType, options: CommonOptions) { try { let data: any; const noticeFn = { qmsg: noticeQmsg, serverchan: noticeServerChan, serverchain: noticeServerChan, pushplus: noticePushPlus, pushplushxtrip: noticePushPlusHxtrip, dingtalk: noticeDingTalk, wecom: noticeWeCom, bark: noticeBark, gocqhttp: noticeGoCqhttp, atri: noticeAtri, pushdeer: noticePushdeer, igot: noticeIgot, telegram: noticeTelegram, feishu: noticeFeishu, ifttt: noticeIfttt, wecombot: noticeWecombot, discord: noticeDiscord, wxpusher: noticeWxPusher, }[channel.toLowerCase()]; if (noticeFn) { data = await noticeFn(options); } else { throw new Error(`<${channel}> is not supported`); } console.debug(`[PUSHOO] Send to <${channel}> result:`, data); return data; } catch (e) { console.error('[PUSHOO] Got error:', e.message); return { error: e }; } } export default notice; export { notice, noticeQmsg, noticeServerChan, noticePushPlus, noticePushPlusHxtrip, noticeDingTalk, noticeWeCom, noticeBark, noticeGoCqhttp, noticeAtri, noticePushdeer, noticeIgot, noticeTelegram, noticeFeishu, noticeIfttt, noticeWecombot, noticeDiscord, noticeWxPusher, };