Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(backend): 通知がミュート・凍結を考慮するようにする #13412

Merged
merged 62 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
417bb2d
Never return broken notifications #409
dakkar Feb 11, 2024
fae7cb2
Update Changelog
kakkokari-gtyih Feb 13, 2024
9ce9c6e
Merge branch 'develop' into fix-10650
kakkokari-gtyih Feb 16, 2024
7db0971
Merge branch 'develop' into fix-10650
kakkokari-gtyih Feb 16, 2024
56bbb52
Merge branch 'develop' into fix-10650
kakkokari-gtyih Feb 19, 2024
8d68c50
Update CHANGELOG.md
kakkokari-gtyih Feb 19, 2024
35609b4
enhance: 通知がミュートを考慮するようにする
tai-cha Feb 19, 2024
b1e57e5
enhance: 通知が凍結も考慮するようにする
tai-cha Feb 19, 2024
09a9484
fix: notifierIdがない通知が消えてしまう問題
tai-cha Feb 19, 2024
094e10a
Add tests (通知がミュートを考慮しているかどうか)
tai-cha Feb 19, 2024
c21b6d9
fix: notifierIdがない通知が消えてしまう問題 (grouped)
tai-cha Feb 19, 2024
c70c2e7
Remove unused import
tai-cha Feb 19, 2024
19296a0
Fix: typo
tai-cha Feb 20, 2024
2096caf
Merge branch 'develop' into notification-hide-muting-user
tai-cha Feb 20, 2024
f0c1c08
Revert "enhance: 通知が凍結も考慮するようにする"
tai-cha Feb 20, 2024
1304a9f
Revert API handling
tai-cha Feb 20, 2024
45a0677
Remove unused imports
tai-cha Feb 20, 2024
b09abb8
enhance: Check if notifierId is valid in NotificationEntityService
tai-cha Feb 20, 2024
ef65252
通知作成時にpackしてnullになったらあとの処理をやめる
tai-cha Feb 20, 2024
239a695
Remove duplication of valid notifier check
tai-cha Feb 20, 2024
d53b837
add filter notification is not null
tai-cha Feb 20, 2024
7e1d5f0
Merge branch 'develop' into notification-hide-muting-user
tai-cha Feb 20, 2024
ffb853b
Revert "Remove duplication of valid notifier check"
tai-cha Feb 20, 2024
3693ce9
Improve performance
tai-cha Feb 20, 2024
ff7f7c8
Fix packGrouped
tai-cha Feb 20, 2024
f5ae663
Refactor: 判定部分を共通化
tai-cha Feb 20, 2024
823b7c3
Fix condition
tai-cha Feb 20, 2024
7c9bacb
use isNotNull
tai-cha Feb 20, 2024
659f8d9
Update CHANGELOG.md
tai-cha Feb 20, 2024
286c7eb
Merge branch 'develop' into notification-hide-muting-user
tai-cha Feb 20, 2024
a4ad66b
Merge branch 'fix-10650' of https://github.com/kakkokari-gtyih/misske…
tai-cha Feb 20, 2024
0cf8946
filterの改善
tai-cha Feb 20, 2024
5dae5ba
Refactor: DONT REPEAT YOURSELF
tai-cha Feb 20, 2024
f985636
Add groupedNotificationTypes
tai-cha Feb 20, 2024
6c69a43
Update misskey-js typedef
tai-cha Feb 20, 2024
e59be54
Merge branch 'develop' of https://github.com/misskey-dev/misskey into…
tai-cha Feb 21, 2024
ed33ac7
Refactor: less sql calls
tai-cha Feb 21, 2024
e2443d8
refactor
tamaina Feb 21, 2024
60109cc
clean up
tamaina Feb 21, 2024
7753124
filter notes to mark as read
tamaina Feb 21, 2024
6b45b81
packed noteがmapなのでそちらを使う
tamaina Feb 21, 2024
6be1d98
if (notesToRead.size > 0)
tamaina Feb 21, 2024
22e2324
if (notes.length === 0) return;
tamaina Feb 21, 2024
9f2b402
fix
tamaina Feb 21, 2024
665cf24
Revert "if (notes.length === 0) return;"
tamaina Feb 21, 2024
ba6cb93
:art:
tamaina Feb 21, 2024
45fe974
console.error
tamaina Feb 21, 2024
8106569
err
tamaina Feb 21, 2024
25f441d
remove try-catch
tamaina Feb 21, 2024
47264af
不要なジェネリクスを除去
tamaina Feb 21, 2024
07b135b
Merge branch 'develop' of https://github.com/misskey-dev/misskey into…
tai-cha Feb 23, 2024
9729f73
Revert (既読処理をpack内で行うものを元に戻す)
tai-cha Feb 23, 2024
665a3c1
Merge branch 'develop' of https://github.com/misskey-dev/misskey into…
tai-cha Feb 23, 2024
8df636e
Clean
tai-cha Feb 23, 2024
03c9e64
Merge branch 'develop' into notification-hide-muting-user
tai-cha Feb 25, 2024
66d0b71
Merge branch 'develop' into notification-hide-muting-user
tai-cha Feb 28, 2024
9f1a9e8
Update packages/backend/src/core/entities/NotificationEntityService.ts
syuilo Feb 28, 2024
eead5ff
Update packages/backend/src/core/entities/NotificationEntityService.ts
syuilo Feb 28, 2024
23408ad
Update packages/backend/src/core/entities/NotificationEntityService.ts
syuilo Feb 28, 2024
bfecc0e
Update packages/backend/src/core/entities/NotificationEntityService.ts
syuilo Feb 28, 2024
01d67e8
Update packages/backend/src/core/NotificationService.ts
syuilo Feb 28, 2024
ff9df0d
Clean
tai-cha Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
## 202x.x.x (unreleased)

### General
- 通知がミュート、凍結を考慮するようになりました
- Enhance: サーバーごとにモデレーションノートを残せるように
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加
Expand All @@ -28,6 +29,8 @@

### Server
- Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正
- Fix: 破損した通知をクライアントに送信しないように
* 通知欄が無限にリロードされる問題が改善する可能性があります
- エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました
- Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正
- エンドポイント`admin/emoji/update`の各種修正
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/NotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export class NotificationService implements OnApplicationShutdown {

const packed = await this.notificationEntityService.pack(notification, notifieeId, {});

if (packed === null) return null;
syuilo marked this conversation as resolved.
Show resolved Hide resolved

// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);

Expand Down
269 changes: 152 additions & 117 deletions packages/backend/src/core/entities/NotificationEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js';
import { CacheService } from '@/core/CacheService.js';
import { RoleEntityService } from './RoleEntityService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';

const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']);
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);

@Injectable()
export class NotificationEntityService implements OnModuleInit {
Expand All @@ -41,6 +41,8 @@ export class NotificationEntityService implements OnModuleInit {
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,

private cacheService: CacheService,

//private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService,
) {
Expand All @@ -52,138 +54,61 @@ export class NotificationEntityService implements OnModuleInit {
this.roleEntityService = this.moduleRef.get('RoleEntityService');
}

@bindThis
public async pack(
src: MiNotification,
/**
* 通知をパックする共通処理
*/
async #packInternal <T extends MiNotification | MiGroupedNotification> (
src: T,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {

checkValidNotifier?: boolean;
},
hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
},
): Promise<Packed<'Notification'>> {
): Promise<Packed<'Notification'> | null> {
const notification = src;
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
hint?.packedNotes != null
? hint.packedNotes.get(notification.noteId)
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
detail: true,
})
) : undefined;
const userIfNeed = 'notifierId' in notification ? (
hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId })
) : undefined;
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;

return await awaitAll({
id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(),
type: notification.type,
userId: 'notifierId' in notification ? notification.notifierId : undefined,
...(userIfNeed != null ? { user: userIfNeed } : {}),
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
} : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,
icon: notification.customIcon,
} : {}),
});
}

@bindThis
public async packMany(
notifications: MiNotification[],
meId: MiUser['id'],
) {
if (notifications.length === 0) return [];

let validNotifications = notifications;

const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'],
}) : [];
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
detail: true,
});
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));

validNotifications = validNotifications.filter(x => !('noteId' in x) || packedNotes.has(x.noteId));
if (options.checkValidNotifier !== false && !(await this.#isValidNotifier(notification, meId))) return null;

const userIds = validNotifications.map(x => 'notifierId' in x ? x.notifierId : null).filter(isNotNull);
const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) },
}) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));

// 既に解決されたフォローリクエストの通知を除外
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
if (followRequestNotifications.length > 0) {
const reqs = await this.followRequestsRepository.find({
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
});
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
}

return await Promise.all(validNotifications.map(x => this.pack(x, meId, {}, {
packedNotes,
packedUsers,
})));
}

@bindThis
public async packGrouped(
src: MiGroupedNotification,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {

},
hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
},
): Promise<Packed<'Notification'>> {
const notification = src;
const noteIfNeed = NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification ? (
const needsNote = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && 'noteId' in notification;
const noteIfNeed = needsNote ? (
hint?.packedNotes != null
? hint.packedNotes.get(notification.noteId)
: this.noteEntityService.pack(notification.noteId, { id: meId }, {
detail: true,
})
) : undefined;
const userIfNeed = 'notifierId' in notification ? (
// if the note has been deleted, don't show this notification
if (needsNote && !noteIfNeed) return null;

const needsUser = 'notifierId' in notification;
const userIfNeed = needsUser ? (
hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId })
) : undefined;
// if the user has been deleted, don't show this notification
if (needsUser && !userIfNeed) return null;

// #region Grouped notifications
if (notification.type === 'reaction:grouped') {
const reactions = await Promise.all(notification.reactions.map(async reaction => {
const reactions = (await Promise.all(notification.reactions.map(async reaction => {
const user = hint?.packedUsers != null
? hint.packedUsers.get(reaction.userId)!
: await this.userEntityService.pack(reaction.userId, { id: meId });
return {
user,
reaction: reaction.reaction,
};
}));
}))).filter(r => isNotNull(r.user));
// if all users have been deleted, don't show this notification
if (!reactions.length) {
syuilo marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

return await awaitAll({
id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(),
Expand All @@ -192,14 +117,19 @@ export class NotificationEntityService implements OnModuleInit {
reactions,
});
} else if (notification.type === 'renote:grouped') {
const users = await Promise.all(notification.userIds.map(userId => {
const users = (await Promise.all(notification.userIds.map(userId => {
const packedUser = hint?.packedUsers != null ? hint.packedUsers.get(userId) : null;
if (packedUser) {
return packedUser;
}

return this.userEntityService.pack(userId, { id: meId });
}));
}))).filter(isNotNull);
// if all users have been deleted, don't show this notification
if (!users.length) {
syuilo marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

return await awaitAll({
id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(),
Expand All @@ -208,8 +138,14 @@ export class NotificationEntityService implements OnModuleInit {
users,
});
}
// #endregion

const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
const needsRole = notification.type === 'roleAssigned';
const role = needsRole ? await this.roleEntityService.pack(notification.roleId) : undefined;
// if the role has been deleted, don't show this notification
if (needsRole && !role) {
return null;
}

return await awaitAll({
id: notification.id,
Expand All @@ -235,15 +171,16 @@ export class NotificationEntityService implements OnModuleInit {
});
}

@bindThis
public async packGroupedMany(
notifications: MiGroupedNotification[],
async #packManyInternal <T extends MiNotification | MiGroupedNotification> (
notifications: T[],
meId: MiUser['id'],
) {
): Promise<T[]> {
if (notifications.length === 0) return [];

let validNotifications = notifications;

validNotifications = await this.#filterValidNotifier(validNotifications, meId);

const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull);
const notes = noteIds.length > 0 ? await this.notesRepository.find({
where: { id: In(noteIds) },
Expand All @@ -269,17 +206,115 @@ export class NotificationEntityService implements OnModuleInit {
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));

// 既に解決されたフォローリクエストの通知を除外
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<MiGroupedNotification, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
const followRequestNotifications = validNotifications.filter((x): x is FilterUnionByProperty<T, 'type', 'receiveFollowRequest'> => x.type === 'receiveFollowRequest');
if (followRequestNotifications.length > 0) {
const reqs = await this.followRequestsRepository.find({
where: { followerId: In(followRequestNotifications.map(x => x.notifierId)) },
});
validNotifications = validNotifications.filter(x => (x.type !== 'receiveFollowRequest') || reqs.some(r => r.followerId === x.notifierId));
}

return await Promise.all(validNotifications.map(x => this.packGrouped(x, meId, {}, {
packedNotes,
packedUsers,
})));
const packPromises = validNotifications.map(x => {
return this.pack(
x,
meId,
{ checkValidNotifier: false },
{ packedNotes, packedUsers },
);
});

return (await Promise.all(packPromises)).filter(isNotNull);
}

@bindThis
public async pack(
src: MiNotification | MiGroupedNotification,
meId: MiUser['id'],
// eslint-disable-next-line @typescript-eslint/ban-types
options: {
checkValidNotifier?: boolean;
},
hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
},
): Promise<Packed<'Notification'> | null> {
return await this.#packInternal(src, meId, options, hint);
}

@bindThis
public async packMany(
notifications: MiNotification[],
meId: MiUser['id'],
): Promise<MiNotification[]> {
return await this.#packManyInternal(notifications, meId);
}

@bindThis
public async packGroupedMany(
notifications: MiGroupedNotification[],
meId: MiUser['id'],
): Promise<MiGroupedNotification[]> {
return await this.#packManyInternal(notifications, meId);
}

/**
* notifierが存在するか、ミュートされていないか、サスペンドされていないかを確認するvalidator
*/
async #validateNotifier <T extends MiNotification | MiGroupedNotification> (
tai-cha marked this conversation as resolved.
Show resolved Hide resolved
notification: T,
userIdsWhoMeMuting: Set<MiUser['id']>,
userMutedInstances: Set<string>,
notifiers: MiUser[],
): Promise<boolean> {
if (!('notifierId' in notification)) return true;
if (userIdsWhoMeMuting.has(notification.notifierId)) return false;

const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null;

if (notifier === null) return false;
syuilo marked this conversation as resolved.
Show resolved Hide resolved
if (notifier.host && userMutedInstances.has(notifier.host)) return false;

if (notifier.isSuspended) return false;

return true;
}

/**
* notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に確認する
*/
async #isValidNotifier(
notification: MiNotification | MiGroupedNotification,
meId: MiUser['id'],
): Promise<boolean> {
return (await this.#filterValidNotifier([notification], meId)).length === 1;
}

/**
* notifierが存在するか、ミュートされていないか、サスペンドされていないかを実際に複数確認する
*/
async #filterValidNotifier <T extends MiNotification | MiGroupedNotification> (
notifications: T[],
meId: MiUser['id'],
): Promise<T[]> {
const [
userIdsWhoMeMuting,
userMutedInstances,
] = await Promise.all([
this.cacheService.userMutingsCache.fetch(meId),
this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)),
]);

const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull);
const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({
where: { id: In(notifierIds) },
}) : [];

const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
const isValid = await this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
return isValid ? notification : null;
}))) as [T|null] ).filter(isNotNull);
syuilo marked this conversation as resolved.
Show resolved Hide resolved

return filteredNotifications;
}
}
Loading
Loading