]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Remove notifications of muted accounts/servers
authorChocobozzz <me@florianbigard.com>
Wed, 18 Nov 2020 10:13:01 +0000 (11:13 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 18 Nov 2020 10:32:57 +0000 (11:32 +0100)
server/controllers/api/server/server-blocklist.ts
server/controllers/api/users/my-blocklist.ts
server/controllers/api/users/my-subscriptions.ts
server/models/account/user-notification.ts
server/tests/api/moderation/blocklist-notification.ts [new file with mode: 0644]
server/tests/api/moderation/index.ts

index f849b15c76e7a86c4628164d614de2696c5bfddd..78e8a7e09dd99d5295ec66f76d07aa32e7c16cc5 100644 (file)
@@ -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()
 }
 
index 3a44376f2d3424ac637b9dabf4ffe72476ed5003..6eba22d52ca0e3973c66668362edfb70577774aa 100644 (file)
@@ -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()
 }
 
index b8c234eef755c4b4566bbf534b35d71d7b7b4249..66b33c477f0ddd2f57b5ccd3bdc56b04f75db6e5 100644 (file)
@@ -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 })
   })
 
index bd89b8973e0a28186a8230d40708946b0752375a..452574dc882faf77eb1c0eab70031c8b2e938ea4 100644 (file)
@@ -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<UserNotificationModel> {
     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 (file)
index 0000000..4fb3c95
--- /dev/null
@@ -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)
+  })
+})
index 6593c001f78e433487ea78c75a146a0c3d415699..874be03d5fdff68b39dc56016c9d897c0764c997 100644 (file)
@@ -1,3 +1,4 @@
 export * from './abuses'
+export * from './blocklist-notification'
 export * from './blocklist'
 export * from './video-blacklist'