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 { 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) =>
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
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 { 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(
|
||||||
|
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