From 164202672d0b3f5fb03bd0cd275b4050654873c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AF=BB?= Date: Thu, 9 Jun 2022 17:17:02 +0800 Subject: [PATCH] feat: bark support (#563) --- src/constants/event-bus.constant.ts | 2 +- src/modules/configs/configs.default.ts | 6 ++ src/modules/configs/configs.dto.ts | 25 +++++++ src/modules/configs/configs.interface.ts | 5 ++ src/modules/configs/configs.service.ts | 23 ++++-- .../gateway/admin/events.gateway.ts | 2 +- src/processors/helper/helper.bark.service.ts | 73 +++++++++++++++++++ src/processors/helper/helper.event.service.ts | 3 +- src/processors/helper/helper.http.service.ts | 2 +- src/processors/helper/helper.module.ts | 16 ++-- .../modules/configs/configs.service.spec.ts | 17 +++-- 11 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 src/processors/helper/helper.bark.service.ts diff --git a/src/constants/event-bus.constant.ts b/src/constants/event-bus.constant.ts index b1b2c796..2600dbe1 100644 --- a/src/constants/event-bus.constant.ts +++ b/src/constants/event-bus.constant.ts @@ -2,7 +2,7 @@ export enum EventBusEvents { EmailInit = 'email.init', PushSearch = 'search.push', TokenExpired = 'token.expired', - CleanAggregateCache = 'cache.aggregate', SystemException = 'system.exception', + ConfigChanged = 'config.changed', } diff --git a/src/modules/configs/configs.default.ts b/src/modules/configs/configs.default.ts index 616e7952..d5d61f5e 100644 --- a/src/modules/configs/configs.default.ts +++ b/src/modules/configs/configs.default.ts @@ -32,6 +32,12 @@ export const generateDefaultConfig: () => IConfig = () => ({ recordIpLocation: true, spamKeywords: [], }, + barkOptions: { + enable: false, + key: '', + serverUrl: 'https://api.day.app', + enableComment: true, + }, friendLinkOptions: { allowApply: true }, backupOptions: { enable: isInDemoMode ? false : true, diff --git a/src/modules/configs/configs.dto.ts b/src/modules/configs/configs.dto.ts index 3ac822df..31ec63c8 100644 --- a/src/modules/configs/configs.dto.ts +++ b/src/modules/configs/configs.dto.ts @@ -288,3 +288,28 @@ export class TextOptionsDto { @JSONSchemaToggleField('开启文本宏替换') macros: boolean } + +@JSONSchema({ title: 'Bark 通知设定' }) +export class BarkOptionsDto { + @IsBoolean() + @IsOptional() + @JSONSchemaToggleField('开启 Bark 通知') + enable: boolean + + @IsString() + @IsOptional() + @JSONSchemaPlainField('设备 Key') + key: string + + @IsUrl() + @IsOptional() + @JSONSchemaPlainField('服务器 URL', { + description: '如果不填写, 则使用默认的服务器, https://day.app/', + }) + serverUrl: string + + @IsOptional() + @IsBoolean() + @JSONSchemaToggleField('开启评论通知') + enableComment?: boolean +} diff --git a/src/modules/configs/configs.interface.ts b/src/modules/configs/configs.interface.ts index f72c4dd9..64d29ed7 100644 --- a/src/modules/configs/configs.interface.ts +++ b/src/modules/configs/configs.interface.ts @@ -7,6 +7,7 @@ import { AlgoliaSearchOptionsDto, BackupOptionsDto, BaiduSearchOptionsDto, + BarkOptionsDto, CommentOptionsDto, FriendLinkOptionsDto, MailOptionsDto, @@ -45,6 +46,10 @@ export abstract class IConfig { @ValidateNested() commentOptions: Required + @Type(() => BarkOptionsDto) + @ValidateNested() + barkOptions: Required + @Type(() => FriendLinkOptionsDto) @ValidateNested() friendLinkOptions: Required diff --git a/src/modules/configs/configs.service.ts b/src/modules/configs/configs.service.ts index d7c8f40a..d1284b37 100644 --- a/src/modules/configs/configs.service.ts +++ b/src/modules/configs/configs.service.ts @@ -11,15 +11,16 @@ import { Logger, ValidationPipe, } from '@nestjs/common' -import { EventEmitter2 } from '@nestjs/event-emitter' import { DocumentType, ReturnModelType } from '@typegoose/typegoose' import { BeAnObject } from '@typegoose/typegoose/lib/types' +import { EventScope } from '~/constants/business-event.constant' import { RedisKeys } from '~/constants/cache.constant' import { EventBusEvents } from '~/constants/event-bus.constant' import { CacheService } from '~/processors/cache/cache.service' +import { EventManagerService } from '~/processors/helper/helper.event.service' import { InjectModel } from '~/transformers/model.transformer' -import { banInDemo, sleep } from '~/utils' +import { sleep } from '~/utils' import { getRedisKey } from '~/utils/redis.util' import * as optionDtos from '../configs/configs.dto' @@ -53,7 +54,7 @@ export class ConfigsService { private readonly userService: UserService, private readonly redis: CacheService, - private readonly eventEmitter: EventEmitter2, + private readonly eventManager: EventManagerService, ) { this.configInit().then(() => { this.logger.log('Config 已经加载完毕!') @@ -165,6 +166,13 @@ export class ConfigsService { const mergedFullConfig = Object.assign({}, config, { [key]: newData }) await this.setConfig(mergedFullConfig) + this.eventManager.emit( + EventBusEvents.ConfigChanged, + { ...newData }, + { + scope: EventScope.TO_SYSTEM, + }, + ) return newData } @@ -178,7 +186,6 @@ export class ConfigsService { key: T, value: Partial, ) { - banInDemo() value = camelcaseKeys(value, { deep: true }) as any switch (key) { @@ -189,7 +196,9 @@ export class ConfigsService { ) if (option.enable) { if (cluster.isPrimary) { - this.eventEmitter.emit(EventBusEvents.EmailInit) + this.eventManager.emit(EventBusEvents.EmailInit, null, { + scope: EventScope.TO_SYSTEM, + }) } else { this.redis.publish(EventBusEvents.EmailInit, '') } @@ -204,7 +213,9 @@ export class ConfigsService { this.validWithDto(AlgoliaSearchOptionsDto, value), ) if (option.enable) { - this.eventEmitter.emit(EventBusEvents.PushSearch) + this.eventManager.emit(EventBusEvents.PushSearch, null, { + scope: EventScope.TO_SYSTEM, + }) } return option } diff --git a/src/processors/gateway/admin/events.gateway.ts b/src/processors/gateway/admin/events.gateway.ts index ecfe0002..90219a0c 100644 --- a/src/processors/gateway/admin/events.gateway.ts +++ b/src/processors/gateway/admin/events.gateway.ts @@ -11,7 +11,6 @@ import { } from '@nestjs/websockets' import { LOG_DIR } from '~/constants/path.constant' -import { getTodayLogFilePath } from '~/global/consola.global' import { CacheService } from '~/processors/cache/cache.service' import { BusinessEvents } from '../../../constants/business-event.constant' @@ -47,6 +46,7 @@ export class AdminEventsGateway this.subscribeSocketToHandlerMap.set(client, handler) if (prevLog) { + const { getTodayLogFilePath } = await import('~/global/consola.global') const stream = fs .createReadStream(resolve(LOG_DIR, getTodayLogFilePath()), { encoding: 'utf-8', diff --git a/src/processors/helper/helper.bark.service.ts b/src/processors/helper/helper.bark.service.ts new file mode 100644 index 00000000..a31158ae --- /dev/null +++ b/src/processors/helper/helper.bark.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' + +import { BusinessEvents } from '~/constants/business-event.constant' +import { CommentModel } from '~/modules/comment/comment.model' +import { ConfigsService } from '~/modules/configs/configs.service' + +import { HttpService } from './helper.http.service' + +export type BarkPushOptions = { + title: string + body: string + category?: string + /** + * An url to the icon, available only on iOS 15 or later + */ + icon?: string + group?: string + url?: string + /** + * Value from here + */ + sound?: string + level?: 'active' | 'timeSensitive' | 'passive' +} + +@Injectable() +export class BarkPushService { + constructor( + private readonly httpService: HttpService, + private readonly config: ConfigsService, + ) {} + + // push comment + @OnEvent(BusinessEvents.COMMENT_CREATE) + async pushCommentEvent(comment: CommentModel) { + const { enable } = await this.config.get('barkOptions') + if (!enable) { + return + } + const master = await this.config.getMaster() + if (comment.author == master.name && comment.author == master.username) { + return + } + const { adminUrl } = await this.config.get('url') + + await this.push({ + title: '收到一条新评论', + body: `${comment.author} 评论了你的文章: ${comment.text}`, + icon: comment.avatar, + url: `${adminUrl}#/comments`, + }) + } + + async push(options: BarkPushOptions) { + const { key, serverUrl = 'https://day.app' } = await this.config.get( + 'barkOptions', + ) + const { title: siteTitle } = await this.config.get('seo') + if (!key) { + throw new Error('Bark key is not configured') + } + const { title, ...rest } = options + const response = await this.httpService.axiosRef.post(`${serverUrl}/push`, { + device_key: key, + title: `[${siteTitle}] ${title}`, + category: siteTitle, + group: siteTitle, + ...rest, + }) + return response.data + } +} diff --git a/src/processors/helper/helper.event.service.ts b/src/processors/helper/helper.event.service.ts index f9d0afb1..a9723dab 100644 --- a/src/processors/helper/helper.event.service.ts +++ b/src/processors/helper/helper.event.service.ts @@ -82,7 +82,7 @@ export class EventManagerService { ): Promise emit( event: EventBusEvents, - data: any, + data?: any, options?: EventManagerOptions, ): Promise async emit(event: string, data: any = null, _options?: EventManagerOptions) { @@ -103,6 +103,7 @@ export class EventManagerService { if (instance instanceof EventEmitter2) { const isObjectLike = typeof data === 'object' && data !== null const payload = isObjectLike ? data : { data } + instance.emit(event, payload) return instance.emit(this.#key, { event, payload, diff --git a/src/processors/helper/helper.http.service.ts b/src/processors/helper/helper.http.service.ts index adcead6f..3bce4fa4 100644 --- a/src/processors/helper/helper.http.service.ts +++ b/src/processors/helper/helper.http.service.ts @@ -27,7 +27,7 @@ export class HttpService { private logger: Logger constructor(private readonly cacheService: CacheService) { this.logger = new Logger(HttpService.name) - this.http = this.bindDebugVerboseInterceptor( + this.http = this.bindInterceptors( axios.create({ ...AXIOS_CONFIG, headers: { diff --git a/src/processors/helper/helper.module.ts b/src/processors/helper/helper.module.ts index 3ef1f23d..bca2748b 100644 --- a/src/processors/helper/helper.module.ts +++ b/src/processors/helper/helper.module.ts @@ -11,6 +11,7 @@ import { PostModule } from '~/modules/post/post.module' import { SearchModule } from '~/modules/search/search.module' import { AssetService } from './helper.asset.service' +import { BarkPushService } from './helper.bark.service' import { CountingService } from './helper.counting.service' import { CronService } from './helper.cron.service' import { EmailService } from './helper.email.service' @@ -22,16 +23,17 @@ import { TaskQueueService } from './helper.tq.service' import { UploadService } from './helper.upload.service' const providers: Provider[] = [ - HttpService, - EmailService, - ImageService, - CronService, - CountingService, - UploadService, AssetService, - TaskQueueService, + BarkPushService, + CountingService, + CronService, + EmailService, EventManagerService, + HttpService, + ImageService, + TaskQueueService, TextMacroService, + UploadService, ] @Module({ diff --git a/test/src/modules/configs/configs.service.spec.ts b/test/src/modules/configs/configs.service.spec.ts index d53d7859..4ab75ead 100644 --- a/test/src/modules/configs/configs.service.spec.ts +++ b/test/src/modules/configs/configs.service.spec.ts @@ -2,7 +2,6 @@ import { dbHelper } from 'test/helper/db-mock.helper' import { MockCacheService, redisHelper } from 'test/helper/redis-mock.helper' import { BadRequestException } from '@nestjs/common' -import { EventEmitter2 } from '@nestjs/event-emitter' import { Test } from '@nestjs/testing' import { getModelForClass } from '@typegoose/typegoose' @@ -11,6 +10,7 @@ import { OptionModel } from '~/modules/configs/configs.model' import { ConfigsService } from '~/modules/configs/configs.service' import { UserService } from '~/modules/user/user.service' import { CacheService } from '~/processors/cache/cache.service' +import { EventManagerService } from '~/processors/helper/helper.event.service' import { getModelToken } from '~/transformers/model.transformer' import { getRedisKey } from '~/utils/redis.util' @@ -44,7 +44,7 @@ describe('Test ConfigsService', () => { provide: CacheService, useValue: redisService$, }, - { provide: EventEmitter2, useValue: { emit: mockEmitFn } }, + { provide: EventManagerService, useValue: { emit: mockEmitFn } }, ], }).compile() @@ -99,34 +99,35 @@ describe('Test ConfigsService', () => { }) it('should emit event if enable email option and update search', async () => { + // + 1 call time because of `config.changed` event await service.patchAndValid('mailOptions', { enable: true }) - expect(mockEmitFn).toBeCalledTimes(1) + expect(mockEmitFn).toBeCalledTimes(3) mockEmitFn.mockClear() await service.patchAndValid('mailOptions', { pass: '*' }) - expect(mockEmitFn).toBeCalledTimes(1) + expect(mockEmitFn).toBeCalledTimes(2) mockEmitFn.mockClear() await service.patchAndValid('mailOptions', { pass: '*', enable: false }) - expect(mockEmitFn).toBeCalledTimes(0) + expect(mockEmitFn).toBeCalledTimes(1) mockEmitFn.mockClear() await service.patchAndValid('algoliaSearchOptions', { enable: true, }) - expect(mockEmitFn).toBeCalledTimes(1) + expect(mockEmitFn).toBeCalledTimes(2) mockEmitFn.mockClear() await service.patchAndValid('algoliaSearchOptions', { indexName: 'x', }) - expect(mockEmitFn).toBeCalledTimes(1) + expect(mockEmitFn).toBeCalledTimes(2) mockEmitFn.mockClear() await service.patchAndValid('algoliaSearchOptions', { enable: false, }) - expect(mockEmitFn).toBeCalledTimes(0) + expect(mockEmitFn).toBeCalledTimes(1) mockEmitFn.mockClear() }) })