From 5e8ddb09f987cd38c946911f99c1c466db863238 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 17:10:43 +0800 Subject: [PATCH 01/12] feat(apps): friends full service --- .../src/friends-service.controller.ts | 7 ++++ .../src/friends-service.module.ts | 10 ++++++ .../src/friends-service.service.ts | 4 +++ apps/friends-service/src/main.ts | 32 +++++++++++++++++++ apps/friends-service/tsconfig.app.json | 9 ++++++ nest-cli.json | 9 ++++++ shared/constants/services.constant.ts | 4 +-- 7 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 apps/friends-service/src/friends-service.controller.ts create mode 100644 apps/friends-service/src/friends-service.module.ts create mode 100644 apps/friends-service/src/friends-service.service.ts create mode 100644 apps/friends-service/src/main.ts create mode 100644 apps/friends-service/tsconfig.app.json diff --git a/apps/friends-service/src/friends-service.controller.ts b/apps/friends-service/src/friends-service.controller.ts new file mode 100644 index 000000000..741edb7d5 --- /dev/null +++ b/apps/friends-service/src/friends-service.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { FriendsService } from './friends-service.service'; + +@Controller() +export class FriendsServiceController { + constructor(private readonly friendsService: FriendsService) {} +} diff --git a/apps/friends-service/src/friends-service.module.ts b/apps/friends-service/src/friends-service.module.ts new file mode 100644 index 000000000..d430f1999 --- /dev/null +++ b/apps/friends-service/src/friends-service.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { FriendsServiceController } from './friends-service.controller'; +import { FriendsService } from './friends-service.service'; + +@Module({ + imports: [], + controllers: [FriendsServiceController], + providers: [FriendsService], +}) +export class FriendsServiceModule {} diff --git a/apps/friends-service/src/friends-service.service.ts b/apps/friends-service/src/friends-service.service.ts new file mode 100644 index 000000000..c3395b6ce --- /dev/null +++ b/apps/friends-service/src/friends-service.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class FriendsService {} diff --git a/apps/friends-service/src/main.ts b/apps/friends-service/src/main.ts new file mode 100644 index 000000000..76a4d3f0e --- /dev/null +++ b/apps/friends-service/src/main.ts @@ -0,0 +1,32 @@ +import { NestFactory } from '@nestjs/core'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { BasicCommer } from '~/shared/commander'; +import { + ServicesEnum, + ServicePorts, +} from '~/shared/constants/services.constant'; +import { registerStdLogger } from '~/shared/global/consola.global'; +import { registerGlobal } from '~/shared/global/index.global'; +import { readEnv, getEnv } from '~/shared/utils/rag-env'; +import { FriendsServiceModule } from './friends-service.module'; + +async function bootstrap() { + registerGlobal(); + registerStdLogger(); + + const argv = BasicCommer.parse().opts(); + readEnv(argv, argv.config); + + const app = await NestFactory.createMicroservice( + FriendsServiceModule, + { + transport: Transport.TCP, + options: { + port: getEnv(ServicesEnum.friends)?.port || ServicePorts.friends, + host: getEnv(ServicesEnum.friends)?.host || undefined, + }, + }, + ); + app.listen(); +} +bootstrap(); diff --git a/apps/friends-service/tsconfig.app.json b/apps/friends-service/tsconfig.app.json new file mode 100644 index 000000000..d97e17a26 --- /dev/null +++ b/apps/friends-service/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/friends-service" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/nest-cli.json b/nest-cli.json index 586d81a8b..cfd6f2abd 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -101,6 +101,15 @@ "compilerOptions": { "tsConfigPath": "apps/comments-service/tsconfig.app.json" } + }, + "friends-service": { + "type": "application", + "root": "apps/friends-service", + "entryFile": "main", + "sourceRoot": "apps/friends-service/src", + "compilerOptions": { + "tsConfigPath": "apps/friends-service/tsconfig.app.json" + } } } } \ No newline at end of file diff --git a/shared/constants/services.constant.ts b/shared/constants/services.constant.ts index ffce603dc..f062b85a0 100644 --- a/shared/constants/services.constant.ts +++ b/shared/constants/services.constant.ts @@ -15,7 +15,7 @@ export enum ServicesEnum { post = 'PAGE_SERVICE', page = 'PAGE_SERVICE', category = 'PAGE_SERVICE', - links = 'LINKS_SERVICE', + friends = 'FRIENDS_SERVICE', comments = 'COMMENTS_SERVICE', config = 'CONFIG_SERVICE', theme = 'THEME_SERVICE', @@ -30,6 +30,6 @@ export enum ServicePorts { post = page, user = 2332, comments = 2333, - links = 2334, + friends = 2334, admin = 2335, } From 8286aab7b794ab01654121f3b3dfd1141d6ebe50 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 17:16:11 +0800 Subject: [PATCH 02/12] feat: models --- apps/friends-service/src/friends.model.ts | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 apps/friends-service/src/friends.model.ts diff --git a/apps/friends-service/src/friends.model.ts b/apps/friends-service/src/friends.model.ts new file mode 100644 index 000000000..892834881 --- /dev/null +++ b/apps/friends-service/src/friends.model.ts @@ -0,0 +1,73 @@ +import { modelOptions, prop } from '@typegoose/typegoose'; +import { IsEmail, IsOptional, IsString, IsUrl } from 'class-validator'; +import { BaseModel } from '~/shared/model/base.model'; + +export enum FriendStatus { + Pending = 0, // 待审核 + Approved = 1, // 已通过 + Spam = 2, // 垃圾友链 + Trash = 3, // 回收站友链 +} + +export enum FeedType { + RSS = 0, + Atom = 1, +} + +@modelOptions({ options: { customName: 'Friends' } }) +export class FriendsModel extends BaseModel { + @prop({ required: true }) + @IsString() + name: string; + + @prop({ required: true }) + @IsString() + link: string; + + @prop() + @IsString() + @IsOptional() + desc?: string; + + @prop() + @IsString() + @IsOptional() + nickname?: string; + + @prop() + @IsUrl() + @IsOptional() + avatar?: string; + + @prop() + @IsEmail() + @IsOptional() + email?: string; + + @prop({ default: FriendStatus.Pending }) + @IsOptional() + status?: FriendStatus; + + @prop() + @IsString() + @IsOptional() + group?: string; + + @prop({ default: false }) + @IsOptional() + autoCheck?: boolean; + + @prop() + @IsUrl() + @IsOptional() + feed?: string; + + @prop({ default: FeedType.RSS }) + @IsOptional() + feedType?: FeedType; + + @prop() + @IsString() + @IsOptional() + feedContents?: string; +} From 7f3963aa02800fa38dd6ed6625fa9fdb6a1a7791 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 20:11:05 +0800 Subject: [PATCH 03/12] feat: service --- .../src/friends-service.module.ts | 4 +- .../src/friends-service.service.ts | 235 ++++++++++++- apps/friends-service/src/friends.model.ts | 40 ++- package.json | 7 +- pnpm-lock.yaml | 312 +++++++++++++++++- shared/constants/echo.constant.ts | 3 + shared/constants/event.constant.ts | 16 + 7 files changed, 596 insertions(+), 21 deletions(-) diff --git a/apps/friends-service/src/friends-service.module.ts b/apps/friends-service/src/friends-service.module.ts index d430f1999..ade71a42f 100644 --- a/apps/friends-service/src/friends-service.module.ts +++ b/apps/friends-service/src/friends-service.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '~/libs/config/src'; +import { HelperModule } from '~/libs/helper/src'; import { FriendsServiceController } from './friends-service.controller'; import { FriendsService } from './friends-service.service'; @Module({ - imports: [], + imports: [HelperModule, ConfigModule], controllers: [FriendsServiceController], providers: [FriendsService], }) diff --git a/apps/friends-service/src/friends-service.service.ts b/apps/friends-service/src/friends-service.service.ts index c3395b6ce..bbb3e97d7 100644 --- a/apps/friends-service/src/friends-service.service.ts +++ b/apps/friends-service/src/friends-service.service.ts @@ -1,4 +1,233 @@ -import { Injectable } from '@nestjs/common'; - +import { HttpStatus, Injectable } from '@nestjs/common'; +import { ReturnModelType } from '@typegoose/typegoose'; +import { v4 } from 'uuid'; +import { HttpService } from '~/libs/helper/src/helper.http.service'; +import { InjectModel } from '~/shared/transformers/model.transformer'; +import { FriendsModel, FriendStatus } from './friends.model'; +import { JSDOM } from 'jsdom'; +import { ConfigService } from '~/libs/config/src'; +import { RpcException } from '@nestjs/microservices'; +import { ExceptionMessage } from '~/shared/constants/echo.constant'; +import { nextTick } from 'process'; +import { FeedParser } from '~/shared/utils'; @Injectable() -export class FriendsService {} +export class FriendsService { + constructor( + @InjectModel(FriendsModel) + private readonly friendsModel: ReturnModelType, + private readonly configService: ConfigService, + private readonly https: HttpService, + ) {} + + public get model() { + return this.friendsModel; + } + + private throwInvalidTokenException(): never { + throw new RpcException({ + code: HttpStatus.UNAUTHORIZED, + message: ExceptionMessage.FriendLinkTokenIsInvalid, + }); + } + + private throwNotFoundException(): never { + throw new RpcException({ + code: HttpStatus.NOT_FOUND, + message: ExceptionMessage.FriendLinkIsNotExist, + }); + } + + private async checkAlive(url: string) { + try { + const res = await this.https.axiosRef.get(url); + return res.status === 200; + } catch (error) { + return false; + } + } + + private async parseDom(url: string) { + const htmlString = await this.https.axiosRef + .get(url) + .then((res) => res.data); + const dom = new JSDOM(htmlString); + return dom; + } + + /** + * 自动检测是否互链, 基于友链页面是否有类似于 wibus 的链接 + * 因为有部分网站是无法获取到友链页面的, 所以这里只能做到大致的检测 + * + * TODO:如果有更好的检测方法, 欢迎提出 + * + * @param url 友链地址 + * @returns boolean + */ + private async autoCheck(url: string) { + if (!(await this.checkAlive(url))) { + return false; + } + const dom = await this.parseDom(url); + const document = dom.window.document; + const myUrl = (await this.configService.get('site')).frontUrl; + const siteName = (await this.configService.get('seo')).title; + const link = document.querySelector(`a[href="${myUrl}"]`); + if (!link) { + return false; + } + const title = link.textContent || link.getAttribute('title') === siteName; + if (!title) { + return false; + } + return true; + } + + private generateToken() { + return v4(); + } + + /** + * friends.get.list + */ + async getList(group: string) { + return this.friendsModel.find({ + group, + status: FriendStatus.Approved, + }); + } + + /** + * friends.get.all.auth + */ + async getAllByMater(status: FriendStatus) { + return this.friendsModel.find({ + status, + }); + } + + /** + * Event: friend.create + */ + async create(data: FriendsModel, isMaster: boolean) { + if (!isMaster) { + data.status = FriendStatus.Pending; + } + data.token = this.generateToken(); + return this.friendsModel.create(data); + } + + /** + * Event: friend.get + */ + async get(id: string) { + return this.friendsModel.findById(id); + } + + /** + * Event: friend.put.auth.token + */ + async update( + id: string, + data: FriendsModel, + isMaster: boolean, + token?: string, + ) { + const friend = await this.friendsModel.findById(id); + if (!friend) { + this.throwNotFoundException(); + } + if (!isMaster) { + if (!token) { + return; + } + if (token !== friend.token) { + this.throwInvalidTokenException(); + } + data.status = FriendStatus.Pending; + } + return this.friendsModel.findByIdAndUpdate(id, data); + } + + /** + * Event: friend.delete.auth.token + */ + async delete(id: string, isMaster: boolean, token?: string) { + const friend = await this.friendsModel.findById(id); + if (!friend) { + this.throwNotFoundException(); + } + + if (!isMaster) { + if (!token) { + return; + } + if (token !== friend.token) { + this.throwInvalidTokenException(); + } + } + return this.friendsModel.findByIdAndDelete(id); + } + + /** + * friend.check.alive + */ + async checkAliveByIds(id: string[]) { + const friends = await this.friendsModel.find({ _id: { $in: id } }); + const result: { + id: string; + isAlive: boolean; + }[] = []; + for (const friend of friends) { + const isAlive = await this.checkAlive(friend.link); + result.push({ id: friend._id, isAlive }); + } + return result; + } + + /** + * friend.analyse.autoCheck + */ + async autoChecker(id: string) { + const friend = await this.friendsModel.findById(id); + if (!friend) { + throw new RpcException({ + code: HttpStatus.NOT_FOUND, + message: ExceptionMessage.FriendLinkIsNotExist, + }); + } + const isAlive = await this.checkAlive(friend.link); + if (!isAlive) { + await this.friendsModel.findByIdAndUpdate(id, { autoCheck: false }); + } + const isAutoCheck = await this.autoCheck(friend.verifyLink); + if (!isAutoCheck) { + await this.friendsModel.findByIdAndUpdate(id, { autoCheck: false }); + } + return this.friendsModel.findByIdAndUpdate(id, { autoCheck: true }); + } + + /** + * friend.analyse.feed + */ + async feedAnalyse() { + const friends = await this.friendsModel.find(); + // use nextTick to avoid blocking the event loop + nextTick(async () => { + for (const friend of friends) { + if (!this.checkAlive(friend.link)) { + continue; + } + if (!friend.feed) { + continue; + } + const xml = await this.https.axiosRef + .get(friend.feed) + .then((res) => res.data); + const feed = FeedParser(xml, friend.feedType); + await this.friendsModel.findByIdAndUpdate(friend._id, { + feedContent: feed, + }); + } + }); + } +} diff --git a/apps/friends-service/src/friends.model.ts b/apps/friends-service/src/friends.model.ts index 892834881..cbc8b9797 100644 --- a/apps/friends-service/src/friends.model.ts +++ b/apps/friends-service/src/friends.model.ts @@ -1,6 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; import { modelOptions, prop } from '@typegoose/typegoose'; import { IsEmail, IsOptional, IsString, IsUrl } from 'class-validator'; import { BaseModel } from '~/shared/model/base.model'; +import { RssParserType } from '~/shared/utils'; export enum FriendStatus { Pending = 0, // 待审核 @@ -9,65 +11,89 @@ export enum FriendStatus { Trash = 3, // 回收站友链 } -export enum FeedType { - RSS = 0, - Atom = 1, -} - @modelOptions({ options: { customName: 'Friends' } }) export class FriendsModel extends BaseModel { + @prop() + @IsString() + @IsOptional() + @ApiProperty({ + required: false, + description: '友链凭证,可用于对方修改友链信息', + }) + token?: string; + @prop({ required: true }) @IsString() + @ApiProperty({ required: true, description: '友链名称' }) name: string; @prop({ required: true }) @IsString() + @ApiProperty({ required: true, description: '友链地址' }) link: string; @prop() @IsString() @IsOptional() + @ApiProperty({ required: false, description: '友链描述' }) desc?: string; @prop() @IsString() @IsOptional() + @ApiProperty({ required: false, description: '友链主人昵称' }) nickname?: string; @prop() @IsUrl() @IsOptional() + @ApiProperty({ required: false, description: '友链主人头像' }) avatar?: string; @prop() @IsEmail() @IsOptional() + @ApiProperty({ required: false, description: '友链主人邮箱' }) email?: string; @prop({ default: FriendStatus.Pending }) @IsOptional() + @ApiProperty({ required: false, description: '友链状态' }) status?: FriendStatus; @prop() @IsString() @IsOptional() + @ApiProperty({ required: false, description: '友链分组' }) group?: string; @prop({ default: false }) @IsOptional() + @ApiProperty({ required: false, description: '是否添加我方博客友链' }) autoCheck?: boolean; + @prop({ required: true }) + @IsString() + @ApiProperty({ + required: true, + description: '友链验证链接,用于验证对方是否添加我方友链', + }) + verifyLink: string; + @prop() @IsUrl() @IsOptional() + @ApiProperty({ required: false, description: '友链 RSS 地址' }) feed?: string; - @prop({ default: FeedType.RSS }) + @prop({ default: RssParserType.RSS }) @IsOptional() - feedType?: FeedType; + @ApiProperty({ required: false, description: '友链 RSS 类型' }) + feedType?: RssParserType; @prop() @IsString() @IsOptional() + @ApiProperty({ required: false, description: '友链 RSS 内容' }) feedContents?: string; } diff --git a/package.json b/package.json index 8eb686645..cbcb36af7 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "cron": "2.1.0", "dayjs": "1.11.7", "js-yaml": "^4.1.0", + "jsdom": "^21.0.0", "jsonwebtoken": "9.0.0", "lodash": "4.17.21", "mongoose": "6.8.2", @@ -93,6 +94,7 @@ "rxjs": "7.8.0", "slugify": "1.6.5", "snakecase-keys": "5.4.4", + "uuid": "^9.0.0", "vm2": "3.9.13", "zx-cjs": "7.0.7-0" }, @@ -104,12 +106,15 @@ "@nestjs/testing": "9.2.1", "@types/bcrypt": "5.0.0", "@types/cache-manager-ioredis": "2.0.3", - "@types/js-yaml": "4.0.5", "@types/jest": "29.2.5", + "@types/js-yaml": "4.0.5", + "@types/jsdom": "^20.0.1", + "@types/jsonwebtoken": "^9.0.0", "@types/lodash": "4.14.191", "@types/mongoose-aggregate-paginate-v2": "1.0.5", "@types/node": "18.11.18", "@types/supertest": "2.0.12", + "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "5.48.0", "@typescript-eslint/parser": "5.48.0", "commitizen": "4.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fce1069f0..2bb840fa8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,10 +22,13 @@ specifiers: '@types/cache-manager-ioredis': 2.0.3 '@types/jest': 29.2.5 '@types/js-yaml': 4.0.5 + '@types/jsdom': ^20.0.1 + '@types/jsonwebtoken': ^9.0.0 '@types/lodash': 4.14.191 '@types/mongoose-aggregate-paginate-v2': 1.0.5 '@types/node': 18.11.18 '@types/supertest': 2.0.12 + '@types/uuid': ^9.0.0 '@typescript-eslint/eslint-plugin': 5.48.0 '@typescript-eslint/parser': 5.48.0 '@vercel/ncc': 0.36.0 @@ -54,6 +57,7 @@ specifiers: ioredis: 5.2.4 jest: 29.3.1 js-yaml: ^4.1.0 + jsdom: ^21.0.0 jsonwebtoken: 9.0.0 lint-staged: 13.1.0 lodash: 4.17.21 @@ -77,6 +81,7 @@ specifiers: ts-node: 10.9.1 tsconfig-paths: 4.1.2 typescript: 4.9.4 + uuid: ^9.0.0 vm2: 3.9.13 webpack: 5.75.0 xml2js: 0.4.23 @@ -108,6 +113,7 @@ dependencies: cron: 2.1.0 dayjs: 1.11.7 js-yaml: 4.1.0 + jsdom: 21.0.0 jsonwebtoken: 9.0.0 lodash: 4.17.21 mongoose: 6.8.2 @@ -122,6 +128,7 @@ dependencies: rxjs: 7.8.0 slugify: 1.6.5 snakecase-keys: 5.4.4 + uuid: 9.0.0 vm2: 3.9.13 zx-cjs: 7.0.7-0 @@ -135,10 +142,13 @@ devDependencies: '@types/cache-manager-ioredis': 2.0.3 '@types/jest': 29.2.5 '@types/js-yaml': 4.0.5 + '@types/jsdom': 20.0.1 + '@types/jsonwebtoken': 9.0.0 '@types/lodash': 4.14.191 '@types/mongoose-aggregate-paginate-v2': 1.0.5 '@types/node': 18.11.18 '@types/supertest': 2.0.12 + '@types/uuid': 9.0.0 '@typescript-eslint/eslint-plugin': 5.48.0_k73wpmdolxikpyqun3p36akaaq '@typescript-eslint/parser': 5.48.0_iukboom6ndih5an6iafl45j2fe commitizen: 4.2.6 @@ -2124,6 +2134,11 @@ packages: '@sinonjs/commons': 1.8.3 dev: true + /@tootallnate/once/2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: false + /@tsconfig/node10/1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -2261,6 +2276,14 @@ packages: resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} dev: true + /@types/jsdom/20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 18.11.18 + '@types/tough-cookie': 4.0.2 + parse5: 7.1.2 + dev: true + /@types/json-schema/7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true @@ -2269,6 +2292,12 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/jsonwebtoken/9.0.0: + resolution: {integrity: sha512-mM4TkDpA9oixqg1Fv2vVpOFyIVLJjm5x4k0V+K/rEsizfjD7Tk7LKk3GTtbB7KCfP0FEHQtsZqFxYA0+sijNVg==} + dependencies: + '@types/node': 18.11.18 + dev: true + /@types/lodash/4.14.191: resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: true @@ -2319,6 +2348,14 @@ packages: '@types/superagent': 4.1.15 dev: true + /@types/tough-cookie/4.0.2: + resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} + dev: true + + /@types/uuid/9.0.0: + resolution: {integrity: sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==} + dev: true + /@types/validator/13.7.10: resolution: {integrity: sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==} @@ -2698,6 +2735,10 @@ packages: resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} dev: true + /abab/2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: false + /abbrev/1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: false @@ -2712,6 +2753,13 @@ packages: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} dev: false + /acorn-globals/7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.8.1 + acorn-walk: 8.2.0 + dev: false + /acorn-import-assertions/1.8.0_acorn@8.8.0: resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} peerDependencies: @@ -2737,6 +2785,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + /acorn/8.8.1: + resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + engines: {node: '>=0.4.0'} + hasBin: true + /agent-base/6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3575,6 +3628,21 @@ packages: which: 2.0.2 dev: true + /cssom/0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: false + + /cssom/0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: false + + /cssstyle/2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: false + /cz-conventional-changelog/3.3.0: resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} engines: {node: '>= 10'} @@ -3592,6 +3660,15 @@ packages: - '@swc/wasm' dev: true + /data-urls/3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: false + /date-fns/2.29.2: resolution: {integrity: sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==} engines: {node: '>=0.11'} @@ -3633,13 +3710,16 @@ packages: dependencies: ms: 2.1.2 + /decimal.js/10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + /dedent/0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true /deepmerge/4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} @@ -3742,6 +3822,13 @@ packages: esutils: 2.0.3 dev: true + /domexception/4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: false + /dot-case/3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: @@ -3795,6 +3882,10 @@ packages: tapable: 2.2.1 dev: true + /entities/4.4.0: + resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} + engines: {node: '>=0.12'} + /error-ex/1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -3875,6 +3966,19 @@ packages: engines: {node: '>=12'} dev: true + /escodegen/2.0.0: + resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + /eslint-config-prettier/8.6.0_eslint@8.31.0: resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} hasBin: true @@ -4087,7 +4191,6 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - dev: true /esquery/1.4.0: resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} @@ -4111,12 +4214,10 @@ packages: /estraverse/5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - dev: true /esutils/2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - dev: true /etag/1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} @@ -4251,7 +4352,6 @@ packages: /fast-levenshtein/2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true /fast-querystring/1.0.0: resolution: {integrity: sha512-3LQi62IhQoDlmt4ULCYmh17vRO2EtS7hTSsG4WwoKWgV7GLMKBOecEh+aiavASnLx8I2y89OD33AGLo0ccRhzA==} @@ -4735,6 +4835,13 @@ packages: parse-passwd: 1.0.0 dev: true + /html-encoding-sniffer/3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: false + /html-escaper/2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -4749,6 +4856,17 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 + /http-proxy-agent/5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /https-proxy-agent/5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -4787,6 +4905,13 @@ packages: safer-buffer: 2.1.2 dev: true + /iconv-lite/0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -5047,6 +5172,10 @@ packages: engines: {node: '>=8'} dev: true + /is-potential-custom-element-name/1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + /is-regex/1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -5617,6 +5746,47 @@ packages: dependencies: argparse: 2.0.1 + /jsdom/21.0.0: + resolution: {integrity: sha512-AIw+3ZakSUtDYvhwPwWHiZsUi3zHugpMEKlNPaurviseYoBqo0zBd3zqoUi3LPCNtPFlEP8FiW9MqCZdjb2IYA==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.8.1 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.0.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.2 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.2 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.12.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /jsesc/2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -5708,6 +5878,14 @@ packages: engines: {node: '>=6'} dev: true + /levn/0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: false + /levn/0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -6217,6 +6395,10 @@ packages: set-blocking: 2.0.0 dev: false + /nwsapi/2.2.2: + resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} + dev: false + /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -6290,6 +6472,18 @@ packages: yaml: 1.10.2 dev: false + /optionator/0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.3 + dev: false + /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -6411,6 +6605,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /parse5/7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.4.0 + /path-exists/4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -6506,6 +6705,11 @@ packages: engines: {node: '>=4'} dev: true + /prelude-ls/1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: false + /prelude-ls/1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -6557,6 +6761,10 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /psl/1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: false + /pump/3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -6575,6 +6783,10 @@ packages: side-channel: 1.0.4 dev: true + /querystringify/2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: false + /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -6678,6 +6890,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + /requires-port/1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: false + /resolve-cwd/3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -6802,7 +7018,6 @@ packages: /safer-buffer/2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true /saslprep/1.0.3: resolution: {integrity: sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==} @@ -6816,6 +7031,13 @@ packages: resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} dev: true + /saxes/6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: false + /schema-utils/3.1.1: resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==} engines: {node: '>= 10.13.0'} @@ -7014,7 +7236,6 @@ packages: /source-map/0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /source-map/0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} @@ -7217,6 +7438,10 @@ packages: engines: {node: '>=0.10'} dev: true + /symbol-tree/3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + /tapable/2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -7264,7 +7489,7 @@ packages: hasBin: true dependencies: '@jridgewell/source-map': 0.3.2 - acorn: 8.8.0 + acorn: 8.8.1 commander: 2.20.3 source-map-support: 0.5.21 dev: true @@ -7328,6 +7553,16 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + /tough-cookie/4.1.2: + resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.1.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + /tr46/0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -7509,6 +7744,13 @@ packages: typescript: 4.9.4 dev: true + /type-check/0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: false + /type-check/0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -7560,6 +7802,11 @@ packages: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + /universalify/0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + /universalify/2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -7581,6 +7828,13 @@ packages: dependencies: punycode: 2.1.1 + /url-parse/1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: false + /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -7618,6 +7872,13 @@ packages: acorn-walk: 8.2.0 dev: false + /w3c-xmlserializer/4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: false + /walker/1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: @@ -7695,6 +7956,18 @@ packages: - uglify-js dev: true + /whatwg-encoding/2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: false + + /whatwg-mimetype/3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: false + /whatwg-url/11.0.0: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} @@ -7748,7 +8021,6 @@ packages: /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} - dev: true /wrap-ansi/6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} @@ -7788,6 +8060,24 @@ packages: signal-exit: 3.0.7 dev: true + /ws/8.12.0: + resolution: {integrity: sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xml-name-validator/4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: false + /xml2js/0.4.23: resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} engines: {node: '>=4.0.0'} @@ -7801,6 +8091,10 @@ packages: engines: {node: '>=4.0'} dev: true + /xmlchars/2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + /y18n/5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} diff --git a/shared/constants/echo.constant.ts b/shared/constants/echo.constant.ts index ccd356e7a..2103310ef 100644 --- a/shared/constants/echo.constant.ts +++ b/shared/constants/echo.constant.ts @@ -33,4 +33,7 @@ export enum ExceptionMessage { InvalidCommentReaction = '无效的评论反应 (。ì _ í。)', StatusIsNotRight = '状态不正确啊 (゚o゚;;', + + FriendLinkIsNotExist = '友链不存在 o(╯□╰)o', + FriendLinkTokenIsInvalid = '友链Token无效,可能找错了哦', } diff --git a/shared/constants/event.constant.ts b/shared/constants/event.constant.ts index 6291eae73..d29e02205 100644 --- a/shared/constants/event.constant.ts +++ b/shared/constants/event.constant.ts @@ -64,3 +64,19 @@ export enum CommentsEvents { CommentAddRecaction = 'comment.add.reaction', CommentRemoveRecaction = 'comment.remove.reaction', } + +export enum FriendsEvents { + FriendsGetList = 'friends.get.list', + FriendsGetAllByMaster = 'friends.get.all.auth', + + FriendCreate = 'friend.create', + FriendGet = 'friend.get', + + FriendPutByMasterOrToken = 'friend.put.auth.token', + + FriendDeleteByMasterOrToken = 'friend.delete.auth.token', + + FriendAnalyseFeed = 'friend.analyse.feed', + FriendAnalyseAutoCheck = 'friend.analyse.autoCheck', + FriendCheckAlive = 'friend.check.alive', +} From 155f3a941e5ac647c683a1b890435a3efd8cfec3 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 20:15:21 +0800 Subject: [PATCH 04/12] perf: export module --- shared/utils/rss-parser.util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/utils/rss-parser.util.ts b/shared/utils/rss-parser.util.ts index 345cc6ba0..8aeaf33c9 100644 --- a/shared/utils/rss-parser.util.ts +++ b/shared/utils/rss-parser.util.ts @@ -88,7 +88,7 @@ const rssParser = ( }); }; -const Parser = (xml: string, type: RssParserType = RssParserType.RSS) => { +const FeedParser = (xml: string, type: RssParserType = RssParserType.RSS) => { let res; if (type === RssParserType.RSS) { rssParser(xml, (result) => { @@ -102,4 +102,4 @@ const Parser = (xml: string, type: RssParserType = RssParserType.RSS) => { return res ? res : null; }; -export { Parser, RssParserType }; +export { FeedParser, RssParserType }; From 3e2fb7dc9ac2a08405ca47bc0197815fc25d9382 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 20:18:20 +0800 Subject: [PATCH 05/12] fix: inject module into database module --- libs/database/src/database.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/database/src/database.module.ts b/libs/database/src/database.module.ts index 3c6ecd749..bf4cfa2ea 100644 --- a/libs/database/src/database.module.ts +++ b/libs/database/src/database.module.ts @@ -1,5 +1,6 @@ import { Global, Module, Provider } from '@nestjs/common'; import { CommentsModel } from '~/apps/comments-service/src/comments.model'; +import { FriendsModel } from '~/apps/friends-service/src/friends.model'; import { CategoryModel } from '~/apps/page-service/src/model/category.model'; import { PageModel } from '~/apps/page-service/src/model/page.model'; import { PostModel } from '~/apps/page-service/src/model/post.model'; @@ -16,6 +17,7 @@ const models = [ PageModel, CategoryModel, CommentsModel, + FriendsModel, ].map((model) => getProviderByTypegooseClass(model)); const providers: Provider[] = [DatabaseService, databaseProvider, ...models]; From 693873f4060f6813e3ee8c093523fcd4ebc8d215 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 21:40:18 +0800 Subject: [PATCH 06/12] feat: controller --- .../src/friends-service.controller.ts | 63 +++++++++++++++++++ .../src/friends-service.service.ts | 4 +- shared/constants/event.constant.ts | 4 +- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/apps/friends-service/src/friends-service.controller.ts b/apps/friends-service/src/friends-service.controller.ts index 741edb7d5..291664c6e 100644 --- a/apps/friends-service/src/friends-service.controller.ts +++ b/apps/friends-service/src/friends-service.controller.ts @@ -1,7 +1,70 @@ import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { FriendsEvents } from '~/shared/constants/event.constant'; import { FriendsService } from './friends-service.service'; +import { FriendsModel, FriendStatus } from './friends.model'; @Controller() export class FriendsServiceController { constructor(private readonly friendsService: FriendsService) {} + + @MessagePattern({ cmd: FriendsEvents.FriendsGetList }) + async getFriendsList(group: string) { + return await this.friendsService.getList(group); + } + + @MessagePattern({ cmd: FriendsEvents.FriendsGetAllByMaster }) + async getAllFriends(status: FriendStatus) { + return await this.friendsService.getAllByMaster(status); + } + + @MessagePattern({ cmd: FriendsEvents.FriendGet }) + async getFriend(id: string) { + return await this.friendsService.get(id); + } + + @MessagePattern({ cmd: FriendsEvents.FriendCreate }) + async createFriend(data: FriendsModel, isMaster: boolean) { + return await this.friendsService.create(data, isMaster); + } + + @MessagePattern({ cmd: FriendsEvents.FriendUpdateByMasterOrToken }) + async updateFriend(input: { + id: string; + data: FriendsModel; + isMaster: boolean; + token?: string; + }) { + return await this.friendsService.update( + input.id, + input.data, + input.isMaster, + input.token, + ); + } + + @MessagePattern({ cmd: FriendsEvents.FriendDeleteByMasterOrToken }) + async deleteFriend(input: { id: string; isMaster: boolean; token?: string }) { + return await this.friendsService.delete( + input.id, + input.isMaster, + input.token, + ); + } + + @MessagePattern({ cmd: FriendsEvents.FriendAnalyseFeed }) + async analyseFeed() { + await this.friendsService.analyseFeed(); + return true; + } + + @MessagePattern({ cmd: FriendsEvents.FriendAnalyseAutoCheck }) + async analyseAutoCheck(id: string) { + return await this.friendsService.analyseAutoCheck(id); + } + + @MessagePattern({ cmd: FriendsEvents.FriendsCheckAlive }) + async checkAlive(ids: string[]) { + return await this.friendsService.checkAliveByIds(ids); + } } diff --git a/apps/friends-service/src/friends-service.service.ts b/apps/friends-service/src/friends-service.service.ts index bbb3e97d7..f3f5d322d 100644 --- a/apps/friends-service/src/friends-service.service.ts +++ b/apps/friends-service/src/friends-service.service.ts @@ -99,7 +99,7 @@ export class FriendsService { /** * friends.get.all.auth */ - async getAllByMater(status: FriendStatus) { + async getAllByMaster(status: FriendStatus) { return this.friendsModel.find({ status, }); @@ -209,7 +209,7 @@ export class FriendsService { /** * friend.analyse.feed */ - async feedAnalyse() { + async analyseFeed() { const friends = await this.friendsModel.find(); // use nextTick to avoid blocking the event loop nextTick(async () => { diff --git a/shared/constants/event.constant.ts b/shared/constants/event.constant.ts index d29e02205..929b8b498 100644 --- a/shared/constants/event.constant.ts +++ b/shared/constants/event.constant.ts @@ -68,15 +68,15 @@ export enum CommentsEvents { export enum FriendsEvents { FriendsGetList = 'friends.get.list', FriendsGetAllByMaster = 'friends.get.all.auth', + FriendsCheckAlive = 'friends.check.alive', FriendCreate = 'friend.create', FriendGet = 'friend.get', - FriendPutByMasterOrToken = 'friend.put.auth.token', + FriendUpdateByMasterOrToken = 'friend.put.auth.token', FriendDeleteByMasterOrToken = 'friend.delete.auth.token', FriendAnalyseFeed = 'friend.analyse.feed', FriendAnalyseAutoCheck = 'friend.analyse.autoCheck', - FriendCheckAlive = 'friend.check.alive', } From a1e1338d4e0a6a3fc3212aaaadbc7f2861822e70 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 21:40:33 +0800 Subject: [PATCH 07/12] perf: typo --- apps/friends-service/src/friends-service.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/friends-service/src/friends-service.service.ts b/apps/friends-service/src/friends-service.service.ts index f3f5d322d..4b642fb10 100644 --- a/apps/friends-service/src/friends-service.service.ts +++ b/apps/friends-service/src/friends-service.service.ts @@ -187,7 +187,7 @@ export class FriendsService { /** * friend.analyse.autoCheck */ - async autoChecker(id: string) { + async analyseAutoCheck(id: string) { const friend = await this.friendsModel.findById(id); if (!friend) { throw new RpcException({ From debae7b16f0bdd19b967b54852b98318321742ba Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 22:06:33 +0800 Subject: [PATCH 08/12] revert: remove basic service --- .../modules/friends/friends.basic.model.ts | 78 ------------------- .../modules/friends/friends.basic.service.ts | 46 ----------- .../src/modules/friends/friends.controller.ts | 32 +++----- .../src/modules/friends/friends.module.ts | 5 +- 4 files changed, 14 insertions(+), 147 deletions(-) delete mode 100644 apps/core/src/modules/friends/friends.basic.model.ts delete mode 100644 apps/core/src/modules/friends/friends.basic.service.ts diff --git a/apps/core/src/modules/friends/friends.basic.model.ts b/apps/core/src/modules/friends/friends.basic.model.ts deleted file mode 100644 index c0ae4e1f0..000000000 --- a/apps/core/src/modules/friends/friends.basic.model.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { modelOptions, prop } from '@typegoose/typegoose'; -import { - IsEmail, - IsNumber, - IsOptional, - IsString, - IsUrl, -} from 'class-validator'; -import { BaseModel } from '~/shared/model/base.model'; - -export enum FriendStatus { - Pending, - Approved, - Ban, -} - -export enum FeedType { - RSS, - ATOM, -} - -export enum FriendType { - UNIDIRECTION, // 单向 - BIDIRECTION, // 双向 -} - -@modelOptions({ options: { customName: 'Friends' } }) -export class FriendsBasicModel extends BaseModel { - @prop({ required: true, trim: true }) - @IsString() - name: string; - - @prop({ required: true, trim: true }) - @IsString() - @IsUrl() - link: string; - - @prop({ required: true }) - @IsString() - @IsUrl() - avatar: string; - - @prop({ required: true }) - @IsString() - description: string; - - @prop({ required: true, trim: true }) - @IsString() - @IsEmail() - email: string; - - @prop({ default: FriendStatus.Pending }) - @IsNumber() - @IsOptional() - status?: FriendStatus; - - // They are used in the friends microservice. - - // @prop({ default: FriendType.UNIDIRECTION }) - // @IsNumber() - // @IsOptional() - // type?: FriendType; - - // @prop() - // @IsString() - // @IsUrl() - // @IsOptional() - // feed: string; - - // @prop({ default: FeedType.RSS }) - // @IsNumber() - // @IsOptional() - // feedType?: FeedType; - - // @prop() - // @IsString() - // secret: string; // Friend can use this secret to update the friend information. -} diff --git a/apps/core/src/modules/friends/friends.basic.service.ts b/apps/core/src/modules/friends/friends.basic.service.ts deleted file mode 100644 index 433559e79..000000000 --- a/apps/core/src/modules/friends/friends.basic.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ReturnModelType } from '@typegoose/typegoose'; -import { InjectModel } from '~/shared/transformers/model.transformer'; -import { FriendsBasicModel, FriendStatus } from './friends.basic.model'; - -@Injectable() -export class FriendsBasicService { - constructor( - @InjectModel(FriendsBasicModel) - private readonly friendsBasicModel: ReturnModelType< - typeof FriendsBasicModel - >, - ) {} - - get model() { - return this.friendsBasicModel; - } - - async getAllFriends(isMaster: boolean) { - return await this.friendsBasicModel.find({ - status: !isMaster && FriendStatus.Approved, - }); - } - - async createFriend(friend: FriendsBasicModel, isMaster: boolean) { - if (!isMaster) { - friend.status = FriendStatus.Pending; - } - return await this.friendsBasicModel.create(friend); - } - - async updateFriend(id: string, friend: FriendsBasicModel, isMaster: boolean) { - if (!isMaster) { - friend.status = FriendStatus.Pending; - } - return await this.friendsBasicModel.findByIdAndUpdate(id, friend); - } - - async updateFriendStatus(id: string, status: FriendStatus) { - return await this.friendsBasicModel.findByIdAndUpdate(id, { status }); - } - - async deleteFriend(id: string) { - return await this.friendsBasicModel.findByIdAndDelete(id); - } -} diff --git a/apps/core/src/modules/friends/friends.controller.ts b/apps/core/src/modules/friends/friends.controller.ts index 80552de1e..5fd06784a 100644 --- a/apps/core/src/modules/friends/friends.controller.ts +++ b/apps/core/src/modules/friends/friends.controller.ts @@ -10,53 +10,45 @@ import { Query, } from '@nestjs/common'; import { ApiOperation } from '@nestjs/swagger'; +import { + FriendsModel, + FriendStatus, +} from '~/apps/friends-service/src/friends.model'; import { ApiName } from '~/shared/common/decorator/openapi.decorator'; import { IsMaster } from '~/shared/common/decorator/role.decorator'; -import { FriendsBasicModel, FriendStatus } from './friends.basic.model'; -import { FriendsBasicService } from './friends.basic.service'; @Controller('friends') @ApiName export class FriendsController { - constructor(private readonly friendsBasicService: FriendsBasicService) {} + constructor() {} @Get('/') @ApiOperation({ summary: '获取所有友链' }) - async getAllFriends(@IsMaster() isMaster: boolean) { - return await this.friendsBasicService.getAllFriends(isMaster); - } + async getAllFriends(@IsMaster() isMaster: boolean) {} @Post('/') @ApiOperation({ summary: '创建友链' }) async createFriend( - @Body() friend: FriendsBasicModel, + @Body() friend: FriendsModel, @IsMaster() isMaster: boolean, - ) { - return await this.friendsBasicService.createFriend(friend, isMaster); - } + ) {} @Put('/:id') @ApiOperation({ summary: '更新友链' }) async updateFriend( @Param('id') id: string, - @Body() friend: FriendsBasicModel, + @Body() friend: FriendsModel, @IsMaster() isMaster: boolean, - ) { - return await this.friendsBasicService.updateFriend(id, friend, isMaster); - } + ) {} @Patch('/:id') @ApiOperation({ summary: '更新友链状态' }) async updateFriendStatus( @Param('id') id: string, @Query('status') status: FriendStatus, - ) { - return await this.friendsBasicService.updateFriendStatus(id, status); - } + ) {} @Delete('/:id') @ApiOperation({ summary: '删除友链' }) - async deleteFriend(@Param('id') id: string) { - return await this.friendsBasicService.deleteFriend(id); - } + async deleteFriend(@Param('id') id: string) {} } diff --git a/apps/core/src/modules/friends/friends.module.ts b/apps/core/src/modules/friends/friends.module.ts index ba3ca2198..9eae8c3a4 100644 --- a/apps/core/src/modules/friends/friends.module.ts +++ b/apps/core/src/modules/friends/friends.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; -import { FriendsBasicService } from './friends.basic.service'; import { FriendsController } from './friends.controller'; @Module({ imports: [], controllers: [FriendsController], - providers: [FriendsBasicService], - exports: [FriendsBasicService], + providers: [], + exports: [], }) export class FriendsModule {} From 3229a33fb88b271171a5bd8ee82802176b804f87 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 22:35:44 +0800 Subject: [PATCH 09/12] feat: public core --- .../src/modules/friends/friends.controller.ts | 111 +++++++++++++++--- .../src/modules/friends/friends.module.ts | 19 ++- .../src/friends-service.controller.ts | 8 +- .../src/friends-service.service.ts | 6 +- shared/constants/event.constant.ts | 2 +- 5 files changed, 121 insertions(+), 25 deletions(-) diff --git a/apps/core/src/modules/friends/friends.controller.ts b/apps/core/src/modules/friends/friends.controller.ts index 5fd06784a..a716a9aeb 100644 --- a/apps/core/src/modules/friends/friends.controller.ts +++ b/apps/core/src/modules/friends/friends.controller.ts @@ -3,52 +3,131 @@ import { Controller, Delete, Get, + Inject, Param, - Patch, Post, Put, Query, } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; import { ApiOperation } from '@nestjs/swagger'; import { FriendsModel, FriendStatus, } from '~/apps/friends-service/src/friends.model'; +import { Auth } from '~/shared/common/decorator/auth.decorator'; import { ApiName } from '~/shared/common/decorator/openapi.decorator'; import { IsMaster } from '~/shared/common/decorator/role.decorator'; +import { FriendsEvents } from '~/shared/constants/event.constant'; +import { ServicesEnum } from '~/shared/constants/services.constant'; +import { transportReqToMicroservice } from '~/shared/microservice.transporter'; @Controller('friends') @ApiName export class FriendsController { - constructor() {} + constructor( + @Inject(ServicesEnum.friends) private readonly friends: ClientProxy, + ) {} + + @Get('/:group') + @ApiOperation({ summary: '获取所有友链(By group)' }) + async getAllFriends(@Param('group') group: string | undefined) { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendsGetList, + group, + ); + } - @Get('/') - @ApiOperation({ summary: '获取所有友链' }) - async getAllFriends(@IsMaster() isMaster: boolean) {} + @Get('/all') + @ApiOperation({ summary: '获取所有友链(Master)' }) + @Auth() + async getAllFriendsMaster(@Query('status') status: FriendStatus) { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendsGetAllByMaster, + status, + ); + } + + @Get('/:id') + @ApiOperation({ summary: '获取友链详情' }) + async getFriend(@Param('id') id: string) { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendGet, + id, + ); + } @Post('/') @ApiOperation({ summary: '创建友链' }) async createFriend( @Body() friend: FriendsModel, @IsMaster() isMaster: boolean, - ) {} + ) { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendCreate, + { friend, isMaster }, + ); + } @Put('/:id') @ApiOperation({ summary: '更新友链' }) async updateFriend( @Param('id') id: string, - @Body() friend: FriendsModel, + @Body() friend: FriendsModel & { token?: string }, @IsMaster() isMaster: boolean, - ) {} - - @Patch('/:id') - @ApiOperation({ summary: '更新友链状态' }) - async updateFriendStatus( - @Param('id') id: string, - @Query('status') status: FriendStatus, - ) {} + ) { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendUpdateByMasterOrToken, + { id, data: friend, isMaster, token: friend.token }, + ); + } @Delete('/:id') @ApiOperation({ summary: '删除友链' }) - async deleteFriend(@Param('id') id: string) {} + async deleteFriend( + @Param('id') id: string, + @Body('token') token: string, + @IsMaster() isMaster: boolean, + ) { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendDeleteByMasterOrToken, + { id, isMaster, token }, + ); + } + + @Get('/alive') + @ApiOperation({ summary: '检查友链是否存活' }) + async checkAliver() { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendsCheckAlive, + null, + ); + } + + @Get('/feeds') + @ApiOperation({ summary: '获取友链 Feed 更新' }) + async getFeed() { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendsAnalyseFeed, + null, + ); + } + + @Get('/:id/check') + @ApiOperation({ summary: '自动检查是否互链' }) + async autoCheck(@Param('id') id: string) { + return transportReqToMicroservice( + this.friends, + FriendsEvents.FriendAnalyseAutoCheck, + id, + ); + } } diff --git a/apps/core/src/modules/friends/friends.module.ts b/apps/core/src/modules/friends/friends.module.ts index 9eae8c3a4..f6790d34c 100644 --- a/apps/core/src/modules/friends/friends.module.ts +++ b/apps/core/src/modules/friends/friends.module.ts @@ -1,8 +1,25 @@ import { Module } from '@nestjs/common'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { + ServicesEnum, + ServicePorts, +} from '~/shared/constants/services.constant'; +import { getEnv } from '~/shared/utils/rag-env'; import { FriendsController } from './friends.controller'; @Module({ - imports: [], + imports: [ + ClientsModule.register([ + { + name: ServicesEnum.friends, + transport: Transport.TCP, + options: { + port: getEnv(ServicesEnum.friends)?.port || ServicePorts.friends, + host: getEnv(ServicesEnum.friends)?.host || undefined, + }, + }, + ]), + ], controllers: [FriendsController], providers: [], exports: [], diff --git a/apps/friends-service/src/friends-service.controller.ts b/apps/friends-service/src/friends-service.controller.ts index 291664c6e..0802d03d4 100644 --- a/apps/friends-service/src/friends-service.controller.ts +++ b/apps/friends-service/src/friends-service.controller.ts @@ -9,7 +9,7 @@ export class FriendsServiceController { constructor(private readonly friendsService: FriendsService) {} @MessagePattern({ cmd: FriendsEvents.FriendsGetList }) - async getFriendsList(group: string) { + async getFriendsList(group: string | undefined) { return await this.friendsService.getList(group); } @@ -52,7 +52,7 @@ export class FriendsServiceController { ); } - @MessagePattern({ cmd: FriendsEvents.FriendAnalyseFeed }) + @MessagePattern({ cmd: FriendsEvents.FriendsAnalyseFeed }) async analyseFeed() { await this.friendsService.analyseFeed(); return true; @@ -64,7 +64,7 @@ export class FriendsServiceController { } @MessagePattern({ cmd: FriendsEvents.FriendsCheckAlive }) - async checkAlive(ids: string[]) { - return await this.friendsService.checkAliveByIds(ids); + async checkAlive() { + return await this.friendsService.checkAliver(); } } diff --git a/apps/friends-service/src/friends-service.service.ts b/apps/friends-service/src/friends-service.service.ts index 4b642fb10..28381d277 100644 --- a/apps/friends-service/src/friends-service.service.ts +++ b/apps/friends-service/src/friends-service.service.ts @@ -89,7 +89,7 @@ export class FriendsService { /** * friends.get.list */ - async getList(group: string) { + async getList(group: string | undefined) { return this.friendsModel.find({ group, status: FriendStatus.Approved, @@ -171,8 +171,8 @@ export class FriendsService { /** * friend.check.alive */ - async checkAliveByIds(id: string[]) { - const friends = await this.friendsModel.find({ _id: { $in: id } }); + async checkAliver() { + const friends = await this.friendsModel.find(); const result: { id: string; isAlive: boolean; diff --git a/shared/constants/event.constant.ts b/shared/constants/event.constant.ts index 929b8b498..64a2d697f 100644 --- a/shared/constants/event.constant.ts +++ b/shared/constants/event.constant.ts @@ -69,6 +69,7 @@ export enum FriendsEvents { FriendsGetList = 'friends.get.list', FriendsGetAllByMaster = 'friends.get.all.auth', FriendsCheckAlive = 'friends.check.alive', + FriendsAnalyseFeed = 'friends.analyse.feed', FriendCreate = 'friend.create', FriendGet = 'friend.get', @@ -77,6 +78,5 @@ export enum FriendsEvents { FriendDeleteByMasterOrToken = 'friend.delete.auth.token', - FriendAnalyseFeed = 'friend.analyse.feed', FriendAnalyseAutoCheck = 'friend.analyse.autoCheck', } From 9f0374228d9ec801a489c372dd820f3abe58dd2e Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 22:42:30 +0800 Subject: [PATCH 10/12] chore: npm-script [skip ci] --- package.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index fd4f50f63..516a206e1 100644 --- a/package.json +++ b/package.json @@ -25,23 +25,26 @@ "preinstall": "npx only-allow pnpm", "prepare": "husky install", "prebuild": "rimraf dist", - "build": "pnpm run build:core && pnpm run build:user-service && pnpm run build:page-service && pnpm run build:comments-service", + "build": "pnpm run build:core && pnpm run build:user-service && pnpm run build:page-service && pnpm run build:comments-service && pnpm run build:friends-service", "build:core": "nest build", "build:user-service": "nest build user-service", "build:page-service": "nest build page-service", "build:comments-service": "nest build comments-service", + "build:friends-service": "nest build friends-service", "dev": "npm run start", - "bundle": "pnpm run bundle:core && pnpm run bundle:user-service && pnpm run bundle:page-service && pnpm run bundle:comments-service", + "bundle": "pnpm run bundle:core && pnpm run bundle:user-service && pnpm run bundle:page-service && pnpm run bundle:comments-service && pnpm run bundle:friends-service", "bundle:core": "cd dist/apps/core/src && ncc build main.js -o ../../../../out/core", "bundle:user-service": "cd dist/apps/user-service/src && ncc build main.js -o ../../../../out/user-service", "bundle:page-service": "cd dist/apps/page-service/src && ncc build main.js -o ../../../../out/page-service", "bundle:comments-service": "cd dist/apps/comments-service/src && ncc build main.js -o ../../../../out/comments-service", + "bundle:friends-service": "cd dist/apps/friends-service/src && ncc build main.js -o ../../../../out/friends-service", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", - "start": "cross-env NODE_ENV=development concurrently --kill-others \"npm run start:core\" \"npm run start:user-service\" \"npm run start:page-service\" \"npm run start:comments-service\"", + "start": "cross-env NODE_ENV=development concurrently --kill-others \"npm run start:core\" \"npm run start:user-service\" \"npm run start:page-service\" \"npm run start:comments-service\" \"npm run start:friends-service\"", "start:core": "cross-env NODE_ENV=development cross-env NODE_ENV=development nest start -w core", "start:user-service": "cross-env NODE_ENV=development nest start -w user-service", "start:page-service": "cross-env NODE_ENV=development nest start -w page-service", "start:comments-service": "cross-env NODE_ENV=development nest start -w comments-service", + "start:friends-service": "cross-env NODE_ENV=development nest start -w friends-service", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "prod": "cross-env NODE_ENV=production pm2-runtime start ecosystem.config.js", "prod:pm2": "cross-env NODE_ENV=production pm2 restart ecosystem.config.js", From b0607c51b712550f179d769a17c026b2c061fa39 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 22:59:35 +0800 Subject: [PATCH 11/12] fix: invalid message data --- apps/core/src/app.module.ts | 2 ++ apps/core/src/modules/friends/friends.controller.ts | 7 ++++--- apps/friends-service/src/friends-service.controller.ts | 5 +++-- apps/friends-service/src/friends-service.service.ts | 10 +++++++++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/core/src/app.module.ts b/apps/core/src/app.module.ts index 5edeeaed1..464b51f52 100644 --- a/apps/core/src/app.module.ts +++ b/apps/core/src/app.module.ts @@ -17,6 +17,7 @@ import { PageModule } from './modules/page/page.module'; import { PostModule } from './modules/post/post.module'; import { UserModule } from './modules/user/user.module'; import { CommentsModule } from './modules/comments/comments.module'; +import { FriendsModule } from './modules/friends/friends.module'; @Module({ imports: [ @@ -30,6 +31,7 @@ import { CommentsModule } from './modules/comments/comments.module'; CategoryModule, AggregateModule, CommentsModule, + FriendsModule, ], controllers: [AppController], providers: [ diff --git a/apps/core/src/modules/friends/friends.controller.ts b/apps/core/src/modules/friends/friends.controller.ts index a716a9aeb..ea3efceb0 100644 --- a/apps/core/src/modules/friends/friends.controller.ts +++ b/apps/core/src/modules/friends/friends.controller.ts @@ -29,13 +29,14 @@ export class FriendsController { @Inject(ServicesEnum.friends) private readonly friends: ClientProxy, ) {} - @Get('/:group') + @Get('/') @ApiOperation({ summary: '获取所有友链(By group)' }) - async getAllFriends(@Param('group') group: string | undefined) { + async getAllFriends(@Query('group') group?: string | undefined) { + console.log('group', group); return transportReqToMicroservice( this.friends, FriendsEvents.FriendsGetList, - group, + group ? group : {}, ); } diff --git a/apps/friends-service/src/friends-service.controller.ts b/apps/friends-service/src/friends-service.controller.ts index 0802d03d4..a096b8a3a 100644 --- a/apps/friends-service/src/friends-service.controller.ts +++ b/apps/friends-service/src/friends-service.controller.ts @@ -9,8 +9,9 @@ export class FriendsServiceController { constructor(private readonly friendsService: FriendsService) {} @MessagePattern({ cmd: FriendsEvents.FriendsGetList }) - async getFriendsList(group: string | undefined) { - return await this.friendsService.getList(group); + async getFriendsList(group: string | undefined | Object) { + const theGroup = group ? (group === Object ? undefined : group) : undefined; + return await this.friendsService.getList(String(theGroup)); } @MessagePattern({ cmd: FriendsEvents.FriendsGetAllByMaster }) diff --git a/apps/friends-service/src/friends-service.service.ts b/apps/friends-service/src/friends-service.service.ts index 28381d277..46f79e6f3 100644 --- a/apps/friends-service/src/friends-service.service.ts +++ b/apps/friends-service/src/friends-service.service.ts @@ -10,6 +10,7 @@ import { RpcException } from '@nestjs/microservices'; import { ExceptionMessage } from '~/shared/constants/echo.constant'; import { nextTick } from 'process'; import { FeedParser } from '~/shared/utils'; +import { isValidObjectId } from 'mongoose'; @Injectable() export class FriendsService { constructor( @@ -120,7 +121,14 @@ export class FriendsService { * Event: friend.get */ async get(id: string) { - return this.friendsModel.findById(id); + if (!isValidObjectId(id)) { + this.throwNotFoundException(); + } + const friend = await this.friendsModel.findById(id); + if (!friend) { + this.throwNotFoundException(); + } + return friend; } /** From 1827e92d03cebb8f2903ec20031bc3f2b70ddc2a Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 8 Jan 2023 23:36:48 +0800 Subject: [PATCH 12/12] verify --- .../src/modules/friends/friends.controller.ts | 9 ++-- .../src/friends-service.controller.ts | 4 +- .../src/friends-service.service.ts | 41 ++++++++++++++----- shared/microservice.transporter.ts | 3 +- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/apps/core/src/modules/friends/friends.controller.ts b/apps/core/src/modules/friends/friends.controller.ts index ea3efceb0..a72013e27 100644 --- a/apps/core/src/modules/friends/friends.controller.ts +++ b/apps/core/src/modules/friends/friends.controller.ts @@ -64,13 +64,13 @@ export class FriendsController { @Post('/') @ApiOperation({ summary: '创建友链' }) async createFriend( - @Body() friend: FriendsModel, + @Body() data: FriendsModel, @IsMaster() isMaster: boolean, ) { return transportReqToMicroservice( this.friends, FriendsEvents.FriendCreate, - { friend, isMaster }, + { data, isMaster }, ); } @@ -108,7 +108,7 @@ export class FriendsController { return transportReqToMicroservice( this.friends, FriendsEvents.FriendsCheckAlive, - null, + {}, ); } @@ -118,7 +118,8 @@ export class FriendsController { return transportReqToMicroservice( this.friends, FriendsEvents.FriendsAnalyseFeed, - null, + {}, + 10000, ); } diff --git a/apps/friends-service/src/friends-service.controller.ts b/apps/friends-service/src/friends-service.controller.ts index a096b8a3a..88870f9e0 100644 --- a/apps/friends-service/src/friends-service.controller.ts +++ b/apps/friends-service/src/friends-service.controller.ts @@ -25,8 +25,8 @@ export class FriendsServiceController { } @MessagePattern({ cmd: FriendsEvents.FriendCreate }) - async createFriend(data: FriendsModel, isMaster: boolean) { - return await this.friendsService.create(data, isMaster); + async createFriend(data: { data: FriendsModel; isMaster: boolean }) { + return await this.friendsService.create(data); } @MessagePattern({ cmd: FriendsEvents.FriendUpdateByMasterOrToken }) diff --git a/apps/friends-service/src/friends-service.service.ts b/apps/friends-service/src/friends-service.service.ts index 46f79e6f3..a184237c1 100644 --- a/apps/friends-service/src/friends-service.service.ts +++ b/apps/friends-service/src/friends-service.service.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpStatus, Injectable, Logger } from '@nestjs/common'; import { ReturnModelType } from '@typegoose/typegoose'; import { v4 } from 'uuid'; import { HttpService } from '~/libs/helper/src/helper.http.service'; @@ -102,19 +102,30 @@ export class FriendsService { */ async getAllByMaster(status: FriendStatus) { return this.friendsModel.find({ - status, + status: status || FriendStatus.Approved, }); } /** * Event: friend.create */ - async create(data: FriendsModel, isMaster: boolean) { - if (!isMaster) { - data.status = FriendStatus.Pending; + async create(input: { data: FriendsModel; isMaster: boolean }) { + if (!input.isMaster) { + input.data.status = FriendStatus.Pending; } - data.token = this.generateToken(); - return this.friendsModel.create(data); + input.data.token = this.generateToken(); + const friend = await this.friendsModel.create(input.data); + nextTick(async () => { + friend.autoCheck = await this.autoCheck(input.data.link); + await friend.save(); + Logger.warn( + `${friend.name} 申请友链 - 互链检测: ${ + friend.autoCheck ? '通过' : '未通过' + }`, + FriendsService.name, + ); + }); + return friend; } /** @@ -146,14 +157,14 @@ export class FriendsService { } if (!isMaster) { if (!token) { - return; + this.throwInvalidTokenException(); } if (token !== friend.token) { this.throwInvalidTokenException(); } - data.status = FriendStatus.Pending; + data.status = FriendStatus.Pending; // 友链方修改后, 状态必须变为待审核,防止下毒 } - return this.friendsModel.findByIdAndUpdate(id, data); + return this.friendsModel.updateOne({ _id: id }, data); } /** @@ -210,8 +221,10 @@ export class FriendsService { const isAutoCheck = await this.autoCheck(friend.verifyLink); if (!isAutoCheck) { await this.friendsModel.findByIdAndUpdate(id, { autoCheck: false }); + Logger.warn(`${friend.name} 互链检测: 未通过`, FriendsService.name); } - return this.friendsModel.findByIdAndUpdate(id, { autoCheck: true }); + await this.friendsModel.findByIdAndUpdate(id, { autoCheck: true }); + return isAutoCheck; } /** @@ -223,9 +236,14 @@ export class FriendsService { nextTick(async () => { for (const friend of friends) { if (!this.checkAlive(friend.link)) { + Logger.warn(`${friend.name} 无法访问,跳过解析`, FriendsService.name); continue; } if (!friend.feed) { + Logger.warn( + `${friend.name} 未提供 feed 地址,跳过解析`, + FriendsService.name, + ); continue; } const xml = await this.https.axiosRef @@ -235,6 +253,7 @@ export class FriendsService { await this.friendsModel.findByIdAndUpdate(friend._id, { feedContent: feed, }); + Logger.log(`解析 ${friend.name} 订阅成功`, FriendsService.name); } }); } diff --git a/shared/microservice.transporter.ts b/shared/microservice.transporter.ts index 4bc35ea1e..2439fac3f 100644 --- a/shared/microservice.transporter.ts +++ b/shared/microservice.transporter.ts @@ -6,9 +6,10 @@ export function transportReqToMicroservice( client: ClientProxy, cmd: string, data: any, + time = 3000, ) { return client.send({ cmd }, data).pipe( - timeout(1000), + timeout(time), catchError((err) => { return throwError( () => new HttpException(err.message || '未知错误', err.code || 500),