feat: bark support (#563)

This commit is contained in:
2022-06-09 17:17:02 +08:00
committed by GitHub
parent 27b85ef396
commit 164202672d
11 changed files with 149 additions and 25 deletions

View File

@@ -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',
}

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import {
AlgoliaSearchOptionsDto,
BackupOptionsDto,
BaiduSearchOptionsDto,
BarkOptionsDto,
CommentOptionsDto,
FriendLinkOptionsDto,
MailOptionsDto,
@@ -45,6 +46,10 @@ export abstract class IConfig {
@ValidateNested()
commentOptions: Required<CommentOptionsDto>
@Type(() => BarkOptionsDto)
@ValidateNested()
barkOptions: Required<BarkOptionsDto>
@Type(() => FriendLinkOptionsDto)
@ValidateNested()
friendLinkOptions: Required<FriendLinkOptionsDto>

View File

@@ -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<IConfig[T]>,
) {
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
}

View File

@@ -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',

View File

@@ -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 <https://github.com/Finb/Bark/tree/master/Sounds>
*/
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
}
}

View File

@@ -82,7 +82,7 @@ export class EventManagerService {
): Promise<void>
emit(
event: EventBusEvents,
data: any,
data?: any,
options?: EventManagerOptions,
): Promise<void>
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,

View File

@@ -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: {

View File

@@ -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<any>[] = [
HttpService,
EmailService,
ImageService,
CronService,
CountingService,
UploadService,
AssetService,
TaskQueueService,
BarkPushService,
CountingService,
CronService,
EmailService,
EventManagerService,
HttpService,
ImageService,
TaskQueueService,
TextMacroService,
UploadService,
]
@Module({

View File

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