hippofish/packages/backend/src/core/AnnouncementService.ts
かっこかり 3ffbf6296f
feat: 個別のお知らせにリンクで飛べるように (#13885)
* feat(announcement): 個別のお知らせにリンクで飛べるように (MisskeyIO#639)

(cherry picked from commit f6bf7f992a78e54d86a4701dbd1e4fda7ef4eb27)

* fix

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>

* fix

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>

* 一覧ページではお知らせpanel全体を押せるように

* お知らせバーは個別ページに飛ばすように

* Update Changelog

* spdx

* attempt to fox test

* remove unnecessary thong

* `announcement` → `announcements/show`

* リンクを押せる場所をタイトルと日付部分のみに変更

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-05-27 17:15:11 +09:00

216 lines
7.1 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets, EntityNotFoundError } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js';
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';
@Injectable()
export class AnnouncementService {
constructor(
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
private announcementEntityService: AnnouncementEntityService,
) {
}
@bindThis
public async getReads(userId: MiUser['id']): Promise<MiAnnouncementRead[]> {
return this.announcementReadsRepository.findBy({
userId: userId,
});
}
@bindThis
public async getUnreadAnnouncements(user: MiUser): Promise<MiAnnouncement[]> {
const readsQuery = this.announcementReadsRepository.createQueryBuilder('read')
.select('read.announcementId')
.where('read.userId = :userId', { userId: user.id });
const q = this.announcementsRepository.createQueryBuilder('announcement')
.where('announcement.isActive = true')
.andWhere('announcement.silence = false')
.andWhere(new Brackets(qb => {
qb.orWhere('announcement.userId = :userId', { userId: user.id });
qb.orWhere('announcement.userId IS NULL');
}))
.andWhere(new Brackets(qb => {
qb.orWhere('announcement.forExistingUsers = false');
qb.orWhere('announcement.id > :userId', { userId: user.id });
}))
.andWhere(`announcement.id NOT IN (${ readsQuery.getQuery() })`);
q.setParameters(readsQuery.getParameters());
return q.getMany();
}
@bindThis
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
const announcement = await this.announcementsRepository.insert({
id: this.idService.gen(),
updatedAt: null,
title: values.title,
text: values.text,
imageUrl: values.imageUrl,
icon: values.icon,
display: values.display,
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
userId: values.userId,
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
const packed = await this.announcementEntityService.pack(announcement);
if (values.userId) {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
announcement: packed,
});
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: values.userId });
this.moderationLogService.log(moderator, 'createUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
userId: values.userId,
userUsername: user.username,
userHost: user.host,
});
}
} else {
this.globalEventService.publishBroadcastStream('announcementCreated', {
announcement: packed,
});
if (moderator) {
this.moderationLogService.log(moderator, 'createGlobalAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
}
}
return {
raw: announcement,
packed: packed,
};
}
@bindThis
public async update(announcement: MiAnnouncement, values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<void> {
await this.announcementsRepository.update(announcement.id, {
updatedAt: new Date(),
title: values.title,
text: values.text,
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
imageUrl: values.imageUrl || null,
display: values.display,
icon: values.icon,
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
isActive: values.isActive,
});
const after = await this.announcementsRepository.findOneByOrFail({ id: announcement.id });
if (moderator) {
if (announcement.userId) {
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
this.moderationLogService.log(moderator, 'updateUserAnnouncement', {
announcementId: announcement.id,
before: announcement,
after: after,
userId: announcement.userId,
userUsername: user.username,
userHost: user.host,
});
} else {
this.moderationLogService.log(moderator, 'updateGlobalAnnouncement', {
announcementId: announcement.id,
before: announcement,
after: after,
});
}
}
}
@bindThis
public async delete(announcement: MiAnnouncement, moderator?: MiUser): Promise<void> {
await this.announcementsRepository.delete(announcement.id);
if (moderator) {
if (announcement.userId) {
const user = await this.usersRepository.findOneByOrFail({ id: announcement.userId });
this.moderationLogService.log(moderator, 'deleteUserAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
userId: announcement.userId,
userUsername: user.username,
userHost: user.host,
});
} else {
this.moderationLogService.log(moderator, 'deleteGlobalAnnouncement', {
announcementId: announcement.id,
announcement: announcement,
});
}
}
}
@bindThis
public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
if (me) {
if (announcement.userId && announcement.userId !== me.id) {
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
}
const read = await this.announcementReadsRepository.findOneBy({
announcementId: announcement.id,
userId: me.id,
});
return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me);
} else {
return this.announcementEntityService.pack(announcement, null);
}
}
@bindThis
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
try {
await this.announcementReadsRepository.insert({
id: this.idService.gen(),
announcementId: announcementId,
userId: user.id,
});
} catch (e) {
return;
}
if ((await this.getUnreadAnnouncements(user)).length === 0) {
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
}
}
}