From ea3674d04dd0a67962224073256dc7d4173527a5 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 18 Nov 2020 11:13:01 +0100 Subject: [PATCH] Remove notifications of muted accounts/servers --- .../api/server/server-blocklist.ts | 22 +- server/controllers/api/users/my-blocklist.ts | 14 + .../controllers/api/users/my-subscriptions.ts | 3 + server/models/account/user-notification.ts | 55 +++- .../api/moderation/blocklist-notification.ts | 258 ++++++++++++++++++ server/tests/api/moderation/index.ts | 1 + 6 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 server/tests/api/moderation/blocklist-notification.ts diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts index f849b15c7..78e8a7e09 100644 --- a/server/controllers/api/server/server-blocklist.ts +++ b/server/controllers/api/server/server-blocklist.ts @@ -1,6 +1,11 @@ -import * as express from 'express' import 'multer' +import * as express from 'express' +import { logger } from '@server/helpers/logger' +import { UserNotificationModel } from '@server/models/account/user-notification' +import { getServerActor } from '@server/models/application/application' +import { UserRight } from '../../../../shared/models/users' import { getFormattedObjects } from '../../../helpers/utils' +import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' import { asyncMiddleware, asyncRetryTransactionMiddleware, @@ -19,10 +24,7 @@ import { unblockServerByServerValidator } from '../../../middlewares/validators' import { AccountBlocklistModel } from '../../../models/account/account-blocklist' -import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' import { ServerBlocklistModel } from '../../../models/server/server-blocklist' -import { UserRight } from '../../../../shared/models/users' -import { getServerActor } from '@server/models/application/application' const serverBlocklistRouter = express.Router() @@ -100,6 +102,12 @@ async function blockAccount (req: express.Request, res: express.Response) { await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id) + UserNotificationModel.removeNotificationsOf({ + id: accountToBlock.id, + type: 'account', + forUserId: null // For all users + }).catch(err => logger.error('Cannot remove notifications after an account mute.', { err })) + return res.status(204).end() } @@ -131,6 +139,12 @@ async function blockServer (req: express.Request, res: express.Response) { await addServerInBlocklist(serverActor.Account.id, serverToBlock.id) + UserNotificationModel.removeNotificationsOf({ + id: serverToBlock.id, + type: 'server', + forUserId: null // For all users + }).catch(err => logger.error('Cannot remove notifications after a server mute.', { err })) + return res.status(204).end() } diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts index 3a44376f2..6eba22d52 100644 --- a/server/controllers/api/users/my-blocklist.ts +++ b/server/controllers/api/users/my-blocklist.ts @@ -20,6 +20,8 @@ import { import { AccountBlocklistModel } from '../../../models/account/account-blocklist' import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' import { ServerBlocklistModel } from '../../../models/server/server-blocklist' +import { UserNotificationModel } from '@server/models/account/user-notification' +import { logger } from '@server/helpers/logger' const myBlocklistRouter = express.Router() @@ -91,6 +93,12 @@ async function blockAccount (req: express.Request, res: express.Response) { await addAccountInBlocklist(user.Account.id, accountToBlock.id) + UserNotificationModel.removeNotificationsOf({ + id: accountToBlock.id, + type: 'account', + forUserId: user.id + }).catch(err => logger.error('Cannot remove notifications after an account mute.', { err })) + return res.status(204).end() } @@ -122,6 +130,12 @@ async function blockServer (req: express.Request, res: express.Response) { await addServerInBlocklist(user.Account.id, serverToBlock.id) + UserNotificationModel.removeNotificationsOf({ + id: serverToBlock.id, + type: 'server', + forUserId: user.id + }).catch(err => logger.error('Cannot remove notifications after a server mute.', { err })) + return res.status(204).end() } diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index b8c234eef..66b33c477 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts @@ -26,6 +26,7 @@ import { } from '../../../middlewares/validators' import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { VideoModel } from '../../../models/video/video' +import { sendUndoFollow } from '@server/lib/activitypub/send' const mySubscriptionsRouter = express.Router() @@ -138,6 +139,8 @@ async function deleteUserSubscription (req: express.Request, res: express.Respon const subscription = res.locals.subscription await sequelizeTypescript.transaction(async t => { + if (subscription.state === 'accepted') await sendUndoFollow(subscription, t) + return subscription.destroy({ transaction: t }) }) diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index bd89b8973..452574dc8 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts @@ -105,7 +105,7 @@ function buildAccountInclude (required: boolean, withActor = false) { include: [ { attributes: [ 'id', 'originCommentId' ], - model: VideoCommentModel, + model: VideoCommentModel.unscoped(), required: true, include: [ { @@ -411,6 +411,59 @@ export class UserNotificationModel extends Model { return UserNotificationModel.update({ read: true }, query) } + static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) { + const id = parseInt(options.id + '', 10) + + function buildAccountWhereQuery (base: string) { + const whereSuffix = options.forUserId + ? ` AND "userNotification"."userId" = ${options.forUserId}` + : '' + + if (options.type === 'account') { + return base + + ` WHERE "account"."id" = ${id} ${whereSuffix}` + } + + return base + + ` WHERE "actor"."serverId" = ${id} ${whereSuffix}` + } + + const queries = [ + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + ), + + // Remove notifications from muted accounts that followed ours + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` + ), + + // Remove notifications from muted accounts that commented something + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` + ), + + buildAccountWhereQuery( + `SELECT "userNotification"."id" FROM "userNotification" ` + + `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` + + `INNER JOIN account ON account.id = "videoComment"."accountId" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + ) + ] + + const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})` + + return UserNotificationModel.sequelize.query(query) + } + toFormattedJSON (this: UserNotificationModelForApi): UserNotification { const video = this.Video ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) diff --git a/server/tests/api/moderation/blocklist-notification.ts b/server/tests/api/moderation/blocklist-notification.ts new file mode 100644 index 000000000..4fb3c95f2 --- /dev/null +++ b/server/tests/api/moderation/blocklist-notification.ts @@ -0,0 +1,258 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import 'mocha' +import * as chai from 'chai' +import { getUserNotifications, markAsReadAllNotifications } from '@shared/extra-utils/users/user-notifications' +import { addUserSubscription, removeUserSubscription } from '@shared/extra-utils/users/user-subscriptions' +import { UserNotification, UserNotificationType } from '@shared/models' +import { + cleanupTests, + createUser, + doubleFollow, + flushAndRunMultipleServers, + ServerInfo, + uploadVideo, + userLogin +} from '../../../../shared/extra-utils/index' +import { waitJobs } from '../../../../shared/extra-utils/server/jobs' +import { + addAccountToAccountBlocklist, + addAccountToServerBlocklist, + addServerToAccountBlocklist, + addServerToServerBlocklist, + removeAccountFromAccountBlocklist, + removeAccountFromServerBlocklist, + removeServerFromAccountBlocklist +} from '../../../../shared/extra-utils/users/blocklist' +import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' +import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' + +const expect = chai.expect + +async function checkNotifications (url: string, token: string, expected: UserNotificationType[]) { + const res = await getUserNotifications(url, token, 0, 10, true) + + const notifications: UserNotification[] = res.body.data + + expect(notifications).to.have.lengthOf(expected.length) + + for (const type of expected) { + expect(notifications.find(n => n.type === type)).to.exist + } +} + +describe('Test blocklist', function () { + let servers: ServerInfo[] + let videoUUID: string + + let userToken1: string + let userToken2: string + let remoteUserToken: string + + async function resetState () { + try { + await removeUserSubscription(servers[1].url, remoteUserToken, 'user1_channel@' + servers[0].host) + await removeUserSubscription(servers[1].url, remoteUserToken, 'user2_channel@' + servers[0].host) + } catch {} + + await waitJobs(servers) + + await markAsReadAllNotifications(servers[0].url, userToken1) + await markAsReadAllNotifications(servers[0].url, userToken2) + + { + const res = await uploadVideo(servers[0].url, userToken1, { name: 'video' }) + videoUUID = res.body.video.uuid + + await waitJobs(servers) + } + + { + await addVideoCommentThread(servers[1].url, remoteUserToken, videoUUID, '@user2@' + servers[0].host + ' hello') + } + + { + + await addUserSubscription(servers[1].url, remoteUserToken, 'user1_channel@' + servers[0].host) + await addUserSubscription(servers[1].url, remoteUserToken, 'user2_channel@' + servers[0].host) + } + + await waitJobs(servers) + } + + before(async function () { + this.timeout(60000) + + servers = await flushAndRunMultipleServers(2) + await setAccessTokensToServers(servers) + + { + const user = { username: 'user1', password: 'password' } + await createUser({ + url: servers[0].url, + accessToken: servers[0].accessToken, + username: user.username, + password: user.password, + videoQuota: -1, + videoQuotaDaily: -1 + }) + + userToken1 = await userLogin(servers[0], user) + await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' }) + } + + { + const user = { username: 'user2', password: 'password' } + await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password }) + + userToken2 = await userLogin(servers[0], user) + } + + { + const user = { username: 'user3', password: 'password' } + await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password }) + + remoteUserToken = await userLogin(servers[1], user) + } + + await doubleFollow(servers[0], servers[1]) + }) + + describe('User blocks another user', function () { + + before(async function () { + this.timeout(30000) + + await resetState() + }) + + it('Should have appropriate notifications', async function () { + const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] + await checkNotifications(servers[0].url, userToken1, notifs) + }) + + it('Should block an account', async function () { + this.timeout(10000) + + await addAccountToAccountBlocklist(servers[0].url, userToken1, 'user3@' + servers[1].host) + await waitJobs(servers) + }) + + it('Should not have notifications from this account', async function () { + await checkNotifications(servers[0].url, userToken1, []) + }) + + it('Should have notifications of this account on user 2', async function () { + const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] + + await checkNotifications(servers[0].url, userToken2, notifs) + + await removeAccountFromAccountBlocklist(servers[0].url, userToken1, 'user3@' + servers[1].host) + }) + }) + + describe('User blocks another server', function () { + + before(async function () { + this.timeout(30000) + + await resetState() + }) + + it('Should have appropriate notifications', async function () { + const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] + await checkNotifications(servers[0].url, userToken1, notifs) + }) + + it('Should block an account', async function () { + this.timeout(10000) + + await addServerToAccountBlocklist(servers[0].url, userToken1, servers[1].host) + await waitJobs(servers) + }) + + it('Should not have notifications from this account', async function () { + await checkNotifications(servers[0].url, userToken1, []) + }) + + it('Should have notifications of this account on user 2', async function () { + const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] + + await checkNotifications(servers[0].url, userToken2, notifs) + + await removeServerFromAccountBlocklist(servers[0].url, userToken1, servers[1].host) + }) + }) + + describe('Server blocks a user', function () { + + before(async function () { + this.timeout(30000) + + await resetState() + }) + + it('Should have appropriate notifications', async function () { + { + const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] + await checkNotifications(servers[0].url, userToken1, notifs) + } + + { + const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] + await checkNotifications(servers[0].url, userToken2, notifs) + } + }) + + it('Should block an account', async function () { + this.timeout(10000) + + await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'user3@' + servers[1].host) + await waitJobs(servers) + }) + + it('Should not have notifications from this account', async function () { + await checkNotifications(servers[0].url, userToken1, []) + await checkNotifications(servers[0].url, userToken2, []) + + await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'user3@' + servers[1].host) + }) + }) + + describe('Server blocks a server', function () { + + before(async function () { + this.timeout(30000) + + await resetState() + }) + + it('Should have appropriate notifications', async function () { + { + const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ] + await checkNotifications(servers[0].url, userToken1, notifs) + } + + { + const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ] + await checkNotifications(servers[0].url, userToken2, notifs) + } + }) + + it('Should block an account', async function () { + this.timeout(10000) + + await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host) + await waitJobs(servers) + }) + + it('Should not have notifications from this account', async function () { + await checkNotifications(servers[0].url, userToken1, []) + await checkNotifications(servers[0].url, userToken2, []) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/server/tests/api/moderation/index.ts b/server/tests/api/moderation/index.ts index 6593c001f..874be03d5 100644 --- a/server/tests/api/moderation/index.ts +++ b/server/tests/api/moderation/index.ts @@ -1,3 +1,4 @@ export * from './abuses' +export * from './blocklist-notification' export * from './blocklist' export * from './video-blacklist'