aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/server/server-blocklist.ts22
-rw-r--r--server/controllers/api/users/my-blocklist.ts14
-rw-r--r--server/controllers/api/users/my-subscriptions.ts3
-rw-r--r--server/models/account/user-notification.ts55
-rw-r--r--server/tests/api/moderation/blocklist-notification.ts258
-rw-r--r--server/tests/api/moderation/index.ts1
6 files changed, 348 insertions, 5 deletions
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 @@
1import * as express from 'express'
2import 'multer' 1import 'multer'
2import * as express from 'express'
3import { logger } from '@server/helpers/logger'
4import { UserNotificationModel } from '@server/models/account/user-notification'
5import { getServerActor } from '@server/models/application/application'
6import { UserRight } from '../../../../shared/models/users'
3import { getFormattedObjects } from '../../../helpers/utils' 7import { getFormattedObjects } from '../../../helpers/utils'
8import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
4import { 9import {
5 asyncMiddleware, 10 asyncMiddleware,
6 asyncRetryTransactionMiddleware, 11 asyncRetryTransactionMiddleware,
@@ -19,10 +24,7 @@ import {
19 unblockServerByServerValidator 24 unblockServerByServerValidator
20} from '../../../middlewares/validators' 25} from '../../../middlewares/validators'
21import { AccountBlocklistModel } from '../../../models/account/account-blocklist' 26import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
22import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
23import { ServerBlocklistModel } from '../../../models/server/server-blocklist' 27import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
24import { UserRight } from '../../../../shared/models/users'
25import { getServerActor } from '@server/models/application/application'
26 28
27const serverBlocklistRouter = express.Router() 29const serverBlocklistRouter = express.Router()
28 30
@@ -100,6 +102,12 @@ async function blockAccount (req: express.Request, res: express.Response) {
100 102
101 await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id) 103 await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id)
102 104
105 UserNotificationModel.removeNotificationsOf({
106 id: accountToBlock.id,
107 type: 'account',
108 forUserId: null // For all users
109 }).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
110
103 return res.status(204).end() 111 return res.status(204).end()
104} 112}
105 113
@@ -131,6 +139,12 @@ async function blockServer (req: express.Request, res: express.Response) {
131 139
132 await addServerInBlocklist(serverActor.Account.id, serverToBlock.id) 140 await addServerInBlocklist(serverActor.Account.id, serverToBlock.id)
133 141
142 UserNotificationModel.removeNotificationsOf({
143 id: serverToBlock.id,
144 type: 'server',
145 forUserId: null // For all users
146 }).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
147
134 return res.status(204).end() 148 return res.status(204).end()
135} 149}
136 150
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 {
20import { AccountBlocklistModel } from '../../../models/account/account-blocklist' 20import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
21import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' 21import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
22import { ServerBlocklistModel } from '../../../models/server/server-blocklist' 22import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
23import { UserNotificationModel } from '@server/models/account/user-notification'
24import { logger } from '@server/helpers/logger'
23 25
24const myBlocklistRouter = express.Router() 26const myBlocklistRouter = express.Router()
25 27
@@ -91,6 +93,12 @@ async function blockAccount (req: express.Request, res: express.Response) {
91 93
92 await addAccountInBlocklist(user.Account.id, accountToBlock.id) 94 await addAccountInBlocklist(user.Account.id, accountToBlock.id)
93 95
96 UserNotificationModel.removeNotificationsOf({
97 id: accountToBlock.id,
98 type: 'account',
99 forUserId: user.id
100 }).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
101
94 return res.status(204).end() 102 return res.status(204).end()
95} 103}
96 104
@@ -122,6 +130,12 @@ async function blockServer (req: express.Request, res: express.Response) {
122 130
123 await addServerInBlocklist(user.Account.id, serverToBlock.id) 131 await addServerInBlocklist(user.Account.id, serverToBlock.id)
124 132
133 UserNotificationModel.removeNotificationsOf({
134 id: serverToBlock.id,
135 type: 'server',
136 forUserId: user.id
137 }).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
138
125 return res.status(204).end() 139 return res.status(204).end()
126} 140}
127 141
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 {
26} from '../../../middlewares/validators' 26} from '../../../middlewares/validators'
27import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 27import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
28import { VideoModel } from '../../../models/video/video' 28import { VideoModel } from '../../../models/video/video'
29import { sendUndoFollow } from '@server/lib/activitypub/send'
29 30
30const mySubscriptionsRouter = express.Router() 31const mySubscriptionsRouter = express.Router()
31 32
@@ -138,6 +139,8 @@ async function deleteUserSubscription (req: express.Request, res: express.Respon
138 const subscription = res.locals.subscription 139 const subscription = res.locals.subscription
139 140
140 await sequelizeTypescript.transaction(async t => { 141 await sequelizeTypescript.transaction(async t => {
142 if (subscription.state === 'accepted') await sendUndoFollow(subscription, t)
143
141 return subscription.destroy({ transaction: t }) 144 return subscription.destroy({ transaction: t })
142 }) 145 })
143 146
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) {
105 include: [ 105 include: [
106 { 106 {
107 attributes: [ 'id', 'originCommentId' ], 107 attributes: [ 'id', 'originCommentId' ],
108 model: VideoCommentModel, 108 model: VideoCommentModel.unscoped(),
109 required: true, 109 required: true,
110 include: [ 110 include: [
111 { 111 {
@@ -411,6 +411,59 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
411 return UserNotificationModel.update({ read: true }, query) 411 return UserNotificationModel.update({ read: true }, query)
412 } 412 }
413 413
414 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
415 const id = parseInt(options.id + '', 10)
416
417 function buildAccountWhereQuery (base: string) {
418 const whereSuffix = options.forUserId
419 ? ` AND "userNotification"."userId" = ${options.forUserId}`
420 : ''
421
422 if (options.type === 'account') {
423 return base +
424 ` WHERE "account"."id" = ${id} ${whereSuffix}`
425 }
426
427 return base +
428 ` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
429 }
430
431 const queries = [
432 buildAccountWhereQuery(
433 `SELECT "userNotification"."id" FROM "userNotification" ` +
434 `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` +
435 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
436 ),
437
438 // Remove notifications from muted accounts that followed ours
439 buildAccountWhereQuery(
440 `SELECT "userNotification"."id" FROM "userNotification" ` +
441 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
442 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
443 `INNER JOIN account ON account."actorId" = actor.id `
444 ),
445
446 // Remove notifications from muted accounts that commented something
447 buildAccountWhereQuery(
448 `SELECT "userNotification"."id" FROM "userNotification" ` +
449 `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
450 `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
451 `INNER JOIN account ON account."actorId" = actor.id `
452 ),
453
454 buildAccountWhereQuery(
455 `SELECT "userNotification"."id" FROM "userNotification" ` +
456 `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` +
457 `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
458 `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
459 )
460 ]
461
462 const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
463
464 return UserNotificationModel.sequelize.query(query)
465 }
466
414 toFormattedJSON (this: UserNotificationModelForApi): UserNotification { 467 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
415 const video = this.Video 468 const video = this.Video
416 ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) 469 ? 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 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { getUserNotifications, markAsReadAllNotifications } from '@shared/extra-utils/users/user-notifications'
6import { addUserSubscription, removeUserSubscription } from '@shared/extra-utils/users/user-subscriptions'
7import { UserNotification, UserNotificationType } from '@shared/models'
8import {
9 cleanupTests,
10 createUser,
11 doubleFollow,
12 flushAndRunMultipleServers,
13 ServerInfo,
14 uploadVideo,
15 userLogin
16} from '../../../../shared/extra-utils/index'
17import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
18import {
19 addAccountToAccountBlocklist,
20 addAccountToServerBlocklist,
21 addServerToAccountBlocklist,
22 addServerToServerBlocklist,
23 removeAccountFromAccountBlocklist,
24 removeAccountFromServerBlocklist,
25 removeServerFromAccountBlocklist
26} from '../../../../shared/extra-utils/users/blocklist'
27import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
28import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
29
30const expect = chai.expect
31
32async function checkNotifications (url: string, token: string, expected: UserNotificationType[]) {
33 const res = await getUserNotifications(url, token, 0, 10, true)
34
35 const notifications: UserNotification[] = res.body.data
36
37 expect(notifications).to.have.lengthOf(expected.length)
38
39 for (const type of expected) {
40 expect(notifications.find(n => n.type === type)).to.exist
41 }
42}
43
44describe('Test blocklist', function () {
45 let servers: ServerInfo[]
46 let videoUUID: string
47
48 let userToken1: string
49 let userToken2: string
50 let remoteUserToken: string
51
52 async function resetState () {
53 try {
54 await removeUserSubscription(servers[1].url, remoteUserToken, 'user1_channel@' + servers[0].host)
55 await removeUserSubscription(servers[1].url, remoteUserToken, 'user2_channel@' + servers[0].host)
56 } catch {}
57
58 await waitJobs(servers)
59
60 await markAsReadAllNotifications(servers[0].url, userToken1)
61 await markAsReadAllNotifications(servers[0].url, userToken2)
62
63 {
64 const res = await uploadVideo(servers[0].url, userToken1, { name: 'video' })
65 videoUUID = res.body.video.uuid
66
67 await waitJobs(servers)
68 }
69
70 {
71 await addVideoCommentThread(servers[1].url, remoteUserToken, videoUUID, '@user2@' + servers[0].host + ' hello')
72 }
73
74 {
75
76 await addUserSubscription(servers[1].url, remoteUserToken, 'user1_channel@' + servers[0].host)
77 await addUserSubscription(servers[1].url, remoteUserToken, 'user2_channel@' + servers[0].host)
78 }
79
80 await waitJobs(servers)
81 }
82
83 before(async function () {
84 this.timeout(60000)
85
86 servers = await flushAndRunMultipleServers(2)
87 await setAccessTokensToServers(servers)
88
89 {
90 const user = { username: 'user1', password: 'password' }
91 await createUser({
92 url: servers[0].url,
93 accessToken: servers[0].accessToken,
94 username: user.username,
95 password: user.password,
96 videoQuota: -1,
97 videoQuotaDaily: -1
98 })
99
100 userToken1 = await userLogin(servers[0], user)
101 await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
102 }
103
104 {
105 const user = { username: 'user2', password: 'password' }
106 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
107
108 userToken2 = await userLogin(servers[0], user)
109 }
110
111 {
112 const user = { username: 'user3', password: 'password' }
113 await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
114
115 remoteUserToken = await userLogin(servers[1], user)
116 }
117
118 await doubleFollow(servers[0], servers[1])
119 })
120
121 describe('User blocks another user', function () {
122
123 before(async function () {
124 this.timeout(30000)
125
126 await resetState()
127 })
128
129 it('Should have appropriate notifications', async function () {
130 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
131 await checkNotifications(servers[0].url, userToken1, notifs)
132 })
133
134 it('Should block an account', async function () {
135 this.timeout(10000)
136
137 await addAccountToAccountBlocklist(servers[0].url, userToken1, 'user3@' + servers[1].host)
138 await waitJobs(servers)
139 })
140
141 it('Should not have notifications from this account', async function () {
142 await checkNotifications(servers[0].url, userToken1, [])
143 })
144
145 it('Should have notifications of this account on user 2', async function () {
146 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
147
148 await checkNotifications(servers[0].url, userToken2, notifs)
149
150 await removeAccountFromAccountBlocklist(servers[0].url, userToken1, 'user3@' + servers[1].host)
151 })
152 })
153
154 describe('User blocks another server', function () {
155
156 before(async function () {
157 this.timeout(30000)
158
159 await resetState()
160 })
161
162 it('Should have appropriate notifications', async function () {
163 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
164 await checkNotifications(servers[0].url, userToken1, notifs)
165 })
166
167 it('Should block an account', async function () {
168 this.timeout(10000)
169
170 await addServerToAccountBlocklist(servers[0].url, userToken1, servers[1].host)
171 await waitJobs(servers)
172 })
173
174 it('Should not have notifications from this account', async function () {
175 await checkNotifications(servers[0].url, userToken1, [])
176 })
177
178 it('Should have notifications of this account on user 2', async function () {
179 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
180
181 await checkNotifications(servers[0].url, userToken2, notifs)
182
183 await removeServerFromAccountBlocklist(servers[0].url, userToken1, servers[1].host)
184 })
185 })
186
187 describe('Server blocks a user', function () {
188
189 before(async function () {
190 this.timeout(30000)
191
192 await resetState()
193 })
194
195 it('Should have appropriate notifications', async function () {
196 {
197 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
198 await checkNotifications(servers[0].url, userToken1, notifs)
199 }
200
201 {
202 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
203 await checkNotifications(servers[0].url, userToken2, notifs)
204 }
205 })
206
207 it('Should block an account', async function () {
208 this.timeout(10000)
209
210 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'user3@' + servers[1].host)
211 await waitJobs(servers)
212 })
213
214 it('Should not have notifications from this account', async function () {
215 await checkNotifications(servers[0].url, userToken1, [])
216 await checkNotifications(servers[0].url, userToken2, [])
217
218 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'user3@' + servers[1].host)
219 })
220 })
221
222 describe('Server blocks a server', function () {
223
224 before(async function () {
225 this.timeout(30000)
226
227 await resetState()
228 })
229
230 it('Should have appropriate notifications', async function () {
231 {
232 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
233 await checkNotifications(servers[0].url, userToken1, notifs)
234 }
235
236 {
237 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
238 await checkNotifications(servers[0].url, userToken2, notifs)
239 }
240 })
241
242 it('Should block an account', async function () {
243 this.timeout(10000)
244
245 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
246 await waitJobs(servers)
247 })
248
249 it('Should not have notifications from this account', async function () {
250 await checkNotifications(servers[0].url, userToken1, [])
251 await checkNotifications(servers[0].url, userToken2, [])
252 })
253 })
254
255 after(async function () {
256 await cleanupTests(servers)
257 })
258})
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 @@
1export * from './abuses' 1export * from './abuses'
2export * from './blocklist-notification'
2export * from './blocklist' 3export * from './blocklist'
3export * from './video-blacklist' 4export * from './video-blacklist'