From ce6fd56d51c50e9f6cec903a2d753bc3820d9d15 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sun, 7 Jan 2024 00:11:14 +0800 Subject: [PATCH] feat: add tokenizer for notification --- src/server/model/monitor/runner.ts | 30 +++++---- src/server/model/notification/index.ts | 11 +++- .../model/notification/provider/smtp.ts | 5 +- .../model/notification/provider/type.ts | 3 +- src/server/model/notification/token/index.ts | 40 ++++++++++++ .../notification/token/tokenizer/base.ts | 58 ++++++++++++++++++ .../notification/token/tokenizer/html.ts | 35 +++++++++++ .../notification/token/tokenizer/markdown.ts | 30 +++++++++ src/server/model/notification/token/type.ts | 31 ++++++++++ src/server/trpc/routers/notification.ts | 12 ++-- website/static/img/logo@128.png | Bin 0 -> 3579 bytes 11 files changed, 232 insertions(+), 23 deletions(-) create mode 100644 src/server/model/notification/token/index.ts create mode 100644 src/server/model/notification/token/tokenizer/base.ts create mode 100644 src/server/model/notification/token/tokenizer/html.ts create mode 100644 src/server/model/notification/token/tokenizer/markdown.ts create mode 100644 src/server/model/notification/token/type.ts create mode 100644 website/static/img/logo@128.png diff --git a/src/server/model/monitor/runner.ts b/src/server/model/monitor/runner.ts index e1843d0..2c71916 100644 --- a/src/server/model/monitor/runner.ts +++ b/src/server/model/monitor/runner.ts @@ -5,6 +5,8 @@ import { monitorProviders } from './provider'; import { sendNotification } from '../notification'; import dayjs from 'dayjs'; import { logger } from '../../utils/logger'; +import { token } from '../notification/token'; +import { ContentToken } from '../notification/token/type'; /** * Class which actually run monitor data collect @@ -55,21 +57,23 @@ export class MonitorRunner { 'DOWN', `Monitor [${monitor.name}] has been down` ); - await this.notify( - `[${monitor.name}] 🔴 Down`, - `[${monitor.name}] 🔴 Down\nTime: ${dayjs().format( - 'YYYY-MM-DD HH:mm:ss (z)' - )}` - ); + await this.notify(`[${monitor.name}] 🔴 Down`, [ + token.text( + `[${monitor.name}] 🔴 Down\nTime: ${dayjs().format( + 'YYYY-MM-DD HH:mm:ss (z)' + )}` + ), + ]); currentStatus = 'DOWN'; } else if (value > 0 && currentStatus === 'DOWN') { await this.createEvent('UP', `Monitor [${monitor.name}] has been up`); - await this.notify( - `[${monitor.name}] ✅ Up`, - `[${monitor.name}] ✅ Up\nTime: ${dayjs().format( - 'YYYY-MM-DD HH:mm:ss (z)' - )}` - ); + await this.notify(`[${monitor.name}] ✅ Up`, [ + token.text( + `[${monitor.name}] ✅ Up\nTime: ${dayjs().format( + 'YYYY-MM-DD HH:mm:ss (z)' + )}` + ), + ]); currentStatus = 'UP'; } @@ -122,7 +126,7 @@ export class MonitorRunner { }); } - async notify(title: string, message: string) { + async notify(title: string, message: ContentToken[]) { const notifications = this.monitor.notifications; await Promise.all( notifications.map((n) => diff --git a/src/server/model/notification/index.ts b/src/server/model/notification/index.ts index 1d9e8f1..8d60e08 100644 --- a/src/server/model/notification/index.ts +++ b/src/server/model/notification/index.ts @@ -1,10 +1,17 @@ import { Notification } from '@prisma/client'; import { notificationProviders } from './provider'; +import { ExactType } from '../../../types'; +import { ContentToken } from './token'; export async function sendNotification( - notification: Pick, + notification: ExactType< + Pick, + { + type: keyof typeof notificationProviders; + } + >, title: string, - message: string + message: ContentToken[] ) { const type = notification.type; diff --git a/src/server/model/notification/provider/smtp.ts b/src/server/model/notification/provider/smtp.ts index fb0a7c9..18ffa5f 100644 --- a/src/server/model/notification/provider/smtp.ts +++ b/src/server/model/notification/provider/smtp.ts @@ -1,6 +1,7 @@ import { NotificationProvider } from './type'; import nodemailer from 'nodemailer'; import SMTPTransport from 'nodemailer/lib/smtp-transport'; +import { htmlContentTokenizer } from '../token'; interface SMTPPayload { hostname: string; @@ -35,7 +36,7 @@ export const smtp: NotificationProvider = { } const subject = title; - const bodyTextContent = message; + const bodyTextContent = htmlContentTokenizer.parse(message); const transporter = nodemailer.createTransport(config); await transporter.sendMail({ @@ -44,7 +45,7 @@ export const smtp: NotificationProvider = { cc: payload.cc, bcc: payload.bcc, subject: subject, - text: bodyTextContent, + html: bodyTextContent, }); }, }; diff --git a/src/server/model/notification/provider/type.ts b/src/server/model/notification/provider/type.ts index e1fa951..2e35de8 100644 --- a/src/server/model/notification/provider/type.ts +++ b/src/server/model/notification/provider/type.ts @@ -1,9 +1,10 @@ import { Notification } from '@prisma/client'; +import { ContentToken } from '../token'; export interface NotificationProvider { send: ( notification: Pick, title: string, - message: string + message: ContentToken[] ) => Promise; } diff --git a/src/server/model/notification/token/index.ts b/src/server/model/notification/token/index.ts new file mode 100644 index 0000000..d79bba0 --- /dev/null +++ b/src/server/model/notification/token/index.ts @@ -0,0 +1,40 @@ +import { BaseContentTokenizer } from './tokenizer/base'; +import { HTMLContentTokenizer } from './tokenizer/html'; +import { MarkdownContentTokenizer } from './tokenizer/markdown'; +import { + ContentToken, + ImageContentToken, + NewlineContentToken, + ParagraphContentToken, + TextContentToken, + TitleContentToken, +} from './type'; + +export { ContentToken }; + +export const token = { + text: (content: string): TextContentToken => ({ + type: 'text', + content, + }), + image: (url: string): ImageContentToken => ({ + type: 'image', + url, + }), + title: (content: string, level: 1 | 2 | 3): TitleContentToken => ({ + type: 'title', + level, + content, + }), + paragraph: (content: string): ParagraphContentToken => ({ + type: 'paragraph', + content, + }), + newline: (): NewlineContentToken => ({ + type: 'newline', + }), +}; + +export const baseContentTokenizer = new BaseContentTokenizer(); +export const htmlContentTokenizer = new HTMLContentTokenizer(); +export const markdownContentTokenizer = new MarkdownContentTokenizer(); diff --git a/src/server/model/notification/token/tokenizer/base.ts b/src/server/model/notification/token/tokenizer/base.ts new file mode 100644 index 0000000..e411beb --- /dev/null +++ b/src/server/model/notification/token/tokenizer/base.ts @@ -0,0 +1,58 @@ +import { + ContentToken, + ImageContentToken, + NewlineContentToken, + ParagraphContentToken, + TextContentToken, + TitleContentToken, +} from '../type'; + +export class BaseContentTokenizer { + parseText(token: TextContentToken) { + return token.content; + } + + parseImage(token: ImageContentToken) { + return '[image]'; + } + + parseTitle(token: TitleContentToken) { + return token.content; + } + + parseParagraph(token: ParagraphContentToken) { + return token.content; + } + + parseNewline(token: NewlineContentToken) { + return '\n'; + } + + parse(tokens: ContentToken[]) { + return tokens + .map((token) => { + if (token.type === 'text') { + return this.parseText(token); + } + + if (token.type === 'image') { + return this.parseImage(token); + } + + if (token.type === 'title') { + return this.parseTitle(token); + } + + if (token.type === 'paragraph') { + return this.parseParagraph(token); + } + + if (token.type === 'newline') { + return this.parseNewline(token); + } + + return ''; + }) + .join(''); + } +} diff --git a/src/server/model/notification/token/tokenizer/html.ts b/src/server/model/notification/token/tokenizer/html.ts new file mode 100644 index 0000000..a6351d4 --- /dev/null +++ b/src/server/model/notification/token/tokenizer/html.ts @@ -0,0 +1,35 @@ +import { + ImageContentToken, + NewlineContentToken, + ParagraphContentToken, + TitleContentToken, +} from '../type'; +import { BaseContentTokenizer } from './base'; + +export class HTMLContentTokenizer extends BaseContentTokenizer { + parseImage(token: ImageContentToken) { + return ``; + } + + parseParagraph(token: ParagraphContentToken) { + return `

${token.content}

`; + } + + parseTitle(token: TitleContentToken) { + if (token.level === 1) { + return `

${token.content}

`; + } + if (token.level === 2) { + return `

${token.content}

`; + } + if (token.level === 3) { + return `

${token.content}

`; + } + + return `

${token.content}

`; + } + + parseNewline(token: NewlineContentToken) { + return '
'; + } +} diff --git a/src/server/model/notification/token/tokenizer/markdown.ts b/src/server/model/notification/token/tokenizer/markdown.ts new file mode 100644 index 0000000..db5da75 --- /dev/null +++ b/src/server/model/notification/token/tokenizer/markdown.ts @@ -0,0 +1,30 @@ +import { + ImageContentToken, + ParagraphContentToken, + TitleContentToken, +} from '../type'; +import { BaseContentTokenizer } from './base'; + +export class MarkdownContentTokenizer extends BaseContentTokenizer { + parseImage(token: ImageContentToken) { + return `![](${token.url})`; + } + + parseParagraph(token: ParagraphContentToken) { + return `\n${token.content}\n`; + } + + parseTitle(token: TitleContentToken) { + if (token.level === 1) { + return `\n# ${token.content}\n`; + } + if (token.level === 2) { + return `\n## ${token.content}\n`; + } + if (token.level === 3) { + return `\n### ${token.content}\n`; + } + + return `\n${token.content}\n`; + } +} diff --git a/src/server/model/notification/token/type.ts b/src/server/model/notification/token/type.ts new file mode 100644 index 0000000..881538c --- /dev/null +++ b/src/server/model/notification/token/type.ts @@ -0,0 +1,31 @@ +export type TextContentToken = { + type: 'text'; + content: string; +}; + +export type ImageContentToken = { + type: 'image'; + url: string; +}; + +export type TitleContentToken = { + type: 'title'; + level: 1 | 2 | 3; + content: string; +}; + +export type ParagraphContentToken = { + type: 'paragraph'; + content: string; +}; + +export type NewlineContentToken = { + type: 'newline'; +}; + +export type ContentToken = + | TextContentToken + | ImageContentToken + | TitleContentToken + | ParagraphContentToken + | NewlineContentToken; diff --git a/src/server/trpc/routers/notification.ts b/src/server/trpc/routers/notification.ts index 2d27fea..42602b2 100644 --- a/src/server/trpc/routers/notification.ts +++ b/src/server/trpc/routers/notification.ts @@ -2,6 +2,7 @@ import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc'; import { z } from 'zod'; import { prisma } from '../../model/_client'; import { sendNotification } from '../../model/notification'; +import { token } from '../../model/notification/token'; export const notificationRouter = router({ all: workspaceProcedure.query(({ input }) => { @@ -23,11 +24,12 @@ export const notificationRouter = router({ }) ) .mutation(async ({ input }) => { - await sendNotification( - input, - `${input.name} Notification Testing`, - `This is Notification Testing` - ); + await sendNotification(input, `${input.name} Notification Testing`, [ + token.title('Tianji: Insight into everything', 2), + token.text(`This is Notification Testing from ${input.name}`), + token.newline(), + token.image('https://tianji.msgbyte.com/img/social-card.png'), + ]); }), upsert: workspaceOwnerProcedure .input( diff --git a/website/static/img/logo@128.png b/website/static/img/logo@128.png new file mode 100644 index 0000000000000000000000000000000000000000..ca0b4a73402c9191d97398d6770386291e8de329 GIT binary patch literal 3579 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 Bn3Vtk literal 0 HcmV?d00001