diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index 95899c2ccc..37c0a8d52b 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js'; import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; @Injectable() export class AnnouncementService { @@ -67,6 +68,16 @@ export class AnnouncementService { @bindThis public async create(values: Partial, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> { + if (values.display === 'dialog') { + // Check how many active dialog queries already exist, to enforce a limit + const query = this.announcementsRepository.createQueryBuilder('announcement'); + query.andWhere('announcement.isActive = true'); + const count = await query.getCount(); + if (count >= 5) { + throw new IdentifiableError('c0d15f15-f18e-4a40-bcb1-f310d58204ee', 'Too many dialog announcements.'); + } + } + const announcement = await this.announcementsRepository.insertOne({ id: this.idService.gen(), updatedAt: null, diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts index 8da39810e9..3052fdf2fc 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts @@ -4,8 +4,10 @@ */ import { Injectable } from '@nestjs/common'; +import { ApiError } from '@/server/api/error.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; export const meta = { tags: ['admin'], @@ -48,6 +50,14 @@ export const meta = { }, }, }, + + errors: { + tooManyDialogs: { + message: 'Cannot create the announcement because there are too many active dialog-style announcements.', + code: 'DIALOG_LIMIT_EXCEEDED', + id: '7c1bc084-9c14-4bcf-a910-978cd8e99b5a', + }, + }, } as const; export const paramDef = { @@ -73,22 +83,28 @@ export default class extends Endpoint { // eslint- private announcementService: AnnouncementService, ) { super(meta, paramDef, async (ps, me) => { - const { raw, packed } = await this.announcementService.create({ - updatedAt: null, - title: ps.title, - text: ps.text, - /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ - imageUrl: ps.imageUrl || null, - icon: ps.icon, - display: ps.display, - forExistingUsers: ps.forExistingUsers, - silence: ps.silence, - needConfirmationToRead: ps.needConfirmationToRead, - confetti: ps.confetti, - userId: ps.userId, - }, me); - - return packed; + try { + const { raw, packed } = await this.announcementService.create({ + updatedAt: null, + title: ps.title, + text: ps.text, + /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ + imageUrl: ps.imageUrl || null, + icon: ps.icon, + display: ps.display, + forExistingUsers: ps.forExistingUsers, + silence: ps.silence, + needConfirmationToRead: ps.needConfirmationToRead, + confetti: ps.confetti, + userId: ps.userId, + }, me); + return packed; + } catch (e) { + if (e instanceof IdentifiableError) { + if (e.id === 'c0d15f15-f18e-4a40-bcb1-f310d58204ee') throw new ApiError(meta.errors.tooManyDialogs); + } + throw e; + } }); } }