diff options
-rw-r--r-- | server/controllers/api/server/server-blocklist.ts | 22 | ||||
-rw-r--r-- | server/controllers/api/users/my-blocklist.ts | 14 | ||||
-rw-r--r-- | server/controllers/api/users/my-subscriptions.ts | 3 | ||||
-rw-r--r-- | server/models/account/user-notification.ts | 55 | ||||
-rw-r--r-- | server/tests/api/moderation/blocklist-notification.ts | 258 | ||||
-rw-r--r-- | server/tests/api/moderation/index.ts | 1 |
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 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | import { UserNotificationModel } from '@server/models/account/user-notification' | ||
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { UserRight } from '../../../../shared/models/users' | ||
3 | import { getFormattedObjects } from '../../../helpers/utils' | 7 | import { getFormattedObjects } from '../../../helpers/utils' |
8 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | ||
4 | import { | 9 | import { |
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' |
21 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | 26 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' |
22 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | ||
23 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | 27 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' |
24 | import { UserRight } from '../../../../shared/models/users' | ||
25 | import { getServerActor } from '@server/models/application/application' | ||
26 | 28 | ||
27 | const serverBlocklistRouter = express.Router() | 29 | const 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 { | |||
20 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | 20 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' |
21 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | 21 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' |
22 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | 22 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' |
23 | import { UserNotificationModel } from '@server/models/account/user-notification' | ||
24 | import { logger } from '@server/helpers/logger' | ||
23 | 25 | ||
24 | const myBlocklistRouter = express.Router() | 26 | const 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' |
27 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 27 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
28 | import { VideoModel } from '../../../models/video/video' | 28 | import { VideoModel } from '../../../models/video/video' |
29 | import { sendUndoFollow } from '@server/lib/activitypub/send' | ||
29 | 30 | ||
30 | const mySubscriptionsRouter = express.Router() | 31 | const 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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { getUserNotifications, markAsReadAllNotifications } from '@shared/extra-utils/users/user-notifications' | ||
6 | import { addUserSubscription, removeUserSubscription } from '@shared/extra-utils/users/user-subscriptions' | ||
7 | import { UserNotification, UserNotificationType } from '@shared/models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createUser, | ||
11 | doubleFollow, | ||
12 | flushAndRunMultipleServers, | ||
13 | ServerInfo, | ||
14 | uploadVideo, | ||
15 | userLogin | ||
16 | } from '../../../../shared/extra-utils/index' | ||
17 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
18 | import { | ||
19 | addAccountToAccountBlocklist, | ||
20 | addAccountToServerBlocklist, | ||
21 | addServerToAccountBlocklist, | ||
22 | addServerToServerBlocklist, | ||
23 | removeAccountFromAccountBlocklist, | ||
24 | removeAccountFromServerBlocklist, | ||
25 | removeServerFromAccountBlocklist | ||
26 | } from '../../../../shared/extra-utils/users/blocklist' | ||
27 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | ||
28 | import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' | ||
29 | |||
30 | const expect = chai.expect | ||
31 | |||
32 | async 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 | |||
44 | describe('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 @@ | |||
1 | export * from './abuses' | 1 | export * from './abuses' |
2 | export * from './blocklist-notification' | ||
2 | export * from './blocklist' | 3 | export * from './blocklist' |
3 | export * from './video-blacklist' | 4 | export * from './video-blacklist' |