monkeeShark/packages/backend/src/core/NoteReadService.ts

224 lines
7.5 KiB
TypeScript
Raw Normal View History

import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
2022-09-17 18:27:08 +00:00
import { In, IsNull, Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
2023-03-10 05:22:37 +00:00
import type { Packed } from '@/misc/json-schema.js';
2022-09-17 18:27:08 +00:00
import type { Note } from '@/models/entities/Note.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
2022-09-22 21:21:31 +00:00
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
2022-12-04 01:16:03 +00:00
import { UserEntityService } from '@/core/entities/UserEntityService.js';
2023-02-01 08:29:28 +00:00
import { bindThis } from '@/decorators.js';
2022-09-17 18:27:08 +00:00
import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js';
import { PushNotificationService } from './PushNotificationService.js';
2022-09-17 18:27:08 +00:00
@Injectable()
export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController();
2022-09-17 18:27:08 +00:00
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,
private userEntityService: UserEntityService,
private idService: IdService,
2023-02-04 01:02:03 +00:00
private globalEventService: GlobalEventService,
2022-09-17 18:27:08 +00:00
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
2022-09-17 18:27:08 +00:00
) {
}
@bindThis
2022-09-17 18:27:08 +00:00
public async insertNoteUnread(userId: User['id'], note: Note, params: {
// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
isSpecified: boolean;
isMentioned: boolean;
}): Promise<void> {
//#region ミュートしているなら無視
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
2022-09-17 18:27:08 +00:00
// スレッドミュート
const threadMute = await this.noteThreadMutingsRepository.findOneBy({
userId: userId,
threadId: note.threadId ?? note.id,
});
if (threadMute) return;
2022-09-17 18:27:08 +00:00
const unread = {
id: this.idService.genId(),
noteId: note.id,
userId: userId,
isSpecified: params.isSpecified,
isMentioned: params.isMentioned,
noteChannelId: note.channelId,
noteUserId: note.userId,
};
2022-09-17 18:27:08 +00:00
await this.noteUnreadsRepository.insert(unread);
2022-09-17 18:27:08 +00:00
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
2022-09-17 18:27:08 +00:00
const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id });
2022-09-17 18:27:08 +00:00
if (exist == null) return;
2022-09-17 18:27:08 +00:00
if (params.isMentioned) {
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
2022-09-17 18:27:08 +00:00
}
if (params.isSpecified) {
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
2022-09-17 18:27:08 +00:00
}
if (note.channelId) {
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
2022-09-17 18:27:08 +00:00
}
}, () => { /* aborted, ignore it */ });
}
2022-09-17 18:27:08 +00:00
@bindThis
2022-09-17 18:27:08 +00:00
public async read(
userId: User['id'],
notes: (Note | Packed<'Note'>)[],
info?: {
following: Set<User['id']>;
followingChannels: Set<Channel['id']>;
},
): Promise<void> {
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
where: {
followerId: userId,
},
select: ['followeeId'],
})).map(x => x.followeeId));
2022-09-17 18:27:08 +00:00
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
2022-09-17 18:27:08 +00:00
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
readMentions.push(note);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note);
}
2022-09-17 18:27:08 +00:00
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
2022-09-17 18:27:08 +00:00
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
2023-02-01 08:29:28 +00:00
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
2022-09-17 18:27:08 +00:00
readAntennaNotes.push(note);
}
}
}
}
2022-09-17 18:27:08 +00:00
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
});
2022-09-17 18:27:08 +00:00
// TODO: ↓まとめてクエリしたい
this.noteUnreadsRepository.countBy({
userId: userId,
isMentioned: true,
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
2022-09-17 18:27:08 +00:00
}
});
this.noteUnreadsRepository.countBy({
userId: userId,
isSpecified: true,
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
2022-09-17 18:27:08 +00:00
}
});
this.noteUnreadsRepository.countBy({
userId: userId,
noteChannelId: Not(IsNull()),
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'readAllChannels');
2022-09-17 18:27:08 +00:00
}
});
this.notificationService.readNotificationByQuery(userId, {
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
}
2022-09-17 18:27:08 +00:00
if (readAntennaNotes.length > 0) {
await this.antennaNotesRepository.update({
antennaId: In(myAntennas.map(a => a.id)),
noteId: In(readAntennaNotes.map(n => n.id)),
}, {
read: true,
});
2022-09-17 18:27:08 +00:00
// TODO: まとめてクエリしたい
for (const antenna of myAntennas) {
const count = await this.antennaNotesRepository.countBy({
antennaId: antenna.id,
read: false,
});
2022-09-17 18:27:08 +00:00
if (count === 0) {
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
2022-09-17 18:27:08 +00:00
}
}
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
if (!unread) {
2023-02-04 01:02:03 +00:00
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
2022-09-17 18:27:08 +00:00
}
});
}
}
onApplicationShutdown(signal?: string | undefined): void {
this.#shutdownController.abort();
}
2022-09-17 18:27:08 +00:00
}