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 0000000..ca0b4a7 Binary files /dev/null and b/website/static/img/logo@128.png differ