diff --git a/build/post-build-type.js b/build/post-build-type.js deleted file mode 100644 index 541f27a..0000000 --- a/build/post-build-type.js +++ /dev/null @@ -1,53 +0,0 @@ -const path = require('path') -const fs = require('fs') -const rimraf = require('rimraf') -const glob = require('glob') - -glob('dist/**/**', { - ignore: ['dist', 'dist/index.d.ts', 'dist/**/*.js?(.map)'] -}, (err, files) => { - if (err) { - console.error(err) - return - } - remove(files) -}) - -/** - * - * - * @author CaoMeiYouRen - * @date 2021-02-27 - * @param {string[]} files - */ -async function remove(files) { - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (fs.existsSync(file)) { - try { - await asyncRimraf(file) - } catch (error) { - console.error(error) - } - } - } -} - -/** - * - * - * @author CaoMeiYouRen - * @date 2021-02-27 - * @param {string} path - * @returns - */ -async function asyncRimraf(path) { - return new Promise((resolve, reject) => { - rimraf(path, (error) => { - if (error) { - reject(error) - } - resolve(true) - }) - }); -} \ No newline at end of file diff --git a/package.json b/package.json index eb74d6a..7e98a3e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "license": "MIT", "main": "dist/index.js", "module": "dist/index.esm.js", - "browser": "dist/index.browser.js", "types": "dist/index.d.ts", "files": [ "dist" @@ -20,7 +19,7 @@ "build": "cross-env NODE_ENV=production rollup -c", "postbuild": "npm run build:type", "build:type": "api-extractor run", - "postbuild:type": "node build/post-build-type.js", + "postbuild:type": "rimraf \"dist/**/!(*.js?(.map)|index.d.ts)\"", "analyzer": "cross-env NODE_ENV=production ANALYZER=true rollup -c", "dev": "cross-env NODE_ENV=development ts-node-dev src/index.ts", "dev:rollup": "cross-env NODE_ENV=development rollup -c", @@ -40,6 +39,7 @@ "@rollup/plugin-typescript": "^8.2.0", "@semantic-release/changelog": "^5.0.1", "@semantic-release/git": "^9.0.0", + "@types/crypto-js": "^4.0.1", "@types/debug": "^4.1.5", "@types/lodash": "^4.14.168", "@types/module-alias": "^2.0.0", @@ -52,12 +52,12 @@ "conventional-changelog-cmyr-config": "^1.2.3", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", - "debug": "^4.3.1", "eslint": "^7.20.0", "eslint-config-cmyr": "^1.0.10", "husky": "^5.1.1", "lint-staged": "^10.5.4", "lodash": "^4.17.21", + "module-alias": "^2.2.2", "rimraf": "^3.0.2", "rollup": "^2.39.1", "rollup-plugin-analyzer": "^4.0.0", @@ -70,7 +70,9 @@ }, "dependencies": { "axios": "^0.21.1", - "module-alias": "^2.2.2", + "colors": "^1.4.0", + "crypto-js": "^4.0.0", + "debug": "^4.3.1", "qs": "^6.9.6" }, "config": { @@ -93,4 +95,4 @@ "git add" ] } -} \ No newline at end of file +} diff --git a/rollup.config.js b/rollup.config.js index 92ab3f9..4abc59d 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -10,7 +10,6 @@ import _ from 'lodash' import path from 'path' import { dependencies, name } from './package.json' const external = Object.keys(dependencies) // 默认不打包 dependencies -external.push('debug') const outputName = _.upperFirst(_.camelCase(name))// 导出的模块名称 PascalCase const env = process.env const __PROD__ = env.NODE_ENV === 'production' @@ -113,7 +112,7 @@ export default [ plugins: getPlugins({ isBrowser: false, isDeclaration: false, - isMin: true, + isMin: false, }), }, { @@ -131,19 +130,19 @@ export default [ isMin: false, }), }, - { - input: 'src/index.ts', - external, - output: { - file: 'dist/index.browser.js', // 生成 browser - format: 'umd', - name: outputName, - sourcemap: true, - }, - plugins: getPlugins({ - isBrowser: true, - isDeclaration: false, - isMin: true, - }), - }, + // { // 本包不推荐在浏览器中使用,故不生成浏览器版本 + // input: 'src/index.ts', + // external, + // output: { + // file: 'dist/index.browser.js', // 生成 browser + // format: 'umd', + // name: outputName, + // sourcemap: true, + // }, + // plugins: getPlugins({ + // isBrowser: true, + // isDeclaration: false, + // isMin: true, + // }), + // }, ] \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 0b34456..9de86c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,3 @@ -import { ServerChan } from './push/server-chan' -import { ServerChanTurbo } from './push/server-chan-turbo' - -export { - ServerChan, - ServerChanTurbo, -} \ No newline at end of file +export * from './push/dingtalk' +export * from './push/server-chan' +export * from './push/server-chan-turbo' diff --git a/src/interfaces/send.ts b/src/interfaces/send.ts new file mode 100644 index 0000000..0b954da --- /dev/null +++ b/src/interfaces/send.ts @@ -0,0 +1,12 @@ +/** + * 要求所有 push 方法都至少实现了 send 接口 + * + * @author CaoMeiYouRen + * @date 2021-02-27 + * @export + * @interface Send + */ +interface Send { + send(...args: any[]): Promise +} +export { Send } \ No newline at end of file diff --git a/src/push/dingtalk.ts b/src/push/dingtalk.ts new file mode 100644 index 0000000..d0419ba --- /dev/null +++ b/src/push/dingtalk.ts @@ -0,0 +1,61 @@ +import { Send } from '../interfaces/send' +import { ajax } from '@/utils/ajax' +import { AxiosResponse } from 'axios' +import debug from 'debug' +import colors from 'colors' +import CryptoJS from 'crypto-js' +import { Message } from './dingtalk/index' + +const Debugger = debug('push:dingtalk') + +class RobotOption { + accessToken?: string + secret?: string +} + +export class Dingtalk implements Send { + private accessToken?: string + private secret?: string + private webhook: string = 'https://oapi.dingtalk.com/robot/send' + constructor(option: RobotOption = {}) { + Object.assign(this, option) + if (!this.accessToken) { + throw new Error('accessToken is required!') + } + if (!this.secret) { + console.warn(colors.yellow('Secret is undefined')) + } + } + + private getSign(timeStamp: number) { + let signStr = '' + if (this.secret) { + signStr = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(`${timeStamp}\n${this.secret}`, this.secret)) + Debugger('Sign string is %s, result is %s', `${timeStamp}\n${this.secret}`, signStr) + } + return signStr + } + + public async send(message: Message): Promise> { + const timestamp = Date.now() + const sign = this.getSign(timestamp) + const result = await ajax({ + url: this.webhook, + query: { + timestamp, + sign, + access_token: this.accessToken, + }, + data: message.get(), + headers: { + 'Content-Type': 'application/json', + }, + }) + Debugger('Result is %s, %s。', result.data.errcode, result.data.errmsg) + if (result.data.errcode === 310000) { + console.error('Send Failed:', result.data) + Debugger('Please check safe config : %O', result.data) + } + return result + } +} \ No newline at end of file diff --git a/src/push/dingtalk/ActionCard.ts b/src/push/dingtalk/ActionCard.ts new file mode 100644 index 0000000..0e0a89a --- /dev/null +++ b/src/push/dingtalk/ActionCard.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { MessageTemplateAbs } from './template' + +class ActionCard extends MessageTemplateAbs { + title: string + text: string + hideAvatar: number + btnOrientation?: number + singleTitle: string + singleURL: string + btns: { title: string, actionURL: string }[] + constructor() { + super() + this.msgtype = 'actionCard' + + this.title = '' + this.text = '' + // 0-正常发消息者头像,1-隐藏发消息者头像 + this.hideAvatar = 0 + // 0-按钮竖直排列,1-按钮横向排列 + this.btnOrientation = 0 + + // 单个按钮的方案。(设置此项和singleURL后btns无效) + this.singleTitle = '' + this.singleURL = '' + this.btns = [] + } + + setBtns(btns: ConcatArray<{ title: string, actionURL: string }>) { + this.btns = this.btns.concat(btns) + return this + } + + setSingleTitle(title: string) { + this.singleTitle = title + return this + } + + setSingleURL(url: string) { + this.singleURL = url + return this + } + + setTitle(title: string) { + this.title = title + return this + } + + setText(content: string) { + this.text = content + return this + } + + setBtnOrientation(flag: number) { + this.btnOrientation = flag + return this + } + + setHideAvatar(flag: number) { + this.hideAvatar = flag + return this + } + + get() { + return this.render({ + actionCard: { + title: this.title, + text: this.text, + hideAvatar: this.hideAvatar, + btnOrientation: this.btnOrientation, + btns: this.btns, + singleTitle: this.singleTitle, + singleURL: this.singleURL, + }, + }) + } +} + +export { ActionCard } diff --git a/src/push/dingtalk/FeedCard.ts b/src/push/dingtalk/FeedCard.ts new file mode 100644 index 0000000..59b8866 --- /dev/null +++ b/src/push/dingtalk/FeedCard.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { MessageTemplateAbs } from './template' +import { Link } from './Link' + +class FeedCard extends MessageTemplateAbs { + links: Link[] + constructor(links: Link[]) { + super() + this.msgtype = 'feedCard' + + this.links = links || [] + } + + addLinks(links: Link[]) { + this.links = this.links.concat(links) + return this + } + + get() { + return this.render({ + feedCard: { + links: this.links, + }, + }) + } +} + +export { FeedCard } diff --git a/src/push/dingtalk/Link.ts b/src/push/dingtalk/Link.ts new file mode 100644 index 0000000..779885b --- /dev/null +++ b/src/push/dingtalk/Link.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { MessageTemplateAbs } from './template' + +class Link extends MessageTemplateAbs { + title: string + messageUrl: string + picUrl?: string + text: string + constructor(content: any) { + super() + this.msgtype = 'link' + this.title = '' + this.messageUrl = '' + this.picUrl = '' + + this.setContent(content) + } + + setContent(content: string) { + this.text = content + return this + } + + setTitle(title: string) { + this.title = title + return this + } + + setImage(image: string) { + this.picUrl = image + return this + } + + setUrl(url: string) { + this.messageUrl = url + return this + } + + get() { + return this.render({ + link: { + text: this.text, + title: this.title, + picUrl: this.picUrl, + messageUrl: this.messageUrl, + }, + }) + } +} + +export { Link } diff --git a/src/push/dingtalk/Markdown.ts b/src/push/dingtalk/Markdown.ts new file mode 100644 index 0000000..06ec92a --- /dev/null +++ b/src/push/dingtalk/Markdown.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { MessageTemplateAbs } from './template' + +class Markdown extends MessageTemplateAbs { + items: string[] + title: string + constructor() { + super() + this.msgtype = 'markdown' + this.canAt = true + this.items = [] + } + + setTitle(title: string) { + this.title = title + return this + } + + add(text: string | string[]) { + if (Array.isArray(text)) { + this.items.concat(text) + } else { + this.items.push(text) + } + + return this + } + + get() { + return this.render({ + markdown: { + title: this.title, + text: this.items.join('\n'), + }, + }) + } +} + +export { Markdown } diff --git a/src/push/dingtalk/Message.ts b/src/push/dingtalk/Message.ts new file mode 100644 index 0000000..517f84f --- /dev/null +++ b/src/push/dingtalk/Message.ts @@ -0,0 +1,8 @@ +import { ActionCard } from './ActionCard' +import { FeedCard } from './FeedCard' +import { Link } from './Link' +import { Markdown } from './Markdown' +import { MessageTemplateAbs } from './template' +import { Text } from './Text' + +export type Message = MessageTemplateAbs | ActionCard | FeedCard | Link | Markdown | Text diff --git a/src/push/dingtalk/Text.ts b/src/push/dingtalk/Text.ts new file mode 100644 index 0000000..3729cfd --- /dev/null +++ b/src/push/dingtalk/Text.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { MessageTemplateAbs } from './template' + +class Text extends MessageTemplateAbs { + content: string + constructor(content: string) { + super() + this.msgtype = 'text' + this.canAt = true + this.setContent(content) + } + + setContent(content: string) { + this.content = content + return this + } + + get() { + return this.render({ + text: { + content: this.content, + }, + }) + } +} + +export { Text } diff --git a/src/push/dingtalk/index.ts b/src/push/dingtalk/index.ts new file mode 100644 index 0000000..4fcbe68 --- /dev/null +++ b/src/push/dingtalk/index.ts @@ -0,0 +1,8 @@ +export * from './ActionCard' +export * from './FeedCard' +export * from './Link' +export * from './Markdown' +export * from './Message' +export * from './template' +export * from './Text' + diff --git a/src/push/dingtalk/template.ts b/src/push/dingtalk/template.ts new file mode 100644 index 0000000..49f9540 --- /dev/null +++ b/src/push/dingtalk/template.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +class MessageTemplateAbs { + canAt: boolean + isAtAll: boolean + atMobiles: Set + atDingtalkIds: Set + msgtype: string + constructor() { + this.canAt = false + this.isAtAll = false + this.atMobiles = new Set() + this.atDingtalkIds = new Set() + if (new.target === MessageTemplateAbs) { + throw new Error('抽象类不可以实例化') + } + } + + render(options: unknown) { + return Object.assign({ + msgtype: this.msgtype, + }, options, this.canAt + ? { + at: { + atMobiles: Array.from(this.atMobiles), + atDingtalkIds: Array.from(this.atDingtalkIds), + isAtAll: this.isAtAll, + }, + } + : {}) + } + + get(): any { + throw new Error('抽象方法render不可以调用') + } + + toJsonString(): string { + throw new Error('抽象方法toJsonString不可以调用') + } + + atAll() { + this.isAtAll = true + return this + } + + atPhone(phones: string | string[]) { + if (phones instanceof Array) { + phones.forEach((phone) => { + this.atMobiles.add(phone) + }) + } else { + this.atMobiles.add(phones) + } + return this + } + + atId(ids: string | string[]) { + if (ids instanceof Array) { + ids.forEach((phone) => { + this.atDingtalkIds.add(phone) + }) + } else { + this.atDingtalkIds.add(ids) + } + return this + } +} + +export { MessageTemplateAbs } diff --git a/src/push/server-chan-turbo.ts b/src/push/server-chan-turbo.ts index f956514..1085eb9 100644 --- a/src/push/server-chan-turbo.ts +++ b/src/push/server-chan-turbo.ts @@ -1,5 +1,9 @@ +import { Send } from '../interfaces/send' import { ajax } from '@/utils/ajax' import { AxiosResponse } from 'axios' +import debug from 'debug' + +const Debugger = debug('push:server-chan-turbo') /** * 文档 https://sct.ftqq.com/ @@ -9,7 +13,7 @@ import { AxiosResponse } from 'axios' * @export * @class ServerChanTurbo */ -export class ServerChanTurbo { +export class ServerChanTurbo implements Send { /** * @@ -19,6 +23,7 @@ export class ServerChanTurbo { */ constructor(SCTKEY: string) { this.SCTKEY = SCTKEY + Debugger('set SCTKEY: "%s"', SCTKEY) } /** * @@ -36,11 +41,12 @@ export class ServerChanTurbo { * @param desp 消息的内容,支持 Markdown */ public async send(text: string, desp: string = ''): Promise> { + Debugger('text: "%s", desp: "%s"', text, desp) return ajax({ url: `https://sctapi.ftqq.com/${this.SCTKEY}.send`, method: 'POST', headers: { - 'Content-type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/x-www-form-urlencoded', }, data: { text, diff --git a/src/push/server-chan.ts b/src/push/server-chan.ts index c423b65..e16e5f5 100644 --- a/src/push/server-chan.ts +++ b/src/push/server-chan.ts @@ -1,3 +1,4 @@ +import { Send } from '../interfaces/send' import { ajax } from '@/utils/ajax' import { AxiosResponse } from 'axios' @@ -10,7 +11,7 @@ import { AxiosResponse } from 'axios' * @class ServerChan * @deprecated 旧版将在2021年4月后下线,请尽快完成配置的更新。详见https://sc.ftqq.com */ -export class ServerChan { +export class ServerChan implements Send { /** * @@ -40,7 +41,7 @@ export class ServerChan { url: `https://sc.ftqq.com/${this.SCKEY}.send`, method: 'POST', headers: { - 'Content-type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/x-www-form-urlencoded', }, data: { text, diff --git a/src/utils/ajax.ts b/src/utils/ajax.ts index 225771f..0aa9ec1 100644 --- a/src/utils/ajax.ts +++ b/src/utils/ajax.ts @@ -1,5 +1,8 @@ import axios, { AxiosResponse, Method } from 'axios' import qs from 'qs' +import debug from 'debug' + +const Debugger = debug('push:ajax') class AjaxConfig { url: string @@ -20,8 +23,9 @@ class AjaxConfig { */ export async function ajax(config: AjaxConfig): Promise> { try { + Debugger('ajax config: %O', config) const { url, query = {}, data = {}, method = 'GET', headers = {} } = config - const resp = await axios(url, { + const response = await axios(url, { method, headers, params: query, @@ -30,9 +34,9 @@ export async function ajax(config: AjaxConfig): Promise> { baseURL: '', transformRequest(reqData: any, reqHeaders?: Record) { const contentType = Object.keys(reqHeaders).find((e) => { - return e.toLocaleLowerCase().includes('content-type') + return e.toLocaleLowerCase().includes('Content-Type'.toLocaleLowerCase()) }) - if (contentType === 'application/x-www-form-urlencoded' && typeof reqData === 'object') { + if (typeof reqData === 'object' && reqHeaders[contentType] === 'application/x-www-form-urlencoded') { return qs.stringify(reqData) } if (typeof reqData === 'string' || reqData instanceof Buffer || reqData instanceof ArrayBuffer) { @@ -41,7 +45,8 @@ export async function ajax(config: AjaxConfig): Promise> { return JSON.stringify(reqData) }, }) - return resp + Debugger('response data: %O', response.data) + return response } catch (error) { if (error.toJSON) { console.error(error.toJSON()) diff --git a/tsconfig.json b/tsconfig.json index c198b19..b3e0df2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,8 @@ "watch": false, //在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。 "declaration": true, //生成类型文件 "importHelpers": true, + "esModuleInterop": true, + "moduleResolution": "node", "strictPropertyInitialization": false, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, //开启装饰器 @@ -19,7 +21,7 @@ "paths": { "@/*": [ "src/*" - ] + ], }, "lib": [ "esnext",