diff --git a/back/data/notify.ts b/back/data/notify.ts index 5ebe9593..35143e1a 100644 --- a/back/data/notify.ts +++ b/back/data/notify.ts @@ -16,65 +16,66 @@ abstract class NotificationBaseInfo { } export class GoCqHttpBotNotification extends NotificationBaseInfo { - public GOBOT_URL = ''; - public GOBOT_TOKEN = ''; - public GOBOT_QQ = ''; + public goCqHttpBotUrl = ''; + public goCqHttpBotToken = ''; + public goCqHttpBotQq = ''; } export class ServerChanNotification extends NotificationBaseInfo { - public SCKEY = ''; + public serverChanKey = ''; } export class BarkNotification extends NotificationBaseInfo { - public BARK_PUSH = ''; - public BARK_SOUND = ''; - public BARK_GROUP = 'qinglong'; + public barkPush = ''; + public barkSound = ''; + public barkGroup = 'qinglong'; } export class TelegramBotNotification extends NotificationBaseInfo { - public TG_BOT_TOKEN = ''; - public TG_USER_ID = ''; - public TG_PROXY_HOST = ''; - public TG_PROXY_PORT = ''; - public TG_PROXY_AUTH = ''; - public TG_API_HOST = 'api.telegram.org'; + public telegramBotToken = ''; + public telegramBotUserId = ''; + public telegramBotProxyHost = ''; + public telegramBotProxyPort = ''; + public telegramBotProxyAuth = ''; + public telegramBotApiHost = 'api.telegram.org'; } export class DingtalkBotNotification extends NotificationBaseInfo { - public DD_BOT_TOKEN = ''; - public DD_BOT_SECRET = ''; + public dingtalkBotToken = ''; + public dingtalkBotSecret = ''; } export class WeWorkBotNotification extends NotificationBaseInfo { - public QYWX_KEY = ''; + public weWorkBotKey = ''; } export class WeWorkAppNotification extends NotificationBaseInfo { - public QYWX_AM = ''; + public weWorkAppKey = ''; } export class IGotNotification extends NotificationBaseInfo { - public IGOT_PUSH_KEY = ''; + public iGotPushKey = ''; } export class PushPlusNotification extends NotificationBaseInfo { - public PUSH_PLUS_TOKEN = ''; - public PUSH_PLUS_USER = ''; + public pushPlusToken = ''; + public pushPlusUser = ''; } export class EmailNotification extends NotificationBaseInfo { - public service: string = ''; - public user: string = ''; - public pass: string = ''; + public emailService: string = ''; + public emailUser: string = ''; + public emailPass: string = ''; } -export type NotificationInfo = - | GoCqHttpBotNotification - | ServerChanNotification - | BarkNotification - | TelegramBotNotification - | DingtalkBotNotification - | WeWorkBotNotification - | IGotNotification - | PushPlusNotification - | EmailNotification; +export interface NotificationInfo + extends GoCqHttpBotNotification, + ServerChanNotification, + BarkNotification, + TelegramBotNotification, + DingtalkBotNotification, + WeWorkBotNotification, + WeWorkAppNotification, + IGotNotification, + PushPlusNotification, + EmailNotification {} diff --git a/back/services/auth.ts b/back/services/auth.ts index 3a6ff2a4..45ac539a 100644 --- a/back/services/auth.ts +++ b/back/services/auth.ts @@ -14,12 +14,11 @@ import NotificationService from './notify'; @Service() export default class AuthService { + @Inject((type) => NotificationService) + private notificationService!: NotificationService; private authDb = new DataStore({ filename: config.authDbFile }); - constructor( - @Inject('logger') private logger: winston.Logger, - private notificationService: NotificationService, - ) { + constructor(@Inject('logger') private logger: winston.Logger) { this.authDb.loadDatabase((err) => { if (err) throw err; }); diff --git a/back/services/notify.ts b/back/services/notify.ts index 7eced8b7..ab18b1af 100644 --- a/back/services/notify.ts +++ b/back/services/notify.ts @@ -2,9 +2,16 @@ import { NotificationInfo } from '../data/notify'; import { Service, Inject } from 'typedi'; import winston from 'winston'; import AuthService from './auth'; +import got from 'got'; +import nodemailer from 'nodemailer'; +import crypto from 'crypto'; +import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; @Service() export default class NotificationService { + @Inject((type) => AuthService) + private authService!: AuthService; + private modeMap = new Map([ ['goCqHttpBot', this.goCqHttpBot], ['serverChan', this.serverChan], @@ -18,14 +25,12 @@ export default class NotificationService { ['email', this.email], ]); + private timeout = 10000; private title = ''; private content = ''; private params!: Omit; - constructor( - @Inject('logger') private logger: winston.Logger, - private authService: AuthService, - ) {} + constructor(@Inject('logger') private logger: winston.Logger) {} public async notify(title: string, content: string) { const { type, ...rest } = await this.authService.getNotificationMode(); @@ -38,22 +43,257 @@ export default class NotificationService { } } - private async goCqHttpBot() {} + private async goCqHttpBot() { + const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; + const res: any = await got + .post( + `${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`, + { + timeout: this.timeout, + retry: 0, + json: { message: `${this.title}\n${this.content}` }, + }, + ) + .json(); + return res.retcode === 0; + } - private async serverChan() {} + private async serverChan() { + const { serverChanKey } = this.params; + const url = serverChanKey.includes('SCT') + ? `https://sctapi.ftqq.com/${SCKEY}.send` + : `https://sc.ftqq.com/${SCKEY}.send`; + const res: any = await got + .post(url, { + timeout: this.timeout, + retry: 0, + body: `text=${this.title}&desp=${this.content}`, + }) + .json(); + return res.errno === 0 || res.data.errno === 0; + } - private async bark() {} + private async bark() { + const { barkPush, barkSound, barkGroup } = this.params; + const url = `${barkPush}/${encodeURIComponent( + this.title, + )}/${encodeURIComponent( + this.content, + )}?sound=${barkSound}&group=${barkGroup}`; + const res: any = await got + .get(url, { + timeout: this.timeout, + retry: 0, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }) + .json(); + return res.code === 200; + } - private async telegramBot() {} + private async telegramBot() { + const { + telegramBotApiHost, + telegramBotProxyAuth, + telegramBotProxyHost, + telegramBotProxyPort, + telegramBotToken, + telegramBotUserId, + } = this.params; + const url = `https://${telegramBotApiHost}/bot${telegramBotToken}/sendMessage`; + const options = { + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + proxy: `http://${telegramBotProxyHost}:${telegramBotProxyPort}`, + }; + const httpAgent = new HttpProxyAgent(options); + const httpsAgent = new HttpsProxyAgent(options); + const res: any = await got + .post(url, { + timeout: this.timeout, + retry: 0, + body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + agent: { + http: httpAgent, + https: httpsAgent, + }, + }) + .json(); + return !!res.ok; + } - private async dingtalkBot() {} + private async dingtalkBot() { + const { dingtalkBotSecret, dingtalkBotToken } = this.params; + let secretParam = ''; + if (dingtalkBotSecret) { + const dateNow = Date.now(); + const hmac = crypto.createHmac('sha256', dingtalkBotSecret); + hmac.update(`${dateNow}\n${dingtalkBotSecret}`); + const result = encodeURIComponent(hmac.digest('base64')); + secretParam = `×tamp=${dateNow}&sign=${result}`; + } + const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`; + const res: any = await got + .post(url, { + timeout: this.timeout, + retry: 0, + json: { + msgtype: 'text', + text: { + content: ` ${this.title}\n\n${this.content}`, + }, + }, + }) + .json(); + return res.errcode === 0; + } - private async weWorkBot() {} + private async weWorkBot() { + const { weWorkBotKey } = this.params; + const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`; + const res: any = await got + .post(url, { + timeout: this.timeout, + retry: 0, + json: { + msgtype: 'text', + text: { + content: ` ${this.title}\n\n${this.content}`, + }, + }, + }) + .json(); + return res.errcode === 0; + } - private async weWorkApp() {} - private async iGot() {} + private async weWorkApp() { + const { weWorkAppKey } = this.params; + const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] = + weWorkAppKey.split(','); + const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`; + const { access_token } = await got + .post(url, { + timeout: this.timeout, + retry: 0, + json: { + corpid, + corpsecret, + }, + }) + .json(); - private async pushPlus() {} + let options: any = { + msgtype: 'mpnews', + mpnews: { + articles: [ + { + title: `${this.title}`, + thumb_media_id, + author: `智能助手`, + content_source_url: ``, + content: `${this.content.replace(/\n/g, '
')}`, + digest: `${this.content}`, + }, + ], + }, + }; + switch (thumb_media_id) { + case '0': + options = { + msgtype: 'textcard', + textcard: { + title: `${this.title}`, + description: `${this.content}`, + url: 'https://github.com/whyour/qinglong', + btntxt: '更多', + }, + }; + break; - private async email() {} + case '1': + options = { + msgtype: 'text', + text: { + content: `${this.title}\n\n${this.content}`, + }, + }; + break; + } + + const res: any = await got + .post( + `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`, + { + timeout: this.timeout, + retry: 0, + json: { + touser, + agentid, + safe: '0', + ...options, + }, + }, + ) + .json(); + + return res.errcode === 0; + } + + private async iGot() { + const { iGotPushKey } = this.params; + const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`; + const res: any = await got + .post(url, { + timeout: this.timeout, + retry: 0, + body: `title=${this.title}&content=${this.content}`, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }) + .json(); + + return res.ret === 0; + } + + private async pushPlus() { + const { pushPlusToken, pushPlusUser } = this.params; + const url = `https://www.pushplus.plus/send`; + const res: any = await got + .post(url, { + timeout: this.timeout, + retry: 0, + json: { + token: `${pushPlusToken}`, + title: `${this.title}`, + content: `${this.content.replace(/[\n\r]/g, '
')}`, + topic: `${pushPlusUser}`, + }, + }) + .json(); + + return res.code === 200; + } + + private async email() { + const { emailPass, emailService, emailUser } = this.params; + const transporter = nodemailer.createTransport({ + service: emailService, + auth: { + user: emailUser, + pass: emailPass, + }, + }); + + const info = await transporter.sendMail({ + from: `"青龙快讯" <${emailUser}>`, + to: `${emailUser}`, + subject: `${this.title}`, + html: `${this.content.replace(/\n/g, '
')}`, + }); + + transporter.close(); + + return !!info.messageId; + } } diff --git a/package.json b/package.json index 393deaaf..72cee3af 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,14 @@ "express-jwt": "^6.0.0", "express-urlrewrite": "^1.4.0", "got": "^11.8.2", + "hpagent": "^0.1.2", "iconv-lite": "^0.6.3", "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", "nedb": "^1.8.0", "node-fetch": "^2.6.1", "node-schedule": "^2.0.0", + "nodemailer": "^6.6.3", "p-queue": "6.6.2", "reflect-metadata": "^0.1.13", "typedi": "^0.8.0", @@ -58,6 +60,7 @@ "@types/nedb": "^1.8.11", "@types/node": "^14.11.2", "@types/node-fetch": "^2.5.8", + "@types/nodemailer": "^6.4.4", "@types/qrcode.react": "^1.0.1", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a72357ba..e7845647 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ specifiers: '@types/nedb': ^1.8.11 '@types/node': ^14.11.2 '@types/node-fetch': ^2.5.8 + '@types/nodemailer': ^6.4.4 '@types/qrcode.react': ^1.0.1 '@types/react': ^17.0.0 '@types/react-dom': ^17.0.0 @@ -30,6 +31,7 @@ specifiers: express-jwt: ^6.0.0 express-urlrewrite: ^1.4.0 got: ^11.8.2 + hpagent: ^0.1.2 iconv-lite: ^0.6.3 jsonwebtoken: ^8.5.1 lint-staged: ^10.0.7 @@ -37,6 +39,7 @@ specifiers: nedb: ^1.8.0 node-fetch: ^2.6.1 node-schedule: ^2.0.0 + nodemailer: ^6.6.3 nodemon: ^2.0.4 p-queue: 6.6.2 prettier: ^2.2.0 @@ -71,12 +74,14 @@ dependencies: express-jwt: 6.0.0 express-urlrewrite: 1.4.0 got: 11.8.2 + hpagent: 0.1.2 iconv-lite: 0.6.3 jsonwebtoken: 8.5.1 lodash: 4.17.21 nedb: 1.8.0 node-fetch: 2.6.1 node-schedule: 2.0.0 + nodemailer: 6.6.3 p-queue: 6.6.2 reflect-metadata: 0.1.13 typedi: 0.8.0 @@ -95,6 +100,7 @@ devDependencies: '@types/nedb': 1.8.11 '@types/node': 14.14.45 '@types/node-fetch': 2.5.10 + '@types/nodemailer': 6.4.4 '@types/qrcode.react': 1.0.1 '@types/react': 17.0.5 '@types/react-dom': 17.0.5 @@ -1186,6 +1192,16 @@ packages: resolution: {integrity: sha512-n2OQ+0Bz6WEsUjrvcHD1xZ8K+Kgo4cn9/w94s1bJS690QMUWfJPW/m7CCb7gPkA1fcYwL2UpjXP/rq/Eo41m6w==} dev: false + /@types/node/14.17.16: + resolution: {integrity: sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==} + dev: true + + /@types/nodemailer/6.4.4: + resolution: {integrity: sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==} + dependencies: + '@types/node': 14.17.16 + dev: true + /@types/normalize-package-data/2.4.0: resolution: {integrity: sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==} dev: true @@ -4232,6 +4248,10 @@ packages: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true + /hpagent/0.1.2: + resolution: {integrity: sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==} + dev: false + /html-encoding-sniffer/1.0.2: resolution: {integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==} dependencies: @@ -6162,6 +6182,11 @@ packages: sorted-array-functions: 1.3.0 dev: false + /nodemailer/6.6.3: + resolution: {integrity: sha512-faZFufgTMrphYoDjvyVpbpJcYzwyFnbAMmQtj1lVBYAUSm3SOy2fIdd9+Mr4UxPosBa0JRw9bJoIwQn+nswiew==} + engines: {node: '>=6.0.0'} + dev: false + /nodemon/2.0.7: resolution: {integrity: sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==} engines: {node: '>=8.10.0'}