feat: support webhook (#1298)
This commit is contained in:
@@ -61,6 +61,7 @@ import { SyncModule } from './modules/sync/sync.module'
|
|||||||
import { TopicModule } from './modules/topic/topic.module'
|
import { TopicModule } from './modules/topic/topic.module'
|
||||||
import { UpdateModule } from './modules/update/update.module'
|
import { UpdateModule } from './modules/update/update.module'
|
||||||
import { UserModule } from './modules/user/user.module'
|
import { UserModule } from './modules/user/user.module'
|
||||||
|
import { WebhookModule } from './modules/webhook/webhook.module'
|
||||||
import { DatabaseModule } from './processors/database/database.module'
|
import { DatabaseModule } from './processors/database/database.module'
|
||||||
import { GatewayModule } from './processors/gateway/gateway.module'
|
import { GatewayModule } from './processors/gateway/gateway.module'
|
||||||
import { HelperModule } from './processors/helper/helper.module'
|
import { HelperModule } from './processors/helper/helper.module'
|
||||||
@@ -109,6 +110,7 @@ import { RedisModule } from './processors/redis/redis.module'
|
|||||||
TopicModule,
|
TopicModule,
|
||||||
UpdateModule,
|
UpdateModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
WebhookModule,
|
||||||
|
|
||||||
PageProxyModule,
|
PageProxyModule,
|
||||||
RenderEjsModule,
|
RenderEjsModule,
|
||||||
|
|||||||
@@ -53,15 +53,15 @@ export enum BusinessEvents {
|
|||||||
STDOUT = 'STDOUT',
|
STDOUT = 'STDOUT',
|
||||||
|
|
||||||
// activity
|
// activity
|
||||||
ACTIVITY_LIKE = 'activity_like',
|
ACTIVITY_LIKE = 'ACTIVITY_LIKE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EventScope {
|
export enum EventScope {
|
||||||
ALL,
|
TO_VISITOR = 1 << 0,
|
||||||
TO_VISITOR,
|
TO_ADMIN = 1 << 1,
|
||||||
TO_ADMIN,
|
TO_SYSTEM = 1 << 2,
|
||||||
TO_SYSTEM,
|
TO_VISITOR_ADMIN = (1 << 0) | (1 << 1),
|
||||||
TO_VISITOR_ADMIN,
|
TO_SYSTEM_VISITOR = (1 << 0) | (1 << 2),
|
||||||
TO_SYSTEM_VISITOR,
|
TO_SYSTEM_ADMIN = (1 << 1) | (1 << 2),
|
||||||
TO_SYSTEM_ADMIN,
|
ALL = (1 << 0) | (1 << 1) | (1 << 2),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export const CATEGORY_COLLECTION_NAME = 'categories'
|
|||||||
export const COMMENT_COLLECTION_NAME = 'comments'
|
export const COMMENT_COLLECTION_NAME = 'comments'
|
||||||
export const RECENTLY_COLLECTION_NAME = 'recentlies'
|
export const RECENTLY_COLLECTION_NAME = 'recentlies'
|
||||||
|
|
||||||
|
export const Analyze_COLLECTION_NAME = 'analyzes'
|
||||||
|
export const WEBHOOK_EVENT_COLLECTION_NAME = 'webhook_events'
|
||||||
|
|
||||||
export enum CollectionRefTypes {
|
export enum CollectionRefTypes {
|
||||||
Post = POST_COLLECTION_NAME,
|
Post = POST_COLLECTION_NAME,
|
||||||
Note = NOTE_COLLECTION_NAME,
|
Note = NOTE_COLLECTION_NAME,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { UAParser } from 'ua-parser-js'
|
|||||||
|
|
||||||
import { index, modelOptions, prop, Severity } from '@typegoose/typegoose'
|
import { index, modelOptions, prop, Severity } from '@typegoose/typegoose'
|
||||||
|
|
||||||
|
import { Analyze_COLLECTION_NAME } from '~/constants/db.constant'
|
||||||
import { BaseModel } from '~/shared/model/base.model'
|
import { BaseModel } from '~/shared/model/base.model'
|
||||||
|
|
||||||
@modelOptions({
|
@modelOptions({
|
||||||
@@ -13,7 +14,7 @@ import { BaseModel } from '~/shared/model/base.model'
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
customName: 'Analyze',
|
customName: Analyze_COLLECTION_NAME,
|
||||||
allowMixed: Severity.ALLOW,
|
allowMixed: Severity.ALLOW,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { existsSync, statSync } from 'fs'
|
import { existsSync, statSync } from 'fs'
|
||||||
import { readdir, readFile, rm, writeFile } from 'fs/promises'
|
import { readdir, readFile, rm, writeFile } from 'fs/promises'
|
||||||
import { join, resolve } from 'path'
|
import { join, resolve } from 'path'
|
||||||
|
import { flatten } from 'lodash'
|
||||||
import { mkdirp } from 'mkdirp'
|
import { mkdirp } from 'mkdirp'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,10 @@ import { DEMO_MODE, MONGO_DB } from '~/app.config'
|
|||||||
import { CronDescription } from '~/common/decorators/cron-description.decorator'
|
import { CronDescription } from '~/common/decorators/cron-description.decorator'
|
||||||
import { CronOnce } from '~/common/decorators/cron-once.decorator'
|
import { CronOnce } from '~/common/decorators/cron-once.decorator'
|
||||||
import { BusinessEvents, EventScope } from '~/constants/business-event.constant'
|
import { BusinessEvents, EventScope } from '~/constants/business-event.constant'
|
||||||
|
import {
|
||||||
|
Analyze_COLLECTION_NAME,
|
||||||
|
WEBHOOK_EVENT_COLLECTION_NAME,
|
||||||
|
} from '~/constants/db.constant'
|
||||||
import { BACKUP_DIR, DATA_DIR } from '~/constants/path.constant'
|
import { BACKUP_DIR, DATA_DIR } from '~/constants/path.constant'
|
||||||
import { migrateDatabase } from '~/migration/migrate'
|
import { migrateDatabase } from '~/migration/migrate'
|
||||||
import { EventManagerService } from '~/processors/helper/helper.event.service'
|
import { EventManagerService } from '~/processors/helper/helper.event.service'
|
||||||
@@ -25,6 +30,10 @@ import { getFolderSize, installPKG } from '~/utils/system.util'
|
|||||||
|
|
||||||
import { ConfigsService } from '../configs/configs.service'
|
import { ConfigsService } from '../configs/configs.service'
|
||||||
|
|
||||||
|
const excludeCollections = [
|
||||||
|
Analyze_COLLECTION_NAME,
|
||||||
|
WEBHOOK_EVENT_COLLECTION_NAME,
|
||||||
|
]
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BackupService {
|
export class BackupService {
|
||||||
private logger: Logger
|
private logger: Logger
|
||||||
@@ -79,7 +88,14 @@ export class BackupService {
|
|||||||
const backupDirPath = join(BACKUP_DIR, dateDir)
|
const backupDirPath = join(BACKUP_DIR, dateDir)
|
||||||
mkdirp.sync(backupDirPath)
|
mkdirp.sync(backupDirPath)
|
||||||
try {
|
try {
|
||||||
await $`mongodump -h ${MONGO_DB.host} --port ${MONGO_DB.port} -d ${MONGO_DB.dbName} --excludeCollection analyzes -o ${backupDirPath} >/dev/null 2>&1`
|
await $`mongodump -h ${MONGO_DB.host} --port ${MONGO_DB.port} -d ${
|
||||||
|
MONGO_DB.dbName
|
||||||
|
} ${flatten(
|
||||||
|
excludeCollections.map((collection) => [
|
||||||
|
'--excludeCollection',
|
||||||
|
`${collection}`,
|
||||||
|
]),
|
||||||
|
)} -o ${backupDirPath} >/dev/null 2>&1`
|
||||||
// 打包 DB
|
// 打包 DB
|
||||||
cd(backupDirPath)
|
cd(backupDirPath)
|
||||||
await $`mv ${MONGO_DB.dbName} mx-space`.quiet().nothrow()
|
await $`mv ${MONGO_DB.dbName} mx-space`.quiet().nothrow()
|
||||||
|
|||||||
@@ -40,9 +40,8 @@ import { NoteMusic } from './models/music.model'
|
|||||||
export class NoteModel extends WriteBaseModel {
|
export class NoteModel extends WriteBaseModel {
|
||||||
@prop()
|
@prop()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
|
||||||
@Transform(({ value: title }) => (title.length === 0 ? '无题' : title))
|
@Transform(({ value: title }) => (title.length === 0 ? '无题' : title))
|
||||||
title: string
|
declare title: string
|
||||||
@prop({ required: false, unique: true })
|
@prop({ required: false, unique: true })
|
||||||
public nid: number
|
public nid: number
|
||||||
|
|
||||||
|
|||||||
6
apps/core/src/modules/note/note.type.ts
Normal file
6
apps/core/src/modules/note/note.type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { TopicModel } from '../topic/topic.model'
|
||||||
|
import type { NoteModel } from './note.model'
|
||||||
|
|
||||||
|
export type NormalizedNote = Omit<NoteModel, 'password' | 'topic'> & {
|
||||||
|
topic: TopicModel
|
||||||
|
}
|
||||||
6
apps/core/src/modules/post/post.type.ts
Normal file
6
apps/core/src/modules/post/post.type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { CategoryModel } from '../category/category.model'
|
||||||
|
import type { PostModel } from './post.model'
|
||||||
|
|
||||||
|
export type NormalizedPost = Omit<PostModel, 'category'> & {
|
||||||
|
category: CategoryModel
|
||||||
|
}
|
||||||
60
apps/core/src/modules/webhook/webhook-event.model.ts
Normal file
60
apps/core/src/modules/webhook/webhook-event.model.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import Paginate from 'mongoose-paginate-v2'
|
||||||
|
|
||||||
|
import { modelOptions, plugin, prop, Ref } from '@typegoose/typegoose'
|
||||||
|
|
||||||
|
import { WEBHOOK_EVENT_COLLECTION_NAME } from '~/constants/db.constant'
|
||||||
|
import { mongooseLeanId } from '~/shared/model/plugins/lean-id'
|
||||||
|
|
||||||
|
import { WebhookModel } from './webhook.model'
|
||||||
|
|
||||||
|
type JSON = string
|
||||||
|
|
||||||
|
const JSONProps = {
|
||||||
|
type: String,
|
||||||
|
set(value) {
|
||||||
|
if (typeof value === 'object' && value) {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@modelOptions({
|
||||||
|
schemaOptions: {
|
||||||
|
timestamps: {
|
||||||
|
createdAt: 'timestamp',
|
||||||
|
updatedAt: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
customName: WEBHOOK_EVENT_COLLECTION_NAME,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@plugin(Paginate)
|
||||||
|
@plugin(mongooseLeanId)
|
||||||
|
export class WebhookEventModel {
|
||||||
|
@prop(JSONProps)
|
||||||
|
headers: JSON
|
||||||
|
|
||||||
|
@prop(JSONProps)
|
||||||
|
payload: JSON
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
event: string
|
||||||
|
|
||||||
|
@prop({ type: String })
|
||||||
|
response: JSON
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
success: boolean
|
||||||
|
|
||||||
|
@prop({
|
||||||
|
ref: () => WebhookModel,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
hookId: Ref<WebhookModel>
|
||||||
|
|
||||||
|
@prop({
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
status: number
|
||||||
|
}
|
||||||
64
apps/core/src/modules/webhook/webhook.controller.ts
Normal file
64
apps/core/src/modules/webhook/webhook.controller.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Body, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { ApiController } from '~/common/decorators/api-controller.decorator'
|
||||||
|
import { Auth } from '~/common/decorators/auth.decorator'
|
||||||
|
import { HTTPDecorators } from '~/common/decorators/http.decorator'
|
||||||
|
import { BusinessEvents } from '~/constants/business-event.constant'
|
||||||
|
import { MongoIdDto } from '~/shared/dto/id.dto'
|
||||||
|
import { PagerDto } from '~/shared/dto/pager.dto'
|
||||||
|
|
||||||
|
import { WebhookDtoPartial, WebhookModel } from './webhook.model'
|
||||||
|
import { WebhookService } from './webhook.service'
|
||||||
|
|
||||||
|
@ApiController('/webhooks')
|
||||||
|
@Auth()
|
||||||
|
export class WebhookController {
|
||||||
|
constructor(private readonly service: WebhookService) {}
|
||||||
|
|
||||||
|
@Post('/')
|
||||||
|
create(@Body() body: WebhookModel) {
|
||||||
|
body.events = this.service.transformEvents(body.events)
|
||||||
|
|
||||||
|
return this.service.createWebhook(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
async getAll() {
|
||||||
|
return this.service.getAllWebhooks().then((data) => {
|
||||||
|
Reflect.deleteProperty(data, 'secret')
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('/:id')
|
||||||
|
async update(@Body() body: WebhookDtoPartial, @Param() { id }: MongoIdDto) {
|
||||||
|
if (body.events) body.events = this.service.transformEvents(body.events)
|
||||||
|
|
||||||
|
return this.service.updateWebhook(id, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('/:id')
|
||||||
|
async delete(@Param() { id }: MongoIdDto) {
|
||||||
|
return this.service.deleteWebhook(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:id')
|
||||||
|
@HTTPDecorators.Paginator
|
||||||
|
async getEventsByHookId(
|
||||||
|
@Param() { id }: MongoIdDto,
|
||||||
|
|
||||||
|
@Query() query: PagerDto,
|
||||||
|
) {
|
||||||
|
return this.service.getEventsByHookId(id, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/events')
|
||||||
|
async getEventsEnum() {
|
||||||
|
return Object.values(BusinessEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/redispatch/:id')
|
||||||
|
async redispatch(@Param() { id }: MongoIdDto) {
|
||||||
|
return this.service.redispatch(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
46
apps/core/src/modules/webhook/webhook.model.ts
Normal file
46
apps/core/src/modules/webhook/webhook.model.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { IsBoolean, IsEnum, IsString, IsUrl } from 'class-validator'
|
||||||
|
|
||||||
|
import { PartialType } from '@nestjs/mapped-types'
|
||||||
|
import { modelOptions, plugin, prop } from '@typegoose/typegoose'
|
||||||
|
|
||||||
|
import { EventScope } from '~/constants/business-event.constant'
|
||||||
|
import { mongooseLeanId } from '~/shared/model/plugins/lean-id'
|
||||||
|
|
||||||
|
@modelOptions({
|
||||||
|
schemaOptions: {
|
||||||
|
timestamps: {
|
||||||
|
createdAt: 'timestamp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
customName: 'webhooks',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@plugin(mongooseLeanId)
|
||||||
|
export class WebhookModel {
|
||||||
|
@prop({ required: true })
|
||||||
|
@IsUrl({
|
||||||
|
require_protocol: true,
|
||||||
|
})
|
||||||
|
payloadUrl: string
|
||||||
|
|
||||||
|
@prop({ required: true, type: String })
|
||||||
|
@IsString({ each: true })
|
||||||
|
events: string[]
|
||||||
|
|
||||||
|
@prop({ required: true })
|
||||||
|
@IsBoolean()
|
||||||
|
enabled: boolean
|
||||||
|
|
||||||
|
id: string
|
||||||
|
|
||||||
|
@prop({ required: true, select: false })
|
||||||
|
@IsString()
|
||||||
|
secret: string
|
||||||
|
|
||||||
|
@prop({ enum: EventScope })
|
||||||
|
@IsEnum(EventScope)
|
||||||
|
scope: EventScope
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebhookDtoPartial extends PartialType(WebhookModel) {}
|
||||||
10
apps/core/src/modules/webhook/webhook.module.ts
Normal file
10
apps/core/src/modules/webhook/webhook.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { WebhookController } from './webhook.controller'
|
||||||
|
import { WebhookService } from './webhook.service'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [WebhookController],
|
||||||
|
providers: [WebhookService],
|
||||||
|
})
|
||||||
|
export class WebhookModule {}
|
||||||
200
apps/core/src/modules/webhook/webhook.service.ts
Normal file
200
apps/core/src/modules/webhook/webhook.service.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import { createHmac } from 'crypto'
|
||||||
|
import type { OnModuleDestroy, OnModuleInit } from '@nestjs/common'
|
||||||
|
import type { IEventManagerHandlerDisposer } from '~/processors/helper/helper.event.service'
|
||||||
|
import type { PagerDto } from '~/shared/dto/pager.dto'
|
||||||
|
|
||||||
|
import { BadRequestException, Injectable } from '@nestjs/common'
|
||||||
|
import { ReturnModelType } from '@typegoose/typegoose'
|
||||||
|
|
||||||
|
import { BusinessEvents, EventScope } from '~/constants/business-event.constant'
|
||||||
|
import { EventManagerService } from '~/processors/helper/helper.event.service'
|
||||||
|
import { HttpService } from '~/processors/helper/helper.http.service'
|
||||||
|
import { InjectModel } from '~/transformers/model.transformer'
|
||||||
|
|
||||||
|
import { WebhookEventModel } from './webhook-event.model'
|
||||||
|
import { WebhookModel } from './webhook.model'
|
||||||
|
|
||||||
|
const ACCEPT_EVENTS = new Set(Object.values(BusinessEvents))
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WebhookService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(WebhookModel)
|
||||||
|
private readonly webhookModel: ReturnModelType<typeof WebhookModel>,
|
||||||
|
@InjectModel(WebhookEventModel)
|
||||||
|
private readonly webhookEventModel: MongooseModel<WebhookEventModel>,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
private readonly eventService: EventManagerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private eventListenerDisposer: IEventManagerHandlerDisposer
|
||||||
|
onModuleDestroy() {
|
||||||
|
this.eventListenerDisposer()
|
||||||
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.eventListenerDisposer = this.eventService.registerHandler(
|
||||||
|
(type: BusinessEvents, data, scope) => {
|
||||||
|
if (!ACCEPT_EVENTS.has(type)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.sendWebhook(type, data, scope)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
createWebhook(model: WebhookModel) {
|
||||||
|
return this.webhookModel.create(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
transformEvents(events: string[]) {
|
||||||
|
let nextEvents = [] as string[]
|
||||||
|
for (const event of events) {
|
||||||
|
if (event === 'all') {
|
||||||
|
nextEvents = ['all']
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (ACCEPT_EVENTS.has(event as any)) {
|
||||||
|
nextEvents.push(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteWebhook(id: string) {
|
||||||
|
await this.webhookModel.deleteOne({
|
||||||
|
_id: id,
|
||||||
|
})
|
||||||
|
await this.webhookEventModel.deleteMany({
|
||||||
|
hookId: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWebhook(id: string, model: Partial<WebhookModel>) {
|
||||||
|
await this.webhookModel.updateOne(
|
||||||
|
{
|
||||||
|
_id: id,
|
||||||
|
},
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllWebhooks() {
|
||||||
|
return this.webhookModel.find().lean()
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendWebhook(event: string, payload: any, scope: EventScope) {
|
||||||
|
const stringifyPayload = JSON.stringify(payload)
|
||||||
|
const clonedPayload = JSON.parse(stringifyPayload)
|
||||||
|
const enabledWebHooks = await this.webhookModel
|
||||||
|
.find({
|
||||||
|
events: {
|
||||||
|
$in: [event, 'all'],
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
.select('+secret')
|
||||||
|
.lean()
|
||||||
|
|
||||||
|
const scopedWebhooks = enabledWebHooks.filter((webhook) => {
|
||||||
|
return (webhook.scope || EventScope.ALL & scope) === scope
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
scopedWebhooks.map(async (webhook) => {
|
||||||
|
const headers = {
|
||||||
|
'X-Webhook-Signature': generateSha1Signature(
|
||||||
|
webhook.secret,
|
||||||
|
stringifyPayload,
|
||||||
|
),
|
||||||
|
'X-Webhook-Event': event,
|
||||||
|
'X-Webhook-Id': webhook.id,
|
||||||
|
'X-Webhook-Timestamp': Date.now().toString(),
|
||||||
|
'X-Webhook-Signature256': generateSha256Signature(
|
||||||
|
webhook.secret,
|
||||||
|
stringifyPayload,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
const webhookEvent = await this.webhookEventModel.create({
|
||||||
|
event,
|
||||||
|
headers,
|
||||||
|
success: false,
|
||||||
|
payload: stringifyPayload,
|
||||||
|
hookId: webhook.id,
|
||||||
|
response: null,
|
||||||
|
})
|
||||||
|
this.httpService.axiosRef
|
||||||
|
.post(webhook.payloadUrl, clonedPayload, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
webhookEvent.response = JSON.stringify({
|
||||||
|
headers: response.headers,
|
||||||
|
data: response.data,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
webhookEvent.status = response.status
|
||||||
|
webhookEvent.success = true
|
||||||
|
await webhookEvent.save()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!err.response) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
webhookEvent.response = JSON.stringify({
|
||||||
|
headers: err.response.headers,
|
||||||
|
data: err.response.data,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
webhookEvent.status = err.response.status
|
||||||
|
webhookEvent.success = false
|
||||||
|
webhookEvent.save()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async redispatch(id: string) {
|
||||||
|
const record = await this.webhookEventModel.findById(id)
|
||||||
|
if (!record) {
|
||||||
|
throw new BadRequestException('Webhook event not found')
|
||||||
|
}
|
||||||
|
const hook = await this.webhookModel.findById(record.hookId)
|
||||||
|
if (!hook) {
|
||||||
|
throw new BadRequestException('Webhook not found')
|
||||||
|
}
|
||||||
|
const scope = hook.scope
|
||||||
|
|
||||||
|
await this.sendWebhook(record.event, JSON.parse(record.payload), scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsByHookId(hookId: string, query: PagerDto) {
|
||||||
|
const { page, size } = query
|
||||||
|
const skip = (page - 1) * size
|
||||||
|
|
||||||
|
return await this.webhookEventModel.paginate(
|
||||||
|
{
|
||||||
|
hookId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
limit: size,
|
||||||
|
skip,
|
||||||
|
sort: {
|
||||||
|
timestamp: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSha1Signature(secret: string, payload: string): string {
|
||||||
|
const hmac = createHmac('sha1', secret)
|
||||||
|
hmac.update(payload)
|
||||||
|
return hmac.digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSha256Signature(secret: string, payload: string): string {
|
||||||
|
const hmac = createHmac('sha256', secret)
|
||||||
|
hmac.update(payload)
|
||||||
|
return hmac.digest('hex')
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ import { SubscribeModel } from '~/modules/subscribe/subscribe.model'
|
|||||||
import { SyncUpdateModel } from '~/modules/sync-update/sync-update.model'
|
import { SyncUpdateModel } from '~/modules/sync-update/sync-update.model'
|
||||||
import { TopicModel } from '~/modules/topic/topic.model'
|
import { TopicModel } from '~/modules/topic/topic.model'
|
||||||
import { UserModel } from '~/modules/user/user.model'
|
import { UserModel } from '~/modules/user/user.model'
|
||||||
|
import { WebhookEventModel } from '~/modules/webhook/webhook-event.model'
|
||||||
|
import { WebhookModel } from '~/modules/webhook/webhook.model'
|
||||||
import { getProviderByTypegooseClass } from '~/transformers/model.transformer'
|
import { getProviderByTypegooseClass } from '~/transformers/model.transformer'
|
||||||
|
|
||||||
export const databaseModels = [
|
export const databaseModels = [
|
||||||
@@ -39,4 +41,7 @@ export const databaseModels = [
|
|||||||
SubscribeModel,
|
SubscribeModel,
|
||||||
UserModel,
|
UserModel,
|
||||||
SyncUpdateModel,
|
SyncUpdateModel,
|
||||||
|
|
||||||
|
WebhookModel,
|
||||||
|
WebhookEventModel,
|
||||||
].map((model) => getProviderByTypegooseClass(model))
|
].map((model) => getProviderByTypegooseClass(model))
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ export type EventManagerOptions = {
|
|||||||
nextTick?: boolean
|
nextTick?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventHandler = {
|
||||||
|
event: string
|
||||||
|
payload: any
|
||||||
|
scope: EventScope
|
||||||
|
}
|
||||||
|
|
||||||
export type IEventManagerHandlerDisposer = () => void
|
export type IEventManagerHandlerDisposer = () => void
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -40,8 +46,6 @@ export class EventManagerService {
|
|||||||
this.logger = new Logger(EventManagerService.name)
|
this.logger = new Logger(EventManagerService.name)
|
||||||
|
|
||||||
this.listenSystemEvents()
|
this.listenSystemEvents()
|
||||||
|
|
||||||
this.logger.debug('EventManagerService is ready')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapScopeToInstance: Record<
|
private mapScopeToInstance: Record<
|
||||||
@@ -59,10 +63,15 @@ export class EventManagerService {
|
|||||||
this.emitter2,
|
this.emitter2,
|
||||||
this.systemGateway,
|
this.systemGateway,
|
||||||
],
|
],
|
||||||
[EventScope.TO_VISITOR]: [this.webGateway],
|
[EventScope.TO_VISITOR]: [this.webGateway, this.emitter2],
|
||||||
[EventScope.TO_ADMIN]: [this.adminGateway],
|
[EventScope.TO_ADMIN]: [this.adminGateway, this.emitter2],
|
||||||
[EventScope.TO_SYSTEM]: [this.emitter2, this.systemGateway],
|
[EventScope.TO_SYSTEM]: [this.emitter2, this.systemGateway],
|
||||||
[EventScope.TO_VISITOR_ADMIN]: [this.webGateway, this.adminGateway],
|
[EventScope.TO_VISITOR_ADMIN]: [
|
||||||
|
this.webGateway,
|
||||||
|
this.adminGateway,
|
||||||
|
this.emitter2,
|
||||||
|
],
|
||||||
|
|
||||||
[EventScope.TO_SYSTEM_VISITOR]: [
|
[EventScope.TO_SYSTEM_VISITOR]: [
|
||||||
this.emitter2,
|
this.emitter2,
|
||||||
this.webGateway,
|
this.webGateway,
|
||||||
@@ -109,6 +118,7 @@ export class EventManagerService {
|
|||||||
return instance.emit(this.#key, {
|
return instance.emit(this.#key, {
|
||||||
event,
|
event,
|
||||||
payload,
|
payload,
|
||||||
|
scope,
|
||||||
})
|
})
|
||||||
} else if (instance instanceof BroadcastBaseGateway) {
|
} else if (instance instanceof BroadcastBaseGateway) {
|
||||||
return instance.broadcast(event as any, data)
|
return instance.broadcast(event as any, data)
|
||||||
@@ -151,13 +161,13 @@ export class EventManagerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#handlers: ((type: string, data: any) => void)[] = []
|
#handlers: ((type: string, data: any, scope: EventScope) => void)[] = []
|
||||||
|
|
||||||
registerHandler(
|
registerHandler(
|
||||||
handler: (type: EventBusEvents, data: any) => void,
|
handler: (type: EventBusEvents, data: any, scope: EventScope) => void,
|
||||||
): IEventManagerHandlerDisposer
|
): IEventManagerHandlerDisposer
|
||||||
registerHandler(
|
registerHandler(
|
||||||
handler: (type: BusinessEvents, data: any) => void,
|
handler: (type: BusinessEvents, data: any, scope: EventScope) => void,
|
||||||
): IEventManagerHandlerDisposer
|
): IEventManagerHandlerDisposer
|
||||||
registerHandler(handler: Function) {
|
registerHandler(handler: Function) {
|
||||||
this.#handlers.push(handler as any)
|
this.#handlers.push(handler as any)
|
||||||
@@ -169,11 +179,13 @@ export class EventManagerService {
|
|||||||
|
|
||||||
private listenSystemEvents() {
|
private listenSystemEvents() {
|
||||||
this.emitter2.on(this.#key, (data) => {
|
this.emitter2.on(this.#key, (data) => {
|
||||||
const { event, payload } = data
|
const { event, payload, scope } = data
|
||||||
console.debug(`Received event: [${event}]`, payload)
|
console.debug(`[${scope}]: Received event: [${event}]`, payload)
|
||||||
|
|
||||||
// emit current event directly
|
// emit current event directly
|
||||||
this.emitter2.emit(event, payload)
|
this.emitter2.emit(event, payload)
|
||||||
this.#handlers.forEach((handler) => handler(event, payload))
|
|
||||||
|
this.#handlers.forEach((handler) => handler(event, payload, scope))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getRedisKey } from '~/utils'
|
|||||||
import { version } from '../../../package.json'
|
import { version } from '../../../package.json'
|
||||||
import { CacheService } from '../redis/cache.service'
|
import { CacheService } from '../redis/cache.service'
|
||||||
|
|
||||||
|
const DEFAULT_UA = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 MX-Space/${version}`
|
||||||
declare module 'axios' {
|
declare module 'axios' {
|
||||||
interface AxiosRequestConfig {
|
interface AxiosRequestConfig {
|
||||||
__requestStartedAt?: number
|
__requestStartedAt?: number
|
||||||
@@ -33,7 +34,7 @@ export class HttpService {
|
|||||||
axios.create({
|
axios.create({
|
||||||
...AXIOS_CONFIG,
|
...AXIOS_CONFIG,
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 MX-Space/${version}`,
|
'user-agent': DEFAULT_UA,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -49,7 +50,7 @@ export class HttpService {
|
|||||||
private axiosDefaultConfig: AxiosRequestConfig<any> = {
|
private axiosDefaultConfig: AxiosRequestConfig<any> = {
|
||||||
...AXIOS_CONFIG,
|
...AXIOS_CONFIG,
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36 MX-Space/${version}`,
|
'user-agent': DEFAULT_UA,
|
||||||
},
|
},
|
||||||
'axios-retry': {
|
'axios-retry': {
|
||||||
retries: 3,
|
retries: 3,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class WriteBaseModel extends BaseCommentIndexModel {
|
|||||||
@IsDate()
|
@IsDate()
|
||||||
@Transform(({ value }) => (value ? new Date(value) : void 0))
|
@Transform(({ value }) => (value ? new Date(value) : void 0))
|
||||||
@prop()
|
@prop()
|
||||||
created?: Date
|
declare created?: Date
|
||||||
|
|
||||||
@prop(
|
@prop(
|
||||||
{
|
{
|
||||||
|
|||||||
6
packages/webhook/globals.d.ts
vendored
Normal file
6
packages/webhook/globals.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare global {
|
||||||
|
interface JSON {
|
||||||
|
safeParse: typeof JSON.parse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export {}
|
||||||
15
packages/webhook/index.js
Normal file
15
packages/webhook/index.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const { createHandler, BusinessEvents } = require('./dist/index')
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
const handler = createHandler({ secret: 'test' })
|
||||||
|
app.post('/webhook', (req, res) => {
|
||||||
|
handler(req, res)
|
||||||
|
})
|
||||||
|
handler.emitter.on(BusinessEvents.POST_UPDATE, (event) => {
|
||||||
|
console.log(event)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen('13333')
|
||||||
37
packages/webhook/package.json
Normal file
37
packages/webhook/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@mx-space/webhook",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"devDependencies": {
|
||||||
|
"express": "4.18.2"
|
||||||
|
},
|
||||||
|
"main": "dist/index.cjs",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup && node scripts/post-build.js"
|
||||||
|
},
|
||||||
|
"bump": {
|
||||||
|
"before": [
|
||||||
|
"git pull --rebase",
|
||||||
|
"pnpm i",
|
||||||
|
"npm run package"
|
||||||
|
],
|
||||||
|
"after": [
|
||||||
|
"npm publish --access=public"
|
||||||
|
],
|
||||||
|
"tag": false,
|
||||||
|
"commit_message": "chore(release): bump @mx-space/webhook to v${NEW_VERSION}"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"./dist/*": {
|
||||||
|
"import": "./dist/*.js",
|
||||||
|
"require": "./dist/*.cjs"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/webhook/scripts/post-build.js
Normal file
33
packages/webhook/scripts/post-build.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// Function to replace content in a file
|
||||||
|
function replaceContent(filePath, searchValue, replaceValue) {
|
||||||
|
fs.readFile(filePath, 'utf8', (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`Error reading file from disk: ${err}`)
|
||||||
|
} else {
|
||||||
|
// Replace the text
|
||||||
|
const result = data.replace(searchValue, replaceValue)
|
||||||
|
|
||||||
|
// Write the file back
|
||||||
|
fs.writeFile(filePath, result, 'utf8', (writeErr) => {
|
||||||
|
if (writeErr) console.error(`Error writing file: ${writeErr}`)
|
||||||
|
else console.log(`Updated file: ${filePath}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// File paths
|
||||||
|
const files = ['dist/index.d.ts', 'dist/index.d.mts']
|
||||||
|
|
||||||
|
// The string to be replaced and its replacement
|
||||||
|
const searchValue = "import { Ref } from '@typegoose/typegoose';"
|
||||||
|
const replaceValue = 'type Ref <T> = unknown;'
|
||||||
|
|
||||||
|
// Apply the replacement for each file
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = path.join(__dirname, '../', file)
|
||||||
|
replaceContent(filePath, searchValue, replaceValue)
|
||||||
|
})
|
||||||
4
packages/webhook/src/event.enum.ts
Normal file
4
packages/webhook/src/event.enum.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export {
|
||||||
|
BusinessEvents,
|
||||||
|
EventScope,
|
||||||
|
} from '@core/constants/business-event.constant'
|
||||||
103
packages/webhook/src/handler.ts
Normal file
103
packages/webhook/src/handler.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import assert from 'assert'
|
||||||
|
import { createHmac, timingSafeEqual } from 'crypto'
|
||||||
|
import { EventEmitter } from 'events'
|
||||||
|
import type { IncomingMessage, ServerResponse } from 'http'
|
||||||
|
import type { BusinessEvents } from './event.enum'
|
||||||
|
import type { ExtendedEventEmitter } from './types'
|
||||||
|
|
||||||
|
interface CreateHandlerOptions {
|
||||||
|
secret: string
|
||||||
|
events?: 'all' | BusinessEvents[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler = {
|
||||||
|
(
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
callback: (err: Error) => void,
|
||||||
|
): void
|
||||||
|
} & {
|
||||||
|
emitter: ExtendedEventEmitter
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createHandler = (options: CreateHandlerOptions): Handler => {
|
||||||
|
const { secret } = options
|
||||||
|
|
||||||
|
const handler: Handler = async function (req, res) {
|
||||||
|
const signature = req.headers['x-webhook-signature']
|
||||||
|
assert(typeof signature === 'string', 'X-Webhook-Signature must be string')
|
||||||
|
const event = req.headers['x-webhook-event']
|
||||||
|
const signature256 = req.headers['x-webhook-signature256']
|
||||||
|
assert(
|
||||||
|
typeof signature256 === 'string',
|
||||||
|
'X-Webhook-Signature256 must be string',
|
||||||
|
)
|
||||||
|
|
||||||
|
const obj = (req as any).body || (await parseJSONFromRequest(req))
|
||||||
|
const stringifyPayload = JSON.stringify(obj)
|
||||||
|
const isValid =
|
||||||
|
verifyWebhook(secret, stringifyPayload, signature256 as string) &&
|
||||||
|
verifyWebhookSha1(secret, stringifyPayload, signature as string)
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
handler.emitter.emit(event as BusinessEvents, obj)
|
||||||
|
res.statusCode = 200
|
||||||
|
res.end()
|
||||||
|
} else {
|
||||||
|
console.error('revice a invalidate webhook payload', req.headers)
|
||||||
|
handler.emitter.emit('error', new Error('invalidate signature'))
|
||||||
|
|
||||||
|
res.statusCode = 400
|
||||||
|
res.end('invalidate signature')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.emitter = new EventEmitter()
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJSONFromRequest(req: IncomingMessage) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let body = ''
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
body += chunk
|
||||||
|
})
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(body))
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyWebhook(
|
||||||
|
secret: string,
|
||||||
|
payload: string,
|
||||||
|
receivedSignature: string,
|
||||||
|
): boolean {
|
||||||
|
const hmac = createHmac('sha256', secret)
|
||||||
|
hmac.update(payload)
|
||||||
|
|
||||||
|
// 安全地比较两个签名,以防止时间攻击
|
||||||
|
return timingSafeEqual(
|
||||||
|
Buffer.from(receivedSignature),
|
||||||
|
Buffer.from(hmac.digest('hex')),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyWebhookSha1(
|
||||||
|
secret: string,
|
||||||
|
payload: string,
|
||||||
|
receivedSignature: string,
|
||||||
|
): boolean {
|
||||||
|
const hmac = createHmac('sha1', secret)
|
||||||
|
hmac.update(payload)
|
||||||
|
|
||||||
|
// 安全地比较两个签名,以防止时间攻击
|
||||||
|
return timingSafeEqual(
|
||||||
|
Buffer.from(receivedSignature),
|
||||||
|
Buffer.from(hmac.digest('hex')),
|
||||||
|
)
|
||||||
|
}
|
||||||
3
packages/webhook/src/index.ts
Normal file
3
packages/webhook/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './event.enum'
|
||||||
|
export * from './handler'
|
||||||
|
export * from './types'
|
||||||
55
packages/webhook/src/types.ts
Normal file
55
packages/webhook/src/types.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { CommentModel } from '@core/modules/comment/comment.model'
|
||||||
|
import type { LinkModel } from '@core/modules/link/link.model'
|
||||||
|
import type { NoteModel } from '@core/modules/note/note.model'
|
||||||
|
import type { NormalizedNote } from '@core/modules/note/note.type'
|
||||||
|
import type { PageModel } from '@core/modules/page/page.model'
|
||||||
|
import type { PostModel } from '@core/modules/post/post.model'
|
||||||
|
import type { NormalizedPost } from '@core/modules/post/post.type'
|
||||||
|
import type { RecentlyModel } from '@core/modules/recently/recently.model'
|
||||||
|
import type { SayModel } from '@core/modules/say/say.model'
|
||||||
|
import type { EventEmitter } from 'events'
|
||||||
|
import type { BusinessEvents } from './event.enum'
|
||||||
|
|
||||||
|
export interface ExtendedEventEmitter extends EventEmitter {
|
||||||
|
on<T extends BusinessEvents>(
|
||||||
|
event: T,
|
||||||
|
listener: (
|
||||||
|
data: EventPayloadMapping[Extract<T, keyof EventPayloadMapping>],
|
||||||
|
) => void,
|
||||||
|
): this
|
||||||
|
}
|
||||||
|
export type Id = string
|
||||||
|
export type PayloadOnlyId = { data: Id }
|
||||||
|
export interface EventPayloadMapping {
|
||||||
|
[BusinessEvents.POST_CREATE]: NormalizedPost
|
||||||
|
[BusinessEvents.POST_UPDATE]: NormalizedPost
|
||||||
|
[BusinessEvents.POST_DELETE]: PayloadOnlyId
|
||||||
|
|
||||||
|
[BusinessEvents.NOTE_CREATE]: NormalizedNote
|
||||||
|
[BusinessEvents.NOTE_UPDATE]: NormalizedNote
|
||||||
|
[BusinessEvents.NOTE_DELETE]: PayloadOnlyId
|
||||||
|
|
||||||
|
[BusinessEvents.PAGE_CREATE]: PageModel
|
||||||
|
[BusinessEvents.PAGE_UPDATE]: PageModel
|
||||||
|
[BusinessEvents.PAGE_DELETE]: PayloadOnlyId
|
||||||
|
|
||||||
|
[BusinessEvents.SAY_CREATE]: SayModel
|
||||||
|
[BusinessEvents.RECENTLY_CREATE]: RecentlyModel
|
||||||
|
|
||||||
|
[BusinessEvents.ACTIVITY_LIKE]: IActivityLike
|
||||||
|
|
||||||
|
[BusinessEvents.LINK_APPLY]: LinkModel
|
||||||
|
|
||||||
|
[BusinessEvents.COMMENT_CREATE]: Omit<CommentModel, 'ref'> & {
|
||||||
|
ref: Id | PostModel | PageModel | NoteModel | RecentlyModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface IActivityLike {
|
||||||
|
id: string
|
||||||
|
type: 'Note' | 'Post'
|
||||||
|
created: string
|
||||||
|
ref: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
}
|
||||||
30
packages/webhook/tsconfig.json
Normal file
30
packages/webhook/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"paths": {
|
||||||
|
"~/*": [
|
||||||
|
"../../apps/core/src/*"
|
||||||
|
],
|
||||||
|
"@core/*": [
|
||||||
|
"../../apps/core/src/*"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
9
packages/webhook/tsup.config.ts
Normal file
9
packages/webhook/tsup.config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'tsup'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
clean: true,
|
||||||
|
target: 'es2020',
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
dts: true,
|
||||||
|
format: ['cjs', 'esm'],
|
||||||
|
})
|
||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -466,6 +466,12 @@ importers:
|
|||||||
specifier: 7.2.3
|
specifier: 7.2.3
|
||||||
version: 7.2.3
|
version: 7.2.3
|
||||||
|
|
||||||
|
packages/webhook:
|
||||||
|
devDependencies:
|
||||||
|
express:
|
||||||
|
specifier: 4.18.2
|
||||||
|
version: 4.18.2
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
/@aashutoshrathi/word-wrap@1.2.6:
|
/@aashutoshrathi/word-wrap@1.2.6:
|
||||||
|
|||||||
Reference in New Issue
Block a user