From 7f9c7d1d524b9aa27d1f4a6c2237f6148494acc7 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac <36768584+AmarTrebinjac@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:01:21 +0100 Subject: [PATCH] feat: Filter out notifications from blocked users (#2598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should cover the following workers: - articleNewCommentCommentedComment (what the hell is this name 😂) - articleNewCommentPostCommented - commentMention - commentReply - postAdded - postAddedUserNotification - postMention I think these are all the workers that are triggered by another user which are not specifically meant for admins/moderators. Let me know if I missed any! --------- Co-authored-by: Lee Hansel Solevilla <13744167+sshanzel@users.noreply.github.com> --- __tests__/workers/notifications.ts | 60 ++++++++++++++++++- src/notifications/index.ts | 55 ++++++++++++++++- src/notifications/types.ts | 5 +- src/workers/notifications/commentMention.ts | 1 + src/workers/notifications/commentReply.ts | 1 + src/workers/notifications/postAdded.ts | 1 + .../postAddedUserNotification.ts | 1 + src/workers/notifications/postMention.ts | 1 + src/workers/notifications/utils.ts | 1 + 9 files changed, 122 insertions(+), 4 deletions(-) diff --git a/__tests__/workers/notifications.ts b/__tests__/workers/notifications.ts index 500e5a501..6067e7c83 100644 --- a/__tests__/workers/notifications.ts +++ b/__tests__/workers/notifications.ts @@ -21,6 +21,7 @@ import { User, UserAction, UserActionType, + UserNotification, UserPost, UserStreak, } from '../../src/entity'; @@ -30,6 +31,7 @@ import createOrGetConnection from '../../src/db'; import { usersFixture, sourcesFixture, badUsersFixture } from '../fixture'; import { postsFixture } from '../fixture/post'; import { + generateAndStoreNotificationsV2, NotificationBookmarkContext, NotificationCommentContext, NotificationCommenterContext, @@ -54,7 +56,11 @@ import { generateStorageKey, StorageKey, StorageTopic } from '../../src/config'; import { ioRedisPool, setRedisObject } from '../../src/redis'; import { ReportReason } from '../../src/entity/common'; import { ContentPreferenceUser } from '../../src/entity/contentPreference/ContentPreferenceUser'; -import { ContentPreferenceStatus } from '../../src/entity/contentPreference/types'; +import { + ContentPreferenceStatus, + ContentPreferenceType, +} from '../../src/entity/contentPreference/types'; +import { ContentPreference } from '../../src/entity/contentPreference/ContentPreference'; let con: DataSource; @@ -1053,6 +1059,58 @@ describe('article new comment', () => { expect(actual[0].ctx.userIds).toIncludeSameMembers(['1', '3']); }); + it('should filter out blocked users when generating notifications', async () => { + await con.getRepository(Feed).save({ + id: '1', + userId: '1', + }); + + await con.getRepository(ContentPreference).save({ + userId: '1', + feedId: '1', + status: ContentPreferenceStatus.Blocked, + type: ContentPreferenceType.User, + referenceId: '2', + }); + + await generateAndStoreNotificationsV2(con.manager, [ + { + type: NotificationType.ArticleNewComment, + ctx: { + userIds: ['1', '3'], + initiatorId: '2', + commenter: { + id: '2', + name: 'Commenter', + }, + comment: { + id: 'c1', + content: 'Comment', + }, + post: { + id: 'p1', + authorId: '1', + scoutId: '3', + }, + source: { + id: 'a', + type: SourceType.Squad, + }, + }, + }, + ]); + + const notifications = await con + .getRepository(UserNotification) + .createQueryBuilder('un') + .innerJoinAndSelect('un.notification', 'n') + .where('n.type = :type', { type: NotificationType.ArticleNewComment }) + .getMany(); + + expect(notifications).toHaveLength(1); + expect(notifications[0].userId).toEqual('3'); + }); + it('should add notification but ignore users with muted settings', async () => { const worker = await import( '../../src/workers/notifications/articleNewCommentCommentCommented' diff --git a/src/notifications/index.ts b/src/notifications/index.ts index 7fd0acdfe..84050fe6f 100644 --- a/src/notifications/index.ts +++ b/src/notifications/index.ts @@ -4,7 +4,7 @@ import { NotificationV2, UserNotification, } from '../entity'; -import { DeepPartial, EntityManager, ObjectLiteral } from 'typeorm'; +import { DeepPartial, EntityManager, In, ObjectLiteral } from 'typeorm'; import { NotificationBuilder } from './builder'; import { NotificationBaseContext, NotificationBundleV2 } from './types'; import { generateNotificationMap, notificationTitleMap } from './generate'; @@ -12,6 +12,11 @@ import { generateUserNotificationUniqueKey, NotificationType } from './common'; import { NotificationHandlerReturn } from '../workers/notifications/worker'; import { EntityTarget } from 'typeorm/common/EntityTarget'; import { logger } from '../logger'; +import { ContentPreference } from '../entity/contentPreference/ContentPreference'; +import { + ContentPreferenceStatus, + ContentPreferenceType, +} from '../entity/contentPreference/types'; export * from './types'; @@ -212,9 +217,55 @@ export async function generateAndStoreNotificationsV2( if (!args) { return; } + const filteredArgs = []; + + for (const arg of args) { + const { type, ctx } = arg; + if (!ctx.initiatorId) { + filteredArgs.push(arg); + continue; + } + + const userIdChunks: string[][] = []; + for (let i = 0; i < ctx.userIds.length; i += 500) { + userIdChunks.push(ctx.userIds.slice(i, i + 500)); + } + + const contentPreferences: ContentPreference[] = []; + + for (const chunk of userIdChunks) { + const preferences = await entityManager + .getRepository(ContentPreference) + .find({ + where: { + feedId: In(chunk), + referenceId: ctx.initiatorId!, + status: ContentPreferenceStatus.Blocked, + type: ContentPreferenceType.User, + }, + }); + contentPreferences.push(...preferences); + } + + const receivingUserIds = ctx.userIds.filter( + (id) => !contentPreferences.some((pref) => pref.userId === id), + ); + + if (receivingUserIds.length === 0) { + continue; + } + + filteredArgs.push({ + type, + ctx: { + ...ctx, + userIds: receivingUserIds, + }, + }); + } await Promise.all( - args.map(({ type, ctx }) => { + filteredArgs.map(({ type, ctx }) => { const bundle = generateNotificationV2(type, ctx); if (!bundle.userIds.length) { return; diff --git a/src/notifications/types.ts b/src/notifications/types.ts index b00eebaff..8956ddabc 100644 --- a/src/notifications/types.ts +++ b/src/notifications/types.ts @@ -28,7 +28,10 @@ export type NotificationBundleV2 = { attachments?: DeepPartial[]; }; -export type NotificationBaseContext = { userIds: string[] }; +export type NotificationBaseContext = { + userIds: string[]; + initiatorId?: string | null; +}; export type NotificationSubmissionContext = NotificationBaseContext & { submission: Pick; }; diff --git a/src/workers/notifications/commentMention.ts b/src/workers/notifications/commentMention.ts index 03a512ea6..52b2a6e5e 100644 --- a/src/workers/notifications/commentMention.ts +++ b/src/workers/notifications/commentMention.ts @@ -63,6 +63,7 @@ const worker: NotificationWorker = { userIds: [data.commentMention.mentionedUserId], commenter, comment, + initiatorId: commenter.id, }, }, ]; diff --git a/src/workers/notifications/commentReply.ts b/src/workers/notifications/commentReply.ts index c27089b27..59460698b 100644 --- a/src/workers/notifications/commentReply.ts +++ b/src/workers/notifications/commentReply.ts @@ -66,6 +66,7 @@ const worker: NotificationWorker = { type, ctx: { ...ctx, + initiatorId: commenter.id, userIds: userIds.filter((id) => mutes.every(({ userId }) => userId !== id), ), diff --git a/src/workers/notifications/postAdded.ts b/src/workers/notifications/postAdded.ts index 0882240cb..e23a21736 100644 --- a/src/workers/notifications/postAdded.ts +++ b/src/workers/notifications/postAdded.ts @@ -100,6 +100,7 @@ const worker: NotificationWorker = { ctx: { ...baseCtx, doneBy, + initiatorId: post.authorId, userIds: members.map(({ userId }) => userId), } as NotificationPostContext & Partial, }); diff --git a/src/workers/notifications/postAddedUserNotification.ts b/src/workers/notifications/postAddedUserNotification.ts index 69896e778..919ad86ca 100644 --- a/src/workers/notifications/postAddedUserNotification.ts +++ b/src/workers/notifications/postAddedUserNotification.ts @@ -95,6 +95,7 @@ export const postAddedUserNotification = ...baseCtx, userIds: [], user: item, + initiatorId: post.authorId, }, }); diff --git a/src/workers/notifications/postMention.ts b/src/workers/notifications/postMention.ts index 9cadcd6ea..bd19fdf28 100644 --- a/src/workers/notifications/postMention.ts +++ b/src/workers/notifications/postMention.ts @@ -42,6 +42,7 @@ const worker: NotificationWorker = { const ctx: NotificationPostContext & NotificationDoneByContext = { ...postCtx, + initiatorId: mentionedByUserId, userIds: [mentionedUserId], doneBy, doneTo, diff --git a/src/workers/notifications/utils.ts b/src/workers/notifications/utils.ts index 9367e3e2f..87e23ce01 100644 --- a/src/workers/notifications/utils.ts +++ b/src/workers/notifications/utils.ts @@ -185,6 +185,7 @@ export async function articleNewCommentHandler( userIds: users.filter((id) => muted.every(({ userId }) => userId !== id), ), + initiatorId: post.authorId, }, }, ];