feat: add tokenizer for notification

This commit is contained in:
moonrailgun 2024-01-07 00:11:14 +08:00
parent 3e9760d895
commit ce6fd56d51
11 changed files with 232 additions and 23 deletions

View File

@ -5,6 +5,8 @@ import { monitorProviders } from './provider';
import { sendNotification } from '../notification'; import { sendNotification } from '../notification';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { token } from '../notification/token';
import { ContentToken } from '../notification/token/type';
/** /**
* Class which actually run monitor data collect * Class which actually run monitor data collect
@ -55,21 +57,23 @@ export class MonitorRunner {
'DOWN', 'DOWN',
`Monitor [${monitor.name}] has been down` `Monitor [${monitor.name}] has been down`
); );
await this.notify( await this.notify(`[${monitor.name}] 🔴 Down`, [
`[${monitor.name}] 🔴 Down`, token.text(
`[${monitor.name}] 🔴 Down\nTime: ${dayjs().format( `[${monitor.name}] 🔴 Down\nTime: ${dayjs().format(
'YYYY-MM-DD HH:mm:ss (z)' 'YYYY-MM-DD HH:mm:ss (z)'
)}` )}`
); ),
]);
currentStatus = 'DOWN'; currentStatus = 'DOWN';
} else if (value > 0 && currentStatus === 'DOWN') { } else if (value > 0 && currentStatus === 'DOWN') {
await this.createEvent('UP', `Monitor [${monitor.name}] has been up`); await this.createEvent('UP', `Monitor [${monitor.name}] has been up`);
await this.notify( await this.notify(`[${monitor.name}] ✅ Up`, [
`[${monitor.name}] ✅ Up`, token.text(
`[${monitor.name}] ✅ Up\nTime: ${dayjs().format( `[${monitor.name}] ✅ Up\nTime: ${dayjs().format(
'YYYY-MM-DD HH:mm:ss (z)' 'YYYY-MM-DD HH:mm:ss (z)'
)}` )}`
); ),
]);
currentStatus = 'UP'; 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; const notifications = this.monitor.notifications;
await Promise.all( await Promise.all(
notifications.map((n) => notifications.map((n) =>

View File

@ -1,10 +1,17 @@
import { Notification } from '@prisma/client'; import { Notification } from '@prisma/client';
import { notificationProviders } from './provider'; import { notificationProviders } from './provider';
import { ExactType } from '../../../types';
import { ContentToken } from './token';
export async function sendNotification( export async function sendNotification(
notification: Pick<Notification, 'name' | 'type' | 'payload'>, notification: ExactType<
Pick<Notification, 'name' | 'type' | 'payload'>,
{
type: keyof typeof notificationProviders;
}
>,
title: string, title: string,
message: string message: ContentToken[]
) { ) {
const type = notification.type; const type = notification.type;

View File

@ -1,6 +1,7 @@
import { NotificationProvider } from './type'; import { NotificationProvider } from './type';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import SMTPTransport from 'nodemailer/lib/smtp-transport'; import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { htmlContentTokenizer } from '../token';
interface SMTPPayload { interface SMTPPayload {
hostname: string; hostname: string;
@ -35,7 +36,7 @@ export const smtp: NotificationProvider = {
} }
const subject = title; const subject = title;
const bodyTextContent = message; const bodyTextContent = htmlContentTokenizer.parse(message);
const transporter = nodemailer.createTransport(config); const transporter = nodemailer.createTransport(config);
await transporter.sendMail({ await transporter.sendMail({
@ -44,7 +45,7 @@ export const smtp: NotificationProvider = {
cc: payload.cc, cc: payload.cc,
bcc: payload.bcc, bcc: payload.bcc,
subject: subject, subject: subject,
text: bodyTextContent, html: bodyTextContent,
}); });
}, },
}; };

View File

@ -1,9 +1,10 @@
import { Notification } from '@prisma/client'; import { Notification } from '@prisma/client';
import { ContentToken } from '../token';
export interface NotificationProvider { export interface NotificationProvider {
send: ( send: (
notification: Pick<Notification, 'name' | 'type' | 'payload'>, notification: Pick<Notification, 'name' | 'type' | 'payload'>,
title: string, title: string,
message: string message: ContentToken[]
) => Promise<void>; ) => Promise<void>;
} }

View File

@ -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();

View File

@ -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('');
}
}

View File

@ -0,0 +1,35 @@
import {
ImageContentToken,
NewlineContentToken,
ParagraphContentToken,
TitleContentToken,
} from '../type';
import { BaseContentTokenizer } from './base';
export class HTMLContentTokenizer extends BaseContentTokenizer {
parseImage(token: ImageContentToken) {
return `<img src="${token.url}" />`;
}
parseParagraph(token: ParagraphContentToken) {
return `<p>${token.content}</p>`;
}
parseTitle(token: TitleContentToken) {
if (token.level === 1) {
return `<h1>${token.content}</h1>`;
}
if (token.level === 2) {
return `<h2>${token.content}</h2>`;
}
if (token.level === 3) {
return `<h3>${token.content}</h3>`;
}
return `<p>${token.content}</p>`;
}
parseNewline(token: NewlineContentToken) {
return '<br />';
}
}

View File

@ -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`;
}
}

View File

@ -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;

View File

@ -2,6 +2,7 @@ import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc';
import { z } from 'zod'; import { z } from 'zod';
import { prisma } from '../../model/_client'; import { prisma } from '../../model/_client';
import { sendNotification } from '../../model/notification'; import { sendNotification } from '../../model/notification';
import { token } from '../../model/notification/token';
export const notificationRouter = router({ export const notificationRouter = router({
all: workspaceProcedure.query(({ input }) => { all: workspaceProcedure.query(({ input }) => {
@ -23,11 +24,12 @@ export const notificationRouter = router({
}) })
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await sendNotification( await sendNotification(input, `${input.name} Notification Testing`, [
input, token.title('Tianji: Insight into everything', 2),
`${input.name} Notification Testing`, token.text(`This is Notification Testing from ${input.name}`),
`This is Notification Testing` token.newline(),
); token.image('https://tianji.msgbyte.com/img/social-card.png'),
]);
}), }),
upsert: workspaceOwnerProcedure upsert: workspaceOwnerProcedure
.input( .input(

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB