feat: add tokenizer for notification
This commit is contained in:
parent
3e9760d895
commit
ce6fd56d51
@ -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`,
|
||||
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`,
|
||||
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) =>
|
||||
|
@ -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, 'name' | 'type' | 'payload'>,
|
||||
notification: ExactType<
|
||||
Pick<Notification, 'name' | 'type' | 'payload'>,
|
||||
{
|
||||
type: keyof typeof notificationProviders;
|
||||
}
|
||||
>,
|
||||
title: string,
|
||||
message: string
|
||||
message: ContentToken[]
|
||||
) {
|
||||
const type = notification.type;
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Notification } from '@prisma/client';
|
||||
import { ContentToken } from '../token';
|
||||
|
||||
export interface NotificationProvider {
|
||||
send: (
|
||||
notification: Pick<Notification, 'name' | 'type' | 'payload'>,
|
||||
title: string,
|
||||
message: string
|
||||
message: ContentToken[]
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
40
src/server/model/notification/token/index.ts
Normal file
40
src/server/model/notification/token/index.ts
Normal 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();
|
58
src/server/model/notification/token/tokenizer/base.ts
Normal file
58
src/server/model/notification/token/tokenizer/base.ts
Normal 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('');
|
||||
}
|
||||
}
|
35
src/server/model/notification/token/tokenizer/html.ts
Normal file
35
src/server/model/notification/token/tokenizer/html.ts
Normal 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 />';
|
||||
}
|
||||
}
|
30
src/server/model/notification/token/tokenizer/markdown.ts
Normal file
30
src/server/model/notification/token/tokenizer/markdown.ts
Normal 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`;
|
||||
}
|
||||
}
|
31
src/server/model/notification/token/type.ts
Normal file
31
src/server/model/notification/token/type.ts
Normal 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;
|
@ -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(
|
||||
|
BIN
website/static/img/logo@128.png
Normal file
BIN
website/static/img/logo@128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
Loading…
Reference in New Issue
Block a user