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

143 lines
4.3 KiB
TypeScript
Raw Normal View History

/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
2022-09-17 20:27:08 +02:00
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
2023-03-10 06:22:37 +01:00
import type { Packed } from '@/misc/json-schema.js';
import type { MiNote } from '@/models/Note.js';
2022-09-17 20:27:08 +02:00
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
2023-02-01 09:29:28 +01:00
import { bindThis } from '@/decorators.js';
import { trackPromise } from '@/misc/promise-tracker.js';
2022-09-17 20:27:08 +02:00
@Injectable()
export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController();
2022-09-17 20:27:08 +02:00
constructor(
@Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
private idService: IdService,
2023-02-04 02:02:03 +01:00
private globalEventService: GlobalEventService,
2022-09-17 20:27:08 +02:00
) {
}
@bindThis
public async insertNoteUnread(userId: MiUser['id'], note: MiNote, params: {
2022-09-17 20:27:08 +02:00
// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
isSpecified: boolean;
isMentioned: boolean;
}): Promise<void> {
//#region ミュートしているなら無視
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
2022-09-17 20:27:08 +02:00
// スレッドミュート
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: userId,
threadId: note.threadId ?? note.id,
},
2022-09-17 20:27:08 +02:00
});
if (isThreadMuted) return;
2022-09-17 20:27:08 +02:00
const unread = {
id: this.idService.gen(),
2022-09-17 20:27:08 +02:00
noteId: note.id,
userId: userId,
isSpecified: params.isSpecified,
isMentioned: params.isMentioned,
noteUserId: note.userId,
};
2022-09-17 20:27:08 +02:00
await this.noteUnreadsRepository.insert(unread);
2022-09-17 20:27:08 +02:00
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
if (!exist) return;
2022-09-17 20:27:08 +02:00
if (params.isMentioned) {
2023-02-04 02:02:03 +01:00
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
2022-09-17 20:27:08 +02:00
}
if (params.isSpecified) {
2023-02-04 02:02:03 +01:00
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
2022-09-17 20:27:08 +02:00
}
}, () => { /* aborted, ignore it */ });
}
2022-09-17 20:27:08 +02:00
@bindThis
2022-09-17 20:27:08 +02:00
public async read(
userId: MiUser['id'],
notes: (MiNote | Packed<'Note'>)[],
2022-09-17 20:27:08 +02:00
): Promise<void> {
const readMentions: (MiNote | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (MiNote | Packed<'Note'>)[] = [];
2022-09-17 20:27:08 +02: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);
}
}
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
2022-09-17 20:27:08 +02:00
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
2022-09-17 20:27:08 +02:00
});
2022-09-17 20:27:08 +02:00
// TODO: ↓まとめてクエリしたい
trackPromise(this.noteUnreadsRepository.countBy({
2022-09-17 20:27:08 +02:00
userId: userId,
isMentioned: true,
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
2023-02-04 02:02:03 +01:00
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
2022-09-17 20:27:08 +02:00
}
}));
trackPromise(this.noteUnreadsRepository.countBy({
2022-09-17 20:27:08 +02:00
userId: userId,
isSpecified: true,
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
2023-02-04 02:02:03 +01:00
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
2022-09-17 20:27:08 +02:00
}
}));
2022-09-17 20:27:08 +02:00
}
}
2023-05-29 06:21:26 +02:00
@bindThis
public dispose(): void {
this.#shutdownController.abort();
}
2023-05-29 06:21:26 +02:00
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
2022-09-17 20:27:08 +02:00
}