diff --git a/package.json b/package.json index bab59fb0b..9104ab439 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "koa-bodyparser": "^4.3.0", "lerna": "^3.22.0", "mockjs": "^1.1.0", + "nodemailer-mock": "^1.5.11", "nodemon": "^2.0.12", "pg": "^8.6.0", "pg-hstore": "^2.3.3", diff --git a/packages/plugin-notifications/.npmignore b/packages/plugin-notifications/.npmignore new file mode 100644 index 000000000..461574b2f --- /dev/null +++ b/packages/plugin-notifications/.npmignore @@ -0,0 +1,7 @@ +node_modules +*.log +docs +__tests__ +tsconfig.json +src +.fatherrc.ts \ No newline at end of file diff --git a/packages/plugin-notifications/package.json b/packages/plugin-notifications/package.json new file mode 100644 index 000000000..865126aa5 --- /dev/null +++ b/packages/plugin-notifications/package.json @@ -0,0 +1,11 @@ +{ + "name": "@nocobase/plugin-notifications", + "version": "0.5.0-alpha.15", + "main": "lib/index.js", + "license": "MIT", + "dependencies": { + "@nocobase/server": "^0.5.0-alpha.15", + "nodemailer": "^6.6.1" + }, + "gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e" +} diff --git a/packages/plugin-notifications/src/__tests__/notifications.test.ts b/packages/plugin-notifications/src/__tests__/notifications.test.ts new file mode 100644 index 000000000..d8e9ef432 --- /dev/null +++ b/packages/plugin-notifications/src/__tests__/notifications.test.ts @@ -0,0 +1,55 @@ +import Database, { Model, ModelCtor } from '@nocobase/database'; +import { Notification, NotificationService } from '../models'; +import nodemailerMock from 'nodemailer-mock'; +import { mockServer } from '@nocobase/test'; +import _ from 'lodash'; +import plugin from '../server'; + +jest.setTimeout(300000); + +describe('notifications', () => { + let db: Database; + let NotificationModel: ModelCtor; + + beforeEach(async () => { + const app = mockServer(); + app.plugin(plugin); + await app.load(); + db = app.db; + await db.sync(); + NotificationService.createTransport = nodemailerMock.createTransport; + NotificationModel = db.getModel('notifications') as ModelCtor; + }); + + afterEach(() => db.close()); + + it('create', async () => { + const notification = await NotificationModel.create({ + subject: 'Subject', + body: 'hell world', + receiver_options: { + data: 'to@nocobase.com', + fromTable: 'users', + filter: {}, + dataField: 'email', + }, + }); + await notification.updateAssociations({ + service: { + type: 'email', + title: '阿里云邮件推送', + options: { + host: 'smtpdm.aliyun.com', + port: 465, + secure: true, + auth: { + user: 'from@nocobase.com', + pass: 'pass', + }, + from: 'NocoBase', + }, + }, + }); + await notification.send(); + }); +}); diff --git a/packages/plugin-notifications/src/collections/notification_logs.ts b/packages/plugin-notifications/src/collections/notification_logs.ts new file mode 100644 index 000000000..95d1242d8 --- /dev/null +++ b/packages/plugin-notifications/src/collections/notification_logs.ts @@ -0,0 +1,24 @@ +import { TableOptions } from '@nocobase/database'; + +export default { + name: 'notification_logs', + model: 'NotificationLog', + title: '通知日志', + fields: [ + { + title: '接收人', + type: 'json', + name: 'receiver', + }, + { + title: '状态', + type: 'string', + name: 'state', + }, + { + title: '详情', + type: 'json', + name: 'response', + }, + ] +} as TableOptions; diff --git a/packages/plugin-notifications/src/collections/notification_services.ts b/packages/plugin-notifications/src/collections/notification_services.ts new file mode 100644 index 000000000..0bd31ca56 --- /dev/null +++ b/packages/plugin-notifications/src/collections/notification_services.ts @@ -0,0 +1,24 @@ +import { TableOptions } from '@nocobase/database'; + +export default { + name: 'notification_services', + model: 'NotificationService', + title: '通知服务', + fields: [ + { + title: '类型', + type: 'string', + name: 'type', + }, + { + title: '服务名称', + type: 'string', + name: 'title', + }, + { + title: '配置信息', + type: 'json', + name: 'options', + }, + ] +} as TableOptions; diff --git a/packages/plugin-notifications/src/collections/notifications.ts b/packages/plugin-notifications/src/collections/notifications.ts new file mode 100644 index 000000000..f9d6cebb2 --- /dev/null +++ b/packages/plugin-notifications/src/collections/notifications.ts @@ -0,0 +1,41 @@ +import { TableOptions } from '@nocobase/database'; + +export default { + name: 'notifications', + model: 'Notification', + title: '通知', + fields: [ + { + type: 'uid', + name: 'name', + prefix: 'n_', + }, + { + title: '主题', + type: 'string', + name: 'subject', + }, + { + title: '内容', + type: 'text', + name: 'body', + }, + { + title: '接收人配置', + type: 'json', + name: 'receiver_options', + }, + { + title: '发送服务', + type: 'belongsTo', + name: 'service', + target: 'notification_services', + }, + { + title: '日志', + type: 'hasMany', + name: 'logs', + target: 'notification_logs', + }, + ] +} as TableOptions; diff --git a/packages/plugin-notifications/src/models/Notification.ts b/packages/plugin-notifications/src/models/Notification.ts new file mode 100644 index 000000000..cffd737bb --- /dev/null +++ b/packages/plugin-notifications/src/models/Notification.ts @@ -0,0 +1,68 @@ +import { Model } from '@nocobase/database'; +import { NotificationService } from './NotificationService'; +import _ from 'lodash'; + +export class Notification extends Model { + + async getReceiversByOptions(): Promise { + const { data, fromTable, filter, dataField } = this.receiver_options; + let receivers = []; + if (data) { + receivers = Array.isArray(data) ? data : [data]; + } else if (fromTable) { + const M = this.database.getModel(fromTable); + const rows = await M.findAll(M.parseApiJson2({ + filter, + })); + receivers = rows.map(row => row[dataField]); + } + return receivers; + } + + async send(options: any = {}) { + if (!this.service) { + this.service = await this.getService(); + } + const receivers = await this.getReceiversByOptions(); + let { to } = options; + if (to) { + to = Array.isArray(to) ? to : [to]; + receivers.push(...to); + } + console.log(receivers) + for (const receiver of receivers) { + try { + const response = await (this.service as NotificationService).send({ + to: receiver, + subject: this.getSubject(), + html: this.getBody(options), + }); + await this.createLog({ + receiver, + state: 'success', + response, + }); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } catch (error) { + console.error(error); + await this.createLog({ + receiver, + state: 'fail', + response: {}, + }); + } + } + } + + getSubject() { + return this.subject; + } + + getBody(data) { + const compiled = _.template(this.body) + const body = compiled(data); + return body; + } +} diff --git a/packages/plugin-notifications/src/models/NotificationLog.ts b/packages/plugin-notifications/src/models/NotificationLog.ts new file mode 100644 index 000000000..0587cfa0e --- /dev/null +++ b/packages/plugin-notifications/src/models/NotificationLog.ts @@ -0,0 +1,5 @@ +import { Model } from '@nocobase/database'; + +export class NotificationLog extends Model { + +} diff --git a/packages/plugin-notifications/src/models/NotificationService.ts b/packages/plugin-notifications/src/models/NotificationService.ts new file mode 100644 index 000000000..3270aca70 --- /dev/null +++ b/packages/plugin-notifications/src/models/NotificationService.ts @@ -0,0 +1,24 @@ +import { Model } from '@nocobase/database'; +import nodemailer from 'nodemailer'; + +export class NotificationService extends Model { + + static createTransport = nodemailer.createTransport; + + get transporter() { + if (this._transporter) { + return this._transporter; + } + return this._transporter = NotificationService.createTransport(this.options); + } + + async send(options) { + const { from } = this.options; + const mailOptions = { + from, + ...options, + }; + console.log({ mailOptions }); + return this.transporter.sendMail(mailOptions); + } +} diff --git a/packages/plugin-notifications/src/models/index.ts b/packages/plugin-notifications/src/models/index.ts new file mode 100644 index 000000000..190994a9d --- /dev/null +++ b/packages/plugin-notifications/src/models/index.ts @@ -0,0 +1,3 @@ +export * from './Notification'; +export * from './NotificationLog'; +export * from './NotificationService'; diff --git a/packages/plugin-notifications/src/server.ts b/packages/plugin-notifications/src/server.ts new file mode 100644 index 000000000..a5e9c169e --- /dev/null +++ b/packages/plugin-notifications/src/server.ts @@ -0,0 +1,15 @@ +import path from 'path'; +import Database, { registerModels } from '@nocobase/database'; +import { PluginOptions } from '@nocobase/server'; +import * as models from './models'; + +registerModels(models); + +export default { + name: 'notifications', + async load() { + this.app.db.import({ + directory: path.resolve(__dirname, 'collections'), + }); + } +} as PluginOptions diff --git a/yarn.lock b/yarn.lock index f9976651b..5e03774c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7590,7 +7590,7 @@ debug@3.X, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@~4.3.1: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@~4.3.1: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -13894,6 +13894,19 @@ node-xlsx@^0.16.1: buffer-from "^1.1.1" xlsx "^0.17.0" +nodemailer-mock@^1.5.11: + version "1.5.11" + resolved "https://registry.npmjs.org/nodemailer-mock/-/nodemailer-mock-1.5.11.tgz#1bb6b9af44e9007380191d32e33555ea136a819f" + integrity sha512-RbqkppKkptTgSzry3q8u/YD8pxulln/hYQ30O5brFRki8gEKv7KoAYGwfErZM2VsI7nGNBYRo5IOLnMW3NXBcw== + dependencies: + debug "^4.3.2" + nodemailer "^6.x" + +nodemailer@^6.6.1, nodemailer@^6.x: + version "6.6.5" + resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.5.tgz#f9f6953cee5cfe82cbea152eeddacf7a0442049a" + integrity sha512-C/v856DBijUzHcHIgGpQoTrfsH3suKIRAGliIzCstatM2cAa+MYX3LuyCrABiO/cdJTxgBBHXxV1ztiqUwst5A== + nodemon@^2.0.12: version "2.0.12" resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz#5dae4e162b617b91f1873b3bfea215dd71e144d5"