diff options
author | Chocobozzz <me@florianbigard.com> | 2019-08-30 16:50:12 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-09-04 16:24:58 +0200 |
commit | 8424c4026afd7304880a4ce8138a04ffb3d8c938 (patch) | |
tree | 5b42625a59307b03333aa7d293b40b4c90da8f73 /server | |
parent | f69ec5f340638ef577e8f5b9b1fb844778656a1f (diff) | |
download | PeerTube-8424c4026afd7304880a4ce8138a04ffb3d8c938.tar.gz PeerTube-8424c4026afd7304880a4ce8138a04ffb3d8c938.tar.zst PeerTube-8424c4026afd7304880a4ce8138a04ffb3d8c938.zip |
Add auto follow back support for instances
Diffstat (limited to 'server')
32 files changed, 535 insertions, 144 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 21fa85a08..0c52bfa7a 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -300,6 +300,18 @@ function customConfig (): CustomConfig { | |||
300 | enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED, | 300 | enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED, |
301 | manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL | 301 | manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL |
302 | } | 302 | } |
303 | }, | ||
304 | followings: { | ||
305 | instance: { | ||
306 | autoFollowBack: { | ||
307 | enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED | ||
308 | }, | ||
309 | |||
310 | autoFollowIndex: { | ||
311 | enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED, | ||
312 | indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL | ||
313 | } | ||
314 | } | ||
303 | } | 315 | } |
304 | } | 316 | } |
305 | } | 317 | } |
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index d38ce91de..37647622b 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -25,6 +25,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | |||
25 | import { JobQueue } from '../../../lib/job-queue' | 25 | import { JobQueue } from '../../../lib/job-queue' |
26 | import { removeRedundancyOf } from '../../../lib/redundancy' | 26 | import { removeRedundancyOf } from '../../../lib/redundancy' |
27 | import { sequelizeTypescript } from '../../../initializers/database' | 27 | import { sequelizeTypescript } from '../../../initializers/database' |
28 | import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' | ||
28 | 29 | ||
29 | const serverFollowsRouter = express.Router() | 30 | const serverFollowsRouter = express.Router() |
30 | serverFollowsRouter.get('/following', | 31 | serverFollowsRouter.get('/following', |
@@ -172,5 +173,7 @@ async function acceptFollower (req: express.Request, res: express.Response) { | |||
172 | follow.state = 'accepted' | 173 | follow.state = 'accepted' |
173 | await follow.save() | 174 | await follow.save() |
174 | 175 | ||
176 | await autoFollowBackIfNeeded(follow) | ||
177 | |||
175 | return res.status(204).end() | 178 | return res.status(204).end() |
176 | } | 179 | } |
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index f146284e4..017f5219e 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -76,7 +76,8 @@ async function updateNotificationSettings (req: express.Request, res: express.Re | |||
76 | newFollow: body.newFollow, | 76 | newFollow: body.newFollow, |
77 | newUserRegistration: body.newUserRegistration, | 77 | newUserRegistration: body.newUserRegistration, |
78 | commentMention: body.commentMention, | 78 | commentMention: body.commentMention, |
79 | newInstanceFollower: body.newInstanceFollower | 79 | newInstanceFollower: body.newInstanceFollower, |
80 | autoInstanceFollowing: body.autoInstanceFollowing | ||
80 | } | 81 | } |
81 | 82 | ||
82 | await UserNotificationSettingModel.update(values, query) | 83 | await UserNotificationSettingModel.update(values, query) |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 510f7d64d..599f3f5ac 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -232,6 +232,23 @@ const CONFIG = { | |||
232 | get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') } | 232 | get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') } |
233 | } | 233 | } |
234 | }, | 234 | }, |
235 | FOLLOWINGS: { | ||
236 | INSTANCE: { | ||
237 | AUTO_FOLLOW_BACK: { | ||
238 | get ENABLED () { | ||
239 | return config.get<boolean>('followings.instance.auto_follow_back.enabled') | ||
240 | } | ||
241 | }, | ||
242 | AUTO_FOLLOW_INDEX: { | ||
243 | get ENABLED () { | ||
244 | return config.get<boolean>('followings.instance.auto_follow_index.enabled') | ||
245 | }, | ||
246 | get INDEX_URL () { | ||
247 | return config.get<string>('followings.instance.auto_follow_index.index_url') | ||
248 | } | ||
249 | } | ||
250 | } | ||
251 | }, | ||
235 | THEME: { | 252 | THEME: { |
236 | get DEFAULT () { return config.get<string>('theme.default') } | 253 | get DEFAULT () { return config.get<string>('theme.default') } |
237 | } | 254 | } |
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts new file mode 100644 index 000000000..c57e43c91 --- /dev/null +++ b/server/lib/activitypub/follow.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import { MActorFollowActors } from '../../typings/models' | ||
2 | import { CONFIG } from '../../initializers/config' | ||
3 | import { SERVER_ACTOR_NAME } from '../../initializers/constants' | ||
4 | import { JobQueue } from '../job-queue' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { getServerActor } from '../../helpers/utils' | ||
7 | import { ServerModel } from '@server/models/server/server' | ||
8 | |||
9 | async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { | ||
10 | if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return | ||
11 | |||
12 | const follower = actorFollow.ActorFollower | ||
13 | |||
14 | if (follower.type === 'Application' && follower.preferredUsername === SERVER_ACTOR_NAME) { | ||
15 | logger.info('Auto follow back %s.', follower.url) | ||
16 | |||
17 | const me = await getServerActor() | ||
18 | |||
19 | const server = await ServerModel.load(follower.serverId) | ||
20 | const host = server.host | ||
21 | |||
22 | const payload = { | ||
23 | host, | ||
24 | name: SERVER_ACTOR_NAME, | ||
25 | followerActorId: me.id, | ||
26 | isAutoFollow: true | ||
27 | } | ||
28 | |||
29 | JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) | ||
30 | .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err)) | ||
31 | } | ||
32 | } | ||
33 | |||
34 | export { | ||
35 | autoFollowBackIfNeeded | ||
36 | } | ||
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 86f7c764d..dcfbb2c84 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts | |||
@@ -24,7 +24,7 @@ async function processAccept (actor: MActorDefault, targetActor: MActorSignature | |||
24 | if (!follow) throw new Error('Cannot find associated follow.') | 24 | if (!follow) throw new Error('Cannot find associated follow.') |
25 | 25 | ||
26 | if (follow.state !== 'accepted') { | 26 | if (follow.state !== 'accepted') { |
27 | follow.set('state', 'accepted') | 27 | follow.state = 'accepted' |
28 | await follow.save() | 28 | await follow.save() |
29 | 29 | ||
30 | await addFetchOutboxJob(targetActor) | 30 | await addFetchOutboxJob(targetActor) |
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index bc5660395..85f22d654 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts | |||
@@ -10,7 +10,8 @@ import { getAPId } from '../../../helpers/activitypub' | |||
10 | import { getServerActor } from '../../../helpers/utils' | 10 | import { getServerActor } from '../../../helpers/utils' |
11 | import { CONFIG } from '../../../initializers/config' | 11 | import { CONFIG } from '../../../initializers/config' |
12 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' | 12 | import { APProcessorOptions } from '../../../typings/activitypub-processor.model' |
13 | import { MAccount, MActorFollowActors, MActorFollowFull, MActorSignature } from '../../../typings/models' | 13 | import { MActorFollowActors, MActorSignature } from '../../../typings/models' |
14 | import { autoFollowBackIfNeeded } from '../follow' | ||
14 | 15 | ||
15 | async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { | 16 | async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { |
16 | const { activity, byActor } = options | 17 | const { activity, byActor } = options |
@@ -28,7 +29,7 @@ export { | |||
28 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
29 | 30 | ||
30 | async function processFollow (byActor: MActorSignature, targetActorURL: string) { | 31 | async function processFollow (byActor: MActorSignature, targetActorURL: string) { |
31 | const { actorFollow, created, isFollowingInstance } = await sequelizeTypescript.transaction(async t => { | 32 | const { actorFollow, created, isFollowingInstance, targetActor } = await sequelizeTypescript.transaction(async t => { |
32 | const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) | 33 | const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) |
33 | 34 | ||
34 | if (!targetActor) throw new Error('Unknown actor') | 35 | if (!targetActor) throw new Error('Unknown actor') |
@@ -67,21 +68,24 @@ async function processFollow (byActor: MActorSignature, targetActorURL: string) | |||
67 | actorFollow.ActorFollowing = targetActor | 68 | actorFollow.ActorFollowing = targetActor |
68 | 69 | ||
69 | // Target sends to actor he accepted the follow request | 70 | // Target sends to actor he accepted the follow request |
70 | if (actorFollow.state === 'accepted') await sendAccept(actorFollow) | 71 | if (actorFollow.state === 'accepted') { |
72 | await sendAccept(actorFollow) | ||
73 | await autoFollowBackIfNeeded(actorFollow) | ||
74 | } | ||
71 | 75 | ||
72 | return { actorFollow, created, isFollowingInstance } | 76 | return { actorFollow, created, isFollowingInstance, targetActor } |
73 | }) | 77 | }) |
74 | 78 | ||
75 | // Rejected | 79 | // Rejected |
76 | if (!actorFollow) return | 80 | if (!actorFollow) return |
77 | 81 | ||
78 | if (created) { | 82 | if (created) { |
83 | const follower = await ActorModel.loadFull(byActor.id) | ||
84 | const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower }) | ||
85 | |||
79 | if (isFollowingInstance) { | 86 | if (isFollowingInstance) { |
80 | Notifier.Instance.notifyOfNewInstanceFollow(actorFollow) | 87 | Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull) |
81 | } else { | 88 | } else { |
82 | const actorFollowFull = actorFollow as MActorFollowFull | ||
83 | actorFollowFull.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as MAccount | ||
84 | |||
85 | Notifier.Instance.notifyOfNewUserFollow(actorFollowFull) | 89 | Notifier.Instance.notifyOfNewUserFollow(actorFollowFull) |
86 | } | 90 | } |
87 | } | 91 | } |
diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts index 6b17b25da..ce400d8ff 100644 --- a/server/lib/activitypub/send/send-follow.ts +++ b/server/lib/activitypub/send/send-follow.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { ActivityFollow } from '../../../../shared/models/activitypub' | 1 | import { ActivityFollow } from '../../../../shared/models/activitypub' |
2 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
3 | import { getActorFollowActivityPubUrl } from '../url' | 2 | import { getActorFollowActivityPubUrl } from '../url' |
4 | import { unicastTo } from './utils' | 3 | import { unicastTo } from './utils' |
5 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 76349ef8f..bd3d4f252 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -6,8 +6,15 @@ import { JobQueue } from './job-queue' | |||
6 | import { EmailPayload } from './job-queue/handlers/email' | 6 | import { EmailPayload } from './job-queue/handlers/email' |
7 | import { readFileSync } from 'fs-extra' | 7 | import { readFileSync } from 'fs-extra' |
8 | import { WEBSERVER } from '../initializers/constants' | 8 | import { WEBSERVER } from '../initializers/constants' |
9 | import { MCommentOwnerVideo, MVideo, MVideoAbuseVideo, MVideoAccountLight, MVideoBlacklistVideo } from '../typings/models/video' | 9 | import { |
10 | import { MActorFollowActors, MActorFollowFollowingFullFollowerAccount, MUser } from '../typings/models' | 10 | MCommentOwnerVideo, |
11 | MVideo, | ||
12 | MVideoAbuseVideo, | ||
13 | MVideoAccountLight, | ||
14 | MVideoBlacklistLightVideo, | ||
15 | MVideoBlacklistVideo | ||
16 | } from '../typings/models/video' | ||
17 | import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' | ||
11 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' | 18 | import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' |
12 | 19 | ||
13 | type SendEmailOptions = { | 20 | type SendEmailOptions = { |
@@ -107,7 +114,7 @@ class Emailer { | |||
107 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 114 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
108 | } | 115 | } |
109 | 116 | ||
110 | addNewFollowNotification (to: string[], actorFollow: MActorFollowFollowingFullFollowerAccount, followType: 'account' | 'channel') { | 117 | addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { |
111 | const followerName = actorFollow.ActorFollower.Account.getDisplayName() | 118 | const followerName = actorFollow.ActorFollower.Account.getDisplayName() |
112 | const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() | 119 | const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() |
113 | 120 | ||
@@ -144,6 +151,22 @@ class Emailer { | |||
144 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 151 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
145 | } | 152 | } |
146 | 153 | ||
154 | addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { | ||
155 | const text = `Hi dear admin,\n\n` + | ||
156 | `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` + | ||
157 | `\n\n` + | ||
158 | `Cheers,\n` + | ||
159 | `${CONFIG.EMAIL.BODY.SIGNATURE}` | ||
160 | |||
161 | const emailPayload: EmailPayload = { | ||
162 | to, | ||
163 | subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following', | ||
164 | text | ||
165 | } | ||
166 | |||
167 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
168 | } | ||
169 | |||
147 | myVideoPublishedNotification (to: string[], video: MVideo) { | 170 | myVideoPublishedNotification (to: string[], video: MVideo) { |
148 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | 171 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() |
149 | 172 | ||
@@ -265,9 +288,9 @@ class Emailer { | |||
265 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 288 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
266 | } | 289 | } |
267 | 290 | ||
268 | addVideoAutoBlacklistModeratorsNotification (to: string[], video: MVideo) { | 291 | addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { |
269 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' | 292 | const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' |
270 | const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() | 293 | const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() |
271 | 294 | ||
272 | const text = `Hi,\n\n` + | 295 | const text = `Hi,\n\n` + |
273 | `A recently added video was auto-blacklisted and requires moderator review before publishing.` + | 296 | `A recently added video was auto-blacklisted and requires moderator review before publishing.` + |
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 5cb55cad6..af7c8a838 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts | |||
@@ -10,12 +10,13 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | |||
10 | import { ActorModel } from '../../../models/activitypub/actor' | 10 | import { ActorModel } from '../../../models/activitypub/actor' |
11 | import { Notifier } from '../../notifier' | 11 | import { Notifier } from '../../notifier' |
12 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
13 | import { MAccount, MActor, MActorFollowActors, MActorFollowFull, MActorFull } from '../../../typings/models' | 13 | import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models' |
14 | 14 | ||
15 | export type ActivitypubFollowPayload = { | 15 | export type ActivitypubFollowPayload = { |
16 | followerActorId: number | 16 | followerActorId: number |
17 | name: string | 17 | name: string |
18 | host: string | 18 | host: string |
19 | isAutoFollow?: boolean | ||
19 | } | 20 | } |
20 | 21 | ||
21 | async function processActivityPubFollow (job: Bull.Job) { | 22 | async function processActivityPubFollow (job: Bull.Job) { |
@@ -35,7 +36,7 @@ async function processActivityPubFollow (job: Bull.Job) { | |||
35 | 36 | ||
36 | const fromActor = await ActorModel.load(payload.followerActorId) | 37 | const fromActor = await ActorModel.load(payload.followerActorId) |
37 | 38 | ||
38 | return retryTransactionWrapper(follow, fromActor, targetActor) | 39 | return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow) |
39 | } | 40 | } |
40 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
41 | 42 | ||
@@ -45,7 +46,7 @@ export { | |||
45 | 46 | ||
46 | // --------------------------------------------------------------------------- | 47 | // --------------------------------------------------------------------------- |
47 | 48 | ||
48 | async function follow (fromActor: MActor, targetActor: MActorFull) { | 49 | async function follow (fromActor: MActor, targetActor: MActorFull, isAutoFollow = false) { |
49 | if (fromActor.id === targetActor.id) { | 50 | if (fromActor.id === targetActor.id) { |
50 | throw new Error('Follower is the same than target actor.') | 51 | throw new Error('Follower is the same than target actor.') |
51 | } | 52 | } |
@@ -75,14 +76,15 @@ async function follow (fromActor: MActor, targetActor: MActorFull) { | |||
75 | return actorFollow | 76 | return actorFollow |
76 | }) | 77 | }) |
77 | 78 | ||
78 | if (actorFollow.state === 'accepted') { | 79 | const followerFull = await ActorModel.loadFull(fromActor.id) |
79 | const followerFull = Object.assign(fromActor, { Account: await actorFollow.ActorFollower.$get('Account') as MAccount }) | ||
80 | 80 | ||
81 | const actorFollowFull = Object.assign(actorFollow, { | 81 | const actorFollowFull = Object.assign(actorFollow, { |
82 | ActorFollowing: targetActor, | 82 | ActorFollowing: targetActor, |
83 | ActorFollower: followerFull | 83 | ActorFollower: followerFull |
84 | }) | 84 | }) |
85 | 85 | ||
86 | Notifier.Instance.notifyOfNewUserFollow(actorFollowFull) | 86 | if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollowFull) |
87 | } | 87 | if (isAutoFollow === true) Notifier.Instance.notifyOfAutoInstanceFollowing(actorFollowFull) |
88 | |||
89 | return actorFollow | ||
88 | } | 90 | } |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index ff8c93328..93a3e9d90 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -21,6 +21,7 @@ import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumb | |||
21 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
22 | import { MThumbnail } from '../../../typings/models/video/thumbnail' | 22 | import { MThumbnail } from '../../../typings/models/video/thumbnail' |
23 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' | 23 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' |
24 | import { MVideoBlacklistVideo, MVideoBlacklist } from '@server/typings/models' | ||
24 | 25 | ||
25 | type VideoImportYoutubeDLPayload = { | 26 | type VideoImportYoutubeDLPayload = { |
26 | type: 'youtube-dl' | 27 | type: 'youtube-dl' |
@@ -204,7 +205,9 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
204 | Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) | 205 | Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) |
205 | 206 | ||
206 | if (video.isBlacklisted()) { | 207 | if (video.isBlacklisted()) { |
207 | Notifier.Instance.notifyOnVideoAutoBlacklist(video) | 208 | const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video }) |
209 | |||
210 | Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist) | ||
208 | } else { | 211 | } else { |
209 | Notifier.Instance.notifyOnNewVideoIfNeeded(video) | 212 | Notifier.Instance.notifyOnNewVideoIfNeeded(video) |
210 | } | 213 | } |
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index 23f76a21a..b7cc2607d 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -1,30 +1,30 @@ | |||
1 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' | 1 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' |
2 | import { logger } from '../helpers/logger' | 2 | import { logger } from '../helpers/logger' |
3 | import { VideoModel } from '../models/video/video' | ||
4 | import { Emailer } from './emailer' | 3 | import { Emailer } from './emailer' |
5 | import { UserNotificationModel } from '../models/account/user-notification' | 4 | import { UserNotificationModel } from '../models/account/user-notification' |
6 | import { VideoCommentModel } from '../models/video/video-comment' | ||
7 | import { UserModel } from '../models/account/user' | 5 | import { UserModel } from '../models/account/user' |
8 | import { PeerTubeSocket } from './peertube-socket' | 6 | import { PeerTubeSocket } from './peertube-socket' |
9 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
10 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' | 8 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' |
11 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | ||
12 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
13 | import { VideoImportModel } from '../models/video/video-import' | ||
14 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 10 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
15 | import { | 11 | import { |
16 | MCommentOwnerVideo, | 12 | MCommentOwnerVideo, |
17 | MVideo, | ||
18 | MVideoAbuseVideo, | 13 | MVideoAbuseVideo, |
19 | MVideoAccountLight, | 14 | MVideoAccountLight, |
15 | MVideoBlacklistLightVideo, | ||
20 | MVideoBlacklistVideo, | 16 | MVideoBlacklistVideo, |
21 | MVideoFullLight | 17 | MVideoFullLight |
22 | } from '../typings/models/video' | 18 | } from '../typings/models/video' |
23 | import { MUser, MUserAccount, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/typings/models/user' | 19 | import { |
24 | import { MActorFollowActors, MActorFollowFull, MActorFollowFollowingFullFollowerAccount } from '../typings/models' | 20 | MUser, |
25 | import { ActorFollowModel } from '../models/activitypub/actor-follow' | 21 | MUserDefault, |
22 | MUserNotifSettingAccount, | ||
23 | MUserWithNotificationSetting, | ||
24 | UserNotificationModelForApi | ||
25 | } from '@server/typings/models/user' | ||
26 | import { MActorFollowFull } from '../typings/models' | ||
26 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' | 27 | import { MVideoImportVideo } from '@server/typings/models/video/video-import' |
27 | import { AccountModel } from '@server/models/account/account' | ||
28 | 28 | ||
29 | class Notifier { | 29 | class Notifier { |
30 | 30 | ||
@@ -77,9 +77,9 @@ class Notifier { | |||
77 | .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) | 77 | .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) |
78 | } | 78 | } |
79 | 79 | ||
80 | notifyOnVideoAutoBlacklist (video: MVideo): void { | 80 | notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { |
81 | this.notifyModeratorsOfVideoAutoBlacklist(video) | 81 | this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist) |
82 | .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err })) | 82 | .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) |
83 | } | 83 | } |
84 | 84 | ||
85 | notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { | 85 | notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { |
@@ -87,7 +87,7 @@ class Notifier { | |||
87 | .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) | 87 | .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) |
88 | } | 88 | } |
89 | 89 | ||
90 | notifyOnVideoUnblacklist (video: MVideo): void { | 90 | notifyOnVideoUnblacklist (video: MVideoFullLight): void { |
91 | this.notifyVideoOwnerOfUnblacklist(video) | 91 | this.notifyVideoOwnerOfUnblacklist(video) |
92 | .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err })) | 92 | .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err })) |
93 | } | 93 | } |
@@ -97,12 +97,12 @@ class Notifier { | |||
97 | .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) | 97 | .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) |
98 | } | 98 | } |
99 | 99 | ||
100 | notifyOnNewUserRegistration (user: MUserAccount): void { | 100 | notifyOnNewUserRegistration (user: MUserDefault): void { |
101 | this.notifyModeratorsOfNewUserRegistration(user) | 101 | this.notifyModeratorsOfNewUserRegistration(user) |
102 | .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) | 102 | .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) |
103 | } | 103 | } |
104 | 104 | ||
105 | notifyOfNewUserFollow (actorFollow: MActorFollowFollowingFullFollowerAccount): void { | 105 | notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { |
106 | this.notifyUserOfNewActorFollow(actorFollow) | 106 | this.notifyUserOfNewActorFollow(actorFollow) |
107 | .catch(err => { | 107 | .catch(err => { |
108 | logger.error( | 108 | logger.error( |
@@ -114,30 +114,37 @@ class Notifier { | |||
114 | }) | 114 | }) |
115 | } | 115 | } |
116 | 116 | ||
117 | notifyOfNewInstanceFollow (actorFollow: MActorFollowActors): void { | 117 | notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { |
118 | this.notifyAdminsOfNewInstanceFollow(actorFollow) | 118 | this.notifyAdminsOfNewInstanceFollow(actorFollow) |
119 | .catch(err => { | 119 | .catch(err => { |
120 | logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err }) | 120 | logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err }) |
121 | }) | 121 | }) |
122 | } | 122 | } |
123 | 123 | ||
124 | notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void { | ||
125 | this.notifyAdminsOfAutoInstanceFollowing(actorFollow) | ||
126 | .catch(err => { | ||
127 | logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err }) | ||
128 | }) | ||
129 | } | ||
130 | |||
124 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { | 131 | private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { |
125 | // List all followers that are users | 132 | // List all followers that are users |
126 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) | 133 | const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) |
127 | 134 | ||
128 | logger.info('Notifying %d users of new video %s.', users.length, video.url) | 135 | logger.info('Notifying %d users of new video %s.', users.length, video.url) |
129 | 136 | ||
130 | function settingGetter (user: UserModel) { | 137 | function settingGetter (user: MUserWithNotificationSetting) { |
131 | return user.NotificationSetting.newVideoFromSubscription | 138 | return user.NotificationSetting.newVideoFromSubscription |
132 | } | 139 | } |
133 | 140 | ||
134 | async function notificationCreator (user: UserModel) { | 141 | async function notificationCreator (user: MUserWithNotificationSetting) { |
135 | const notification = await UserNotificationModel.create({ | 142 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
136 | type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, | 143 | type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, |
137 | userId: user.id, | 144 | userId: user.id, |
138 | videoId: video.id | 145 | videoId: video.id |
139 | }) | 146 | }) |
140 | notification.Video = video as VideoModel | 147 | notification.Video = video |
141 | 148 | ||
142 | return notification | 149 | return notification |
143 | } | 150 | } |
@@ -162,17 +169,17 @@ class Notifier { | |||
162 | 169 | ||
163 | logger.info('Notifying user %s of new comment %s.', user.username, comment.url) | 170 | logger.info('Notifying user %s of new comment %s.', user.username, comment.url) |
164 | 171 | ||
165 | function settingGetter (user: UserModel) { | 172 | function settingGetter (user: MUserWithNotificationSetting) { |
166 | return user.NotificationSetting.newCommentOnMyVideo | 173 | return user.NotificationSetting.newCommentOnMyVideo |
167 | } | 174 | } |
168 | 175 | ||
169 | async function notificationCreator (user: UserModel) { | 176 | async function notificationCreator (user: MUserWithNotificationSetting) { |
170 | const notification = await UserNotificationModel.create({ | 177 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
171 | type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, | 178 | type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, |
172 | userId: user.id, | 179 | userId: user.id, |
173 | commentId: comment.id | 180 | commentId: comment.id |
174 | }) | 181 | }) |
175 | notification.Comment = comment as VideoCommentModel | 182 | notification.Comment = comment |
176 | 183 | ||
177 | return notification | 184 | return notification |
178 | } | 185 | } |
@@ -207,19 +214,19 @@ class Notifier { | |||
207 | 214 | ||
208 | logger.info('Notifying %d users of new comment %s.', users.length, comment.url) | 215 | logger.info('Notifying %d users of new comment %s.', users.length, comment.url) |
209 | 216 | ||
210 | function settingGetter (user: UserModel) { | 217 | function settingGetter (user: MUserNotifSettingAccount) { |
211 | if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE | 218 | if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE |
212 | 219 | ||
213 | return user.NotificationSetting.commentMention | 220 | return user.NotificationSetting.commentMention |
214 | } | 221 | } |
215 | 222 | ||
216 | async function notificationCreator (user: UserModel) { | 223 | async function notificationCreator (user: MUserNotifSettingAccount) { |
217 | const notification = await UserNotificationModel.create({ | 224 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
218 | type: UserNotificationType.COMMENT_MENTION, | 225 | type: UserNotificationType.COMMENT_MENTION, |
219 | userId: user.id, | 226 | userId: user.id, |
220 | commentId: comment.id | 227 | commentId: comment.id |
221 | }) | 228 | }) |
222 | notification.Comment = comment as VideoCommentModel | 229 | notification.Comment = comment |
223 | 230 | ||
224 | return notification | 231 | return notification |
225 | } | 232 | } |
@@ -231,7 +238,7 @@ class Notifier { | |||
231 | return this.notify({ users, settingGetter, notificationCreator, emailSender }) | 238 | return this.notify({ users, settingGetter, notificationCreator, emailSender }) |
232 | } | 239 | } |
233 | 240 | ||
234 | private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFollowingFullFollowerAccount) { | 241 | private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) { |
235 | if (actorFollow.ActorFollowing.isOwned() === false) return | 242 | if (actorFollow.ActorFollowing.isOwned() === false) return |
236 | 243 | ||
237 | // Account follows one of our account? | 244 | // Account follows one of our account? |
@@ -253,17 +260,17 @@ class Notifier { | |||
253 | 260 | ||
254 | logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) | 261 | logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) |
255 | 262 | ||
256 | function settingGetter (user: UserModel) { | 263 | function settingGetter (user: MUserWithNotificationSetting) { |
257 | return user.NotificationSetting.newFollow | 264 | return user.NotificationSetting.newFollow |
258 | } | 265 | } |
259 | 266 | ||
260 | async function notificationCreator (user: UserModel) { | 267 | async function notificationCreator (user: MUserWithNotificationSetting) { |
261 | const notification = await UserNotificationModel.create({ | 268 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
262 | type: UserNotificationType.NEW_FOLLOW, | 269 | type: UserNotificationType.NEW_FOLLOW, |
263 | userId: user.id, | 270 | userId: user.id, |
264 | actorFollowId: actorFollow.id | 271 | actorFollowId: actorFollow.id |
265 | }) | 272 | }) |
266 | notification.ActorFollow = actorFollow as ActorFollowModel | 273 | notification.ActorFollow = actorFollow |
267 | 274 | ||
268 | return notification | 275 | return notification |
269 | } | 276 | } |
@@ -275,22 +282,22 @@ class Notifier { | |||
275 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | 282 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) |
276 | } | 283 | } |
277 | 284 | ||
278 | private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowActors) { | 285 | private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) { |
279 | const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) | 286 | const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) |
280 | 287 | ||
281 | logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url) | 288 | logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url) |
282 | 289 | ||
283 | function settingGetter (user: UserModel) { | 290 | function settingGetter (user: MUserWithNotificationSetting) { |
284 | return user.NotificationSetting.newInstanceFollower | 291 | return user.NotificationSetting.newInstanceFollower |
285 | } | 292 | } |
286 | 293 | ||
287 | async function notificationCreator (user: UserModel) { | 294 | async function notificationCreator (user: MUserWithNotificationSetting) { |
288 | const notification = await UserNotificationModel.create({ | 295 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
289 | type: UserNotificationType.NEW_INSTANCE_FOLLOWER, | 296 | type: UserNotificationType.NEW_INSTANCE_FOLLOWER, |
290 | userId: user.id, | 297 | userId: user.id, |
291 | actorFollowId: actorFollow.id | 298 | actorFollowId: actorFollow.id |
292 | }) | 299 | }) |
293 | notification.ActorFollow = actorFollow as ActorFollowModel | 300 | notification.ActorFollow = actorFollow |
294 | 301 | ||
295 | return notification | 302 | return notification |
296 | } | 303 | } |
@@ -302,18 +309,45 @@ class Notifier { | |||
302 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | 309 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) |
303 | } | 310 | } |
304 | 311 | ||
312 | private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) { | ||
313 | const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) | ||
314 | |||
315 | logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url) | ||
316 | |||
317 | function settingGetter (user: MUserWithNotificationSetting) { | ||
318 | return user.NotificationSetting.autoInstanceFollowing | ||
319 | } | ||
320 | |||
321 | async function notificationCreator (user: MUserWithNotificationSetting) { | ||
322 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
323 | type: UserNotificationType.AUTO_INSTANCE_FOLLOWING, | ||
324 | userId: user.id, | ||
325 | actorFollowId: actorFollow.id | ||
326 | }) | ||
327 | notification.ActorFollow = actorFollow | ||
328 | |||
329 | return notification | ||
330 | } | ||
331 | |||
332 | function emailSender (emails: string[]) { | ||
333 | return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow) | ||
334 | } | ||
335 | |||
336 | return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) | ||
337 | } | ||
338 | |||
305 | private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) { | 339 | private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) { |
306 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) | 340 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) |
307 | if (moderators.length === 0) return | 341 | if (moderators.length === 0) return |
308 | 342 | ||
309 | logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) | 343 | logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) |
310 | 344 | ||
311 | function settingGetter (user: UserModel) { | 345 | function settingGetter (user: MUserWithNotificationSetting) { |
312 | return user.NotificationSetting.videoAbuseAsModerator | 346 | return user.NotificationSetting.videoAbuseAsModerator |
313 | } | 347 | } |
314 | 348 | ||
315 | async function notificationCreator (user: UserModel) { | 349 | async function notificationCreator (user: MUserWithNotificationSetting) { |
316 | const notification: UserNotificationModelForApi = await UserNotificationModel.create({ | 350 | const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ |
317 | type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, | 351 | type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, |
318 | userId: user.id, | 352 | userId: user.id, |
319 | videoAbuseId: videoAbuse.id | 353 | videoAbuseId: videoAbuse.id |
@@ -330,29 +364,29 @@ class Notifier { | |||
330 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 364 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
331 | } | 365 | } |
332 | 366 | ||
333 | private async notifyModeratorsOfVideoAutoBlacklist (video: MVideo) { | 367 | private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) { |
334 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST) | 368 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST) |
335 | if (moderators.length === 0) return | 369 | if (moderators.length === 0) return |
336 | 370 | ||
337 | logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url) | 371 | logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, videoBlacklist.Video.url) |
338 | 372 | ||
339 | function settingGetter (user: UserModel) { | 373 | function settingGetter (user: MUserWithNotificationSetting) { |
340 | return user.NotificationSetting.videoAutoBlacklistAsModerator | 374 | return user.NotificationSetting.videoAutoBlacklistAsModerator |
341 | } | 375 | } |
342 | async function notificationCreator (user: UserModel) { | ||
343 | 376 | ||
344 | const notification = await UserNotificationModel.create({ | 377 | async function notificationCreator (user: MUserWithNotificationSetting) { |
378 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ | ||
345 | type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS, | 379 | type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS, |
346 | userId: user.id, | 380 | userId: user.id, |
347 | videoId: video.id | 381 | videoBlacklistId: videoBlacklist.id |
348 | }) | 382 | }) |
349 | notification.Video = video as VideoModel | 383 | notification.VideoBlacklist = videoBlacklist |
350 | 384 | ||
351 | return notification | 385 | return notification |
352 | } | 386 | } |
353 | 387 | ||
354 | function emailSender (emails: string[]) { | 388 | function emailSender (emails: string[]) { |
355 | return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video) | 389 | return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist) |
356 | } | 390 | } |
357 | 391 | ||
358 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 392 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
@@ -364,17 +398,17 @@ class Notifier { | |||
364 | 398 | ||
365 | logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url) | 399 | logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url) |
366 | 400 | ||
367 | function settingGetter (user: UserModel) { | 401 | function settingGetter (user: MUserWithNotificationSetting) { |
368 | return user.NotificationSetting.blacklistOnMyVideo | 402 | return user.NotificationSetting.blacklistOnMyVideo |
369 | } | 403 | } |
370 | 404 | ||
371 | async function notificationCreator (user: UserModel) { | 405 | async function notificationCreator (user: MUserWithNotificationSetting) { |
372 | const notification = await UserNotificationModel.create({ | 406 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
373 | type: UserNotificationType.BLACKLIST_ON_MY_VIDEO, | 407 | type: UserNotificationType.BLACKLIST_ON_MY_VIDEO, |
374 | userId: user.id, | 408 | userId: user.id, |
375 | videoBlacklistId: videoBlacklist.id | 409 | videoBlacklistId: videoBlacklist.id |
376 | }) | 410 | }) |
377 | notification.VideoBlacklist = videoBlacklist as VideoBlacklistModel | 411 | notification.VideoBlacklist = videoBlacklist |
378 | 412 | ||
379 | return notification | 413 | return notification |
380 | } | 414 | } |
@@ -386,23 +420,23 @@ class Notifier { | |||
386 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | 420 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) |
387 | } | 421 | } |
388 | 422 | ||
389 | private async notifyVideoOwnerOfUnblacklist (video: MVideo) { | 423 | private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) { |
390 | const user = await UserModel.loadByVideoId(video.id) | 424 | const user = await UserModel.loadByVideoId(video.id) |
391 | if (!user) return | 425 | if (!user) return |
392 | 426 | ||
393 | logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url) | 427 | logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url) |
394 | 428 | ||
395 | function settingGetter (user: UserModel) { | 429 | function settingGetter (user: MUserWithNotificationSetting) { |
396 | return user.NotificationSetting.blacklistOnMyVideo | 430 | return user.NotificationSetting.blacklistOnMyVideo |
397 | } | 431 | } |
398 | 432 | ||
399 | async function notificationCreator (user: UserModel) { | 433 | async function notificationCreator (user: MUserWithNotificationSetting) { |
400 | const notification = await UserNotificationModel.create({ | 434 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
401 | type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO, | 435 | type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO, |
402 | userId: user.id, | 436 | userId: user.id, |
403 | videoId: video.id | 437 | videoId: video.id |
404 | }) | 438 | }) |
405 | notification.Video = video as VideoModel | 439 | notification.Video = video |
406 | 440 | ||
407 | return notification | 441 | return notification |
408 | } | 442 | } |
@@ -420,17 +454,17 @@ class Notifier { | |||
420 | 454 | ||
421 | logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url) | 455 | logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url) |
422 | 456 | ||
423 | function settingGetter (user: UserModel) { | 457 | function settingGetter (user: MUserWithNotificationSetting) { |
424 | return user.NotificationSetting.myVideoPublished | 458 | return user.NotificationSetting.myVideoPublished |
425 | } | 459 | } |
426 | 460 | ||
427 | async function notificationCreator (user: UserModel) { | 461 | async function notificationCreator (user: MUserWithNotificationSetting) { |
428 | const notification = await UserNotificationModel.create({ | 462 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
429 | type: UserNotificationType.MY_VIDEO_PUBLISHED, | 463 | type: UserNotificationType.MY_VIDEO_PUBLISHED, |
430 | userId: user.id, | 464 | userId: user.id, |
431 | videoId: video.id | 465 | videoId: video.id |
432 | }) | 466 | }) |
433 | notification.Video = video as VideoModel | 467 | notification.Video = video |
434 | 468 | ||
435 | return notification | 469 | return notification |
436 | } | 470 | } |
@@ -448,17 +482,17 @@ class Notifier { | |||
448 | 482 | ||
449 | logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier()) | 483 | logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier()) |
450 | 484 | ||
451 | function settingGetter (user: UserModel) { | 485 | function settingGetter (user: MUserWithNotificationSetting) { |
452 | return user.NotificationSetting.myVideoImportFinished | 486 | return user.NotificationSetting.myVideoImportFinished |
453 | } | 487 | } |
454 | 488 | ||
455 | async function notificationCreator (user: UserModel) { | 489 | async function notificationCreator (user: MUserWithNotificationSetting) { |
456 | const notification = await UserNotificationModel.create({ | 490 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
457 | type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR, | 491 | type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR, |
458 | userId: user.id, | 492 | userId: user.id, |
459 | videoImportId: videoImport.id | 493 | videoImportId: videoImport.id |
460 | }) | 494 | }) |
461 | notification.VideoImport = videoImport as VideoImportModel | 495 | notification.VideoImport = videoImport |
462 | 496 | ||
463 | return notification | 497 | return notification |
464 | } | 498 | } |
@@ -472,7 +506,7 @@ class Notifier { | |||
472 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) | 506 | return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) |
473 | } | 507 | } |
474 | 508 | ||
475 | private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserAccount) { | 509 | private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) { |
476 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS) | 510 | const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS) |
477 | if (moderators.length === 0) return | 511 | if (moderators.length === 0) return |
478 | 512 | ||
@@ -481,17 +515,17 @@ class Notifier { | |||
481 | moderators.length, registeredUser.username | 515 | moderators.length, registeredUser.username |
482 | ) | 516 | ) |
483 | 517 | ||
484 | function settingGetter (user: UserModel) { | 518 | function settingGetter (user: MUserWithNotificationSetting) { |
485 | return user.NotificationSetting.newUserRegistration | 519 | return user.NotificationSetting.newUserRegistration |
486 | } | 520 | } |
487 | 521 | ||
488 | async function notificationCreator (user: UserModel) { | 522 | async function notificationCreator (user: MUserWithNotificationSetting) { |
489 | const notification = await UserNotificationModel.create({ | 523 | const notification = await UserNotificationModel.create<UserNotificationModelForApi>({ |
490 | type: UserNotificationType.NEW_USER_REGISTRATION, | 524 | type: UserNotificationType.NEW_USER_REGISTRATION, |
491 | userId: user.id, | 525 | userId: user.id, |
492 | accountId: registeredUser.Account.id | 526 | accountId: registeredUser.Account.id |
493 | }) | 527 | }) |
494 | notification.Account = registeredUser.Account as AccountModel | 528 | notification.Account = registeredUser.Account |
495 | 529 | ||
496 | return notification | 530 | return notification |
497 | } | 531 | } |
@@ -503,11 +537,11 @@ class Notifier { | |||
503 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) | 537 | return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) |
504 | } | 538 | } |
505 | 539 | ||
506 | private async notify (options: { | 540 | private async notify <T extends MUserWithNotificationSetting> (options: { |
507 | users: MUserWithNotificationSetting[], | 541 | users: T[], |
508 | notificationCreator: (user: MUserWithNotificationSetting) => Promise<UserNotificationModelForApi>, | 542 | notificationCreator: (user: T) => Promise<UserNotificationModelForApi>, |
509 | emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, | 543 | emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, |
510 | settingGetter: (user: MUserWithNotificationSetting) => UserNotificationSettingValue | 544 | settingGetter: (user: T) => UserNotificationSettingValue |
511 | }) { | 545 | }) { |
512 | const emails: string[] = [] | 546 | const emails: string[] = [] |
513 | 547 | ||
diff --git a/server/lib/user.ts b/server/lib/user.ts index d84aff464..c45438d95 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -138,7 +138,8 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
138 | newUserRegistration: UserNotificationSettingValue.WEB, | 138 | newUserRegistration: UserNotificationSettingValue.WEB, |
139 | commentMention: UserNotificationSettingValue.WEB, | 139 | commentMention: UserNotificationSettingValue.WEB, |
140 | newFollow: UserNotificationSettingValue.WEB, | 140 | newFollow: UserNotificationSettingValue.WEB, |
141 | newInstanceFollower: UserNotificationSettingValue.WEB | 141 | newInstanceFollower: UserNotificationSettingValue.WEB, |
142 | autoInstanceFollowing: UserNotificationSettingValue.WEB | ||
142 | } | 143 | } |
143 | 144 | ||
144 | return UserNotificationSettingModel.create(values, { transaction: t }) | 145 | return UserNotificationSettingModel.create(values, { transaction: t }) |
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts index a0fc26e84..1dd45b76d 100644 --- a/server/lib/video-blacklist.ts +++ b/server/lib/video-blacklist.ts | |||
@@ -6,7 +6,7 @@ import { logger } from '../helpers/logger' | |||
6 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' | 6 | import { UserAdminFlag } from '../../shared/models/users/user-flag.model' |
7 | import { Hooks } from './plugins/hooks' | 7 | import { Hooks } from './plugins/hooks' |
8 | import { Notifier } from './notifier' | 8 | import { Notifier } from './notifier' |
9 | import { MUser, MVideoBlacklist, MVideoWithBlacklistLight } from '@server/typings/models' | 9 | import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models' |
10 | 10 | ||
11 | async function autoBlacklistVideoIfNeeded (parameters: { | 11 | async function autoBlacklistVideoIfNeeded (parameters: { |
12 | video: MVideoWithBlacklistLight, | 12 | video: MVideoWithBlacklistLight, |
@@ -31,7 +31,7 @@ async function autoBlacklistVideoIfNeeded (parameters: { | |||
31 | reason: 'Auto-blacklisted. Moderator review required.', | 31 | reason: 'Auto-blacklisted. Moderator review required.', |
32 | type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED | 32 | type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED |
33 | } | 33 | } |
34 | const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate<MVideoBlacklist>({ | 34 | const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate<MVideoBlacklistVideo>({ |
35 | where: { | 35 | where: { |
36 | videoId: video.id | 36 | videoId: video.id |
37 | }, | 37 | }, |
@@ -40,7 +40,9 @@ async function autoBlacklistVideoIfNeeded (parameters: { | |||
40 | }) | 40 | }) |
41 | video.VideoBlacklist = videoBlacklist | 41 | video.VideoBlacklist = videoBlacklist |
42 | 42 | ||
43 | if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(video) | 43 | videoBlacklist.Video = video |
44 | |||
45 | if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist) | ||
44 | 46 | ||
45 | logger.info('Video %s auto-blacklisted.', video.uuid) | 47 | logger.info('Video %s auto-blacklisted.', video.uuid) |
46 | 48 | ||
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts index 308b32655..fbfcb0a4c 100644 --- a/server/middlewares/validators/user-notifications.ts +++ b/server/middlewares/validators/user-notifications.ts | |||
@@ -43,6 +43,8 @@ const updateNotificationSettingsValidator = [ | |||
43 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new user registration notification setting'), | 43 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new user registration notification setting'), |
44 | body('newInstanceFollower') | 44 | body('newInstanceFollower') |
45 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance follower notification setting'), | 45 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance follower notification setting'), |
46 | body('autoInstanceFollowing') | ||
47 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance following notification setting'), | ||
46 | 48 | ||
47 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 49 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
48 | logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body }) | 50 | logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body }) |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 394a55f5e..ba1094536 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -381,7 +381,7 @@ export class AccountModel extends Model<AccountModel> { | |||
381 | } | 381 | } |
382 | 382 | ||
383 | toActivityPubObject (this: MAccountAP) { | 383 | toActivityPubObject (this: MAccountAP) { |
384 | const obj = this.Actor.toActivityPubObject(this.name, 'Account') | 384 | const obj = this.Actor.toActivityPubObject(this.name) |
385 | 385 | ||
386 | return Object.assign(obj, { | 386 | return Object.assign(obj, { |
387 | summary: this.description | 387 | summary: this.description |
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index 1506295cf..dc69a17fd 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts | |||
@@ -114,6 +114,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM | |||
114 | @AllowNull(false) | 114 | @AllowNull(false) |
115 | @Default(null) | 115 | @Default(null) |
116 | @Is( | 116 | @Is( |
117 | 'UserNotificationSettingNewInstanceFollower', | ||
118 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing') | ||
119 | ) | ||
120 | @Column | ||
121 | autoInstanceFollowing: UserNotificationSettingValue | ||
122 | |||
123 | @AllowNull(false) | ||
124 | @Default(null) | ||
125 | @Is( | ||
117 | 'UserNotificationSettingNewFollow', | 126 | 'UserNotificationSettingNewFollow', |
118 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') | 127 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') |
119 | ) | 128 | ) |
@@ -165,7 +174,8 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM | |||
165 | newUserRegistration: this.newUserRegistration, | 174 | newUserRegistration: this.newUserRegistration, |
166 | commentMention: this.commentMention, | 175 | commentMention: this.commentMention, |
167 | newFollow: this.newFollow, | 176 | newFollow: this.newFollow, |
168 | newInstanceFollower: this.newInstanceFollower | 177 | newInstanceFollower: this.newInstanceFollower, |
178 | autoInstanceFollowing: this.autoInstanceFollowing | ||
169 | } | 179 | } |
170 | } | 180 | } |
171 | } | 181 | } |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 9b13a8376..ccb81b891 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -135,13 +135,18 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
135 | ] | 135 | ] |
136 | }, | 136 | }, |
137 | { | 137 | { |
138 | attributes: [ 'preferredUsername' ], | 138 | attributes: [ 'preferredUsername', 'type' ], |
139 | model: ActorModel.unscoped(), | 139 | model: ActorModel.unscoped(), |
140 | required: true, | 140 | required: true, |
141 | as: 'ActorFollowing', | 141 | as: 'ActorFollowing', |
142 | include: [ | 142 | include: [ |
143 | buildChannelInclude(false), | 143 | buildChannelInclude(false), |
144 | buildAccountInclude(false) | 144 | buildAccountInclude(false), |
145 | { | ||
146 | attributes: [ 'host' ], | ||
147 | model: ServerModel.unscoped(), | ||
148 | required: false | ||
149 | } | ||
145 | ] | 150 | ] |
146 | } | 151 | } |
147 | ] | 152 | ] |
@@ -404,6 +409,11 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
404 | 409 | ||
405 | const account = this.Account ? this.formatActor(this.Account) : undefined | 410 | const account = this.Account ? this.formatActor(this.Account) : undefined |
406 | 411 | ||
412 | const actorFollowingType = { | ||
413 | Application: 'instance' as 'instance', | ||
414 | Group: 'channel' as 'channel', | ||
415 | Person: 'account' as 'account' | ||
416 | } | ||
407 | const actorFollow = this.ActorFollow ? { | 417 | const actorFollow = this.ActorFollow ? { |
408 | id: this.ActorFollow.id, | 418 | id: this.ActorFollow.id, |
409 | state: this.ActorFollow.state, | 419 | state: this.ActorFollow.state, |
@@ -415,9 +425,10 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
415 | host: this.ActorFollow.ActorFollower.getHost() | 425 | host: this.ActorFollow.ActorFollower.getHost() |
416 | }, | 426 | }, |
417 | following: { | 427 | following: { |
418 | type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', | 428 | type: actorFollowingType[this.ActorFollow.ActorFollowing.type], |
419 | displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), | 429 | displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), |
420 | name: this.ActorFollow.ActorFollowing.preferredUsername | 430 | name: this.ActorFollow.ActorFollowing.preferredUsername, |
431 | host: this.ActorFollow.ActorFollowing.getHost() | ||
421 | } | 432 | } |
422 | } : undefined | 433 | } : undefined |
423 | 434 | ||
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 67a1b5bc1..05de1905d 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -43,7 +43,6 @@ import { | |||
43 | MActorFormattable, | 43 | MActorFormattable, |
44 | MActorFull, | 44 | MActorFull, |
45 | MActorHost, | 45 | MActorHost, |
46 | MActorRedundancyAllowedOpt, | ||
47 | MActorServer, | 46 | MActorServer, |
48 | MActorSummaryFormattable | 47 | MActorSummaryFormattable |
49 | } from '../../typings/models' | 48 | } from '../../typings/models' |
@@ -430,15 +429,8 @@ export class ActorModel extends Model<ActorModel> { | |||
430 | }) | 429 | }) |
431 | } | 430 | } |
432 | 431 | ||
433 | toActivityPubObject (this: MActorAP, name: string, type: 'Account' | 'Application' | 'VideoChannel') { | 432 | toActivityPubObject (this: MActorAP, name: string) { |
434 | let activityPubType | 433 | let activityPubType |
435 | if (type === 'Account') { | ||
436 | activityPubType = 'Person' as 'Person' | ||
437 | } else if (type === 'Application') { | ||
438 | activityPubType = 'Application' as 'Application' | ||
439 | } else { // VideoChannel | ||
440 | activityPubType = 'Group' as 'Group' | ||
441 | } | ||
442 | 434 | ||
443 | let icon = undefined | 435 | let icon = undefined |
444 | if (this.avatarId) { | 436 | if (this.avatarId) { |
@@ -451,7 +443,7 @@ export class ActorModel extends Model<ActorModel> { | |||
451 | } | 443 | } |
452 | 444 | ||
453 | const json = { | 445 | const json = { |
454 | type: activityPubType, | 446 | type: this.type, |
455 | id: this.url, | 447 | id: this.url, |
456 | following: this.getFollowingUrl(), | 448 | following: this.getFollowingUrl(), |
457 | followers: this.getFollowersUrl(), | 449 | followers: this.getFollowersUrl(), |
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index 3b6759b5c..8b07115f1 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -51,6 +51,16 @@ export class ServerModel extends Model<ServerModel> { | |||
51 | }) | 51 | }) |
52 | BlockedByAccounts: ServerBlocklistModel[] | 52 | BlockedByAccounts: ServerBlocklistModel[] |
53 | 53 | ||
54 | static load (id: number): Bluebird<MServer> { | ||
55 | const query = { | ||
56 | where: { | ||
57 | id | ||
58 | } | ||
59 | } | ||
60 | |||
61 | return ServerModel.findOne(query) | ||
62 | } | ||
63 | |||
54 | static loadByHost (host: string): Bluebird<MServer> { | 64 | static loadByHost (host: string): Bluebird<MServer> { |
55 | const query = { | 65 | const query = { |
56 | where: { | 66 | where: { |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 7178631b4..05545bd9d 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -517,7 +517,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
517 | } | 517 | } |
518 | 518 | ||
519 | toActivityPubObject (this: MChannelAP): ActivityPubActor { | 519 | toActivityPubObject (this: MChannelAP): ActivityPubActor { |
520 | const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel') | 520 | const obj = this.Actor.toActivityPubObject(this.name) |
521 | 521 | ||
522 | return Object.assign(obj, { | 522 | return Object.assign(obj, { |
523 | summary: this.description, | 523 | summary: this.description, |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 7773ae1e7..1221735c5 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -5,8 +5,16 @@ import 'mocha' | |||
5 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' | 5 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' |
6 | 6 | ||
7 | import { | 7 | import { |
8 | createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, flushAndRunServer, ServerInfo, | 8 | cleanupTests, |
9 | setAccessTokensToServers, userLogin, immutableAssign, cleanupTests | 9 | createUser, |
10 | flushAndRunServer, | ||
11 | immutableAssign, | ||
12 | makeDeleteRequest, | ||
13 | makeGetRequest, | ||
14 | makePutBodyRequest, | ||
15 | ServerInfo, | ||
16 | setAccessTokensToServers, | ||
17 | userLogin | ||
10 | } from '../../../../shared/extra-utils' | 18 | } from '../../../../shared/extra-utils' |
11 | 19 | ||
12 | describe('Test config API validators', function () { | 20 | describe('Test config API validators', function () { |
@@ -98,6 +106,17 @@ describe('Test config API validators', function () { | |||
98 | enabled: false, | 106 | enabled: false, |
99 | manualApproval: true | 107 | manualApproval: true |
100 | } | 108 | } |
109 | }, | ||
110 | followings: { | ||
111 | instance: { | ||
112 | autoFollowBack: { | ||
113 | enabled: true | ||
114 | }, | ||
115 | autoFollowIndex: { | ||
116 | enabled: true, | ||
117 | indexUrl: 'https://index.example.com' | ||
118 | } | ||
119 | } | ||
101 | } | 120 | } |
102 | } | 121 | } |
103 | 122 | ||
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts index 14ee20d45..3b06be7ef 100644 --- a/server/tests/api/check-params/user-notifications.ts +++ b/server/tests/api/check-params/user-notifications.ts | |||
@@ -172,7 +172,8 @@ describe('Test user notifications API validators', function () { | |||
172 | commentMention: UserNotificationSettingValue.WEB, | 172 | commentMention: UserNotificationSettingValue.WEB, |
173 | newFollow: UserNotificationSettingValue.WEB, | 173 | newFollow: UserNotificationSettingValue.WEB, |
174 | newUserRegistration: UserNotificationSettingValue.WEB, | 174 | newUserRegistration: UserNotificationSettingValue.WEB, |
175 | newInstanceFollower: UserNotificationSettingValue.WEB | 175 | newInstanceFollower: UserNotificationSettingValue.WEB, |
176 | autoInstanceFollowing: UserNotificationSettingValue.WEB | ||
176 | } | 177 | } |
177 | 178 | ||
178 | it('Should fail with missing fields', async function () { | 179 | it('Should fail with missing fields', async function () { |
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts index 6fa630562..62b797b47 100644 --- a/server/tests/api/notifications/user-notifications.ts +++ b/server/tests/api/notifications/user-notifications.ts | |||
@@ -16,8 +16,8 @@ import { | |||
16 | immutableAssign, | 16 | immutableAssign, |
17 | registerUser, | 17 | registerUser, |
18 | removeVideoFromBlacklist, | 18 | removeVideoFromBlacklist, |
19 | reportVideoAbuse, | 19 | reportVideoAbuse, unfollow, |
20 | updateCustomConfig, | 20 | updateCustomConfig, updateCustomSubConfig, |
21 | updateMyUser, | 21 | updateMyUser, |
22 | updateVideo, | 22 | updateVideo, |
23 | updateVideoChannel, | 23 | updateVideoChannel, |
@@ -45,7 +45,8 @@ import { | |||
45 | getUserNotifications, | 45 | getUserNotifications, |
46 | markAsReadAllNotifications, | 46 | markAsReadAllNotifications, |
47 | markAsReadNotifications, | 47 | markAsReadNotifications, |
48 | updateMyNotificationSettings | 48 | updateMyNotificationSettings, |
49 | checkAutoInstanceFollowing | ||
49 | } from '../../../../shared/extra-utils/users/user-notifications' | 50 | } from '../../../../shared/extra-utils/users/user-notifications' |
50 | import { | 51 | import { |
51 | User, | 52 | User, |
@@ -108,7 +109,8 @@ describe('Test users notifications', function () { | |||
108 | commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 109 | commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
109 | newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 110 | newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
110 | newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, | 111 | newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
111 | newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | 112 | newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, |
113 | autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL | ||
112 | } | 114 | } |
113 | 115 | ||
114 | before(async function () { | 116 | before(async function () { |
@@ -897,6 +899,36 @@ describe('Test users notifications', function () { | |||
897 | const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } | 899 | const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } |
898 | await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence') | 900 | await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence') |
899 | }) | 901 | }) |
902 | |||
903 | it('Should send a notification on auto follow back', async function () { | ||
904 | this.timeout(40000) | ||
905 | |||
906 | await unfollow(servers[2].url, servers[2].accessToken, servers[0]) | ||
907 | await waitJobs(servers) | ||
908 | |||
909 | const config = { | ||
910 | followings: { | ||
911 | instance: { | ||
912 | autoFollowBack: { enabled: true } | ||
913 | } | ||
914 | } | ||
915 | } | ||
916 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config) | ||
917 | |||
918 | await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken) | ||
919 | |||
920 | await waitJobs(servers) | ||
921 | |||
922 | const followerHost = servers[0].host | ||
923 | const followingHost = servers[2].host | ||
924 | await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence') | ||
925 | |||
926 | const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } | ||
927 | await checkAutoInstanceFollowing(immutableAssign(baseParams, userOverride), followerHost, followingHost, 'absence') | ||
928 | |||
929 | config.followings.instance.autoFollowBack.enabled = false | ||
930 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config) | ||
931 | }) | ||
900 | }) | 932 | }) |
901 | 933 | ||
902 | describe('New actor follow', function () { | 934 | describe('New actor follow', function () { |
diff --git a/server/tests/api/server/auto-follows.ts b/server/tests/api/server/auto-follows.ts new file mode 100644 index 000000000..32ad259c9 --- /dev/null +++ b/server/tests/api/server/auto-follows.ts | |||
@@ -0,0 +1,148 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | acceptFollower, | ||
7 | cleanupTests, | ||
8 | flushAndRunMultipleServers, | ||
9 | ServerInfo, | ||
10 | setAccessTokensToServers, | ||
11 | unfollow, | ||
12 | updateCustomSubConfig | ||
13 | } from '../../../../shared/extra-utils/index' | ||
14 | import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort } from '../../../../shared/extra-utils/server/follows' | ||
15 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
16 | import { ActorFollow } from '../../../../shared/models/actors' | ||
17 | |||
18 | const expect = chai.expect | ||
19 | |||
20 | async function checkFollow (follower: ServerInfo, following: ServerInfo, exists: boolean) { | ||
21 | { | ||
22 | const res = await getFollowersListPaginationAndSort(following.url, 0, 5, '-createdAt') | ||
23 | const follows = res.body.data as ActorFollow[] | ||
24 | |||
25 | if (exists === true) { | ||
26 | expect(res.body.total).to.equal(1) | ||
27 | |||
28 | expect(follows[ 0 ].follower.host).to.equal(follower.host) | ||
29 | expect(follows[ 0 ].state).to.equal('accepted') | ||
30 | } else { | ||
31 | expect(follows.filter(f => f.state === 'accepted')).to.have.lengthOf(0) | ||
32 | } | ||
33 | } | ||
34 | |||
35 | { | ||
36 | const res = await getFollowingListPaginationAndSort(follower.url, 0, 5, '-createdAt') | ||
37 | const follows = res.body.data as ActorFollow[] | ||
38 | |||
39 | if (exists === true) { | ||
40 | expect(res.body.total).to.equal(1) | ||
41 | |||
42 | expect(follows[ 0 ].following.host).to.equal(following.host) | ||
43 | expect(follows[ 0 ].state).to.equal('accepted') | ||
44 | } else { | ||
45 | expect(follows.filter(f => f.state === 'accepted')).to.have.lengthOf(0) | ||
46 | } | ||
47 | } | ||
48 | } | ||
49 | |||
50 | async function server1Follows2 (servers: ServerInfo[]) { | ||
51 | await follow(servers[0].url, [ servers[1].host ], servers[0].accessToken) | ||
52 | |||
53 | await waitJobs(servers) | ||
54 | } | ||
55 | |||
56 | async function resetFollows (servers: ServerInfo[]) { | ||
57 | try { | ||
58 | await unfollow(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ]) | ||
59 | await unfollow(servers[ 1 ].url, servers[ 1 ].accessToken, servers[ 0 ]) | ||
60 | } catch { /* empty */ } | ||
61 | |||
62 | await waitJobs(servers) | ||
63 | |||
64 | await checkFollow(servers[0], servers[1], false) | ||
65 | await checkFollow(servers[1], servers[0], false) | ||
66 | } | ||
67 | |||
68 | describe('Test auto follows', function () { | ||
69 | let servers: ServerInfo[] = [] | ||
70 | |||
71 | before(async function () { | ||
72 | this.timeout(30000) | ||
73 | |||
74 | servers = await flushAndRunMultipleServers(2) | ||
75 | |||
76 | // Get the access tokens | ||
77 | await setAccessTokensToServers(servers) | ||
78 | }) | ||
79 | |||
80 | describe('Auto follow back', function () { | ||
81 | |||
82 | it('Should not auto follow back if the option is not enabled', async function () { | ||
83 | this.timeout(15000) | ||
84 | |||
85 | await server1Follows2(servers) | ||
86 | |||
87 | await checkFollow(servers[0], servers[1], true) | ||
88 | await checkFollow(servers[1], servers[0], false) | ||
89 | |||
90 | await resetFollows(servers) | ||
91 | }) | ||
92 | |||
93 | it('Should auto follow back on auto accept if the option is enabled', async function () { | ||
94 | this.timeout(15000) | ||
95 | |||
96 | const config = { | ||
97 | followings: { | ||
98 | instance: { | ||
99 | autoFollowBack: { enabled: true } | ||
100 | } | ||
101 | } | ||
102 | } | ||
103 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config) | ||
104 | |||
105 | await server1Follows2(servers) | ||
106 | |||
107 | await checkFollow(servers[0], servers[1], true) | ||
108 | await checkFollow(servers[1], servers[0], true) | ||
109 | |||
110 | await resetFollows(servers) | ||
111 | }) | ||
112 | |||
113 | it('Should wait the acceptation before auto follow back', async function () { | ||
114 | this.timeout(30000) | ||
115 | |||
116 | const config = { | ||
117 | followings: { | ||
118 | instance: { | ||
119 | autoFollowBack: { enabled: true } | ||
120 | } | ||
121 | }, | ||
122 | followers: { | ||
123 | instance: { | ||
124 | manualApproval: true | ||
125 | } | ||
126 | } | ||
127 | } | ||
128 | await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config) | ||
129 | |||
130 | await server1Follows2(servers) | ||
131 | |||
132 | await checkFollow(servers[0], servers[1], false) | ||
133 | await checkFollow(servers[1], servers[0], false) | ||
134 | |||
135 | await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@' + servers[0].host) | ||
136 | await waitJobs(servers) | ||
137 | |||
138 | await checkFollow(servers[0], servers[1], true) | ||
139 | await checkFollow(servers[1], servers[0], true) | ||
140 | |||
141 | await resetFollows(servers) | ||
142 | }) | ||
143 | }) | ||
144 | |||
145 | after(async function () { | ||
146 | await cleanupTests(servers) | ||
147 | }) | ||
148 | }) | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 78fdc9cc0..b2f1933d1 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -68,6 +68,10 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) { | |||
68 | 68 | ||
69 | expect(data.followers.instance.enabled).to.be.true | 69 | expect(data.followers.instance.enabled).to.be.true |
70 | expect(data.followers.instance.manualApproval).to.be.false | 70 | expect(data.followers.instance.manualApproval).to.be.false |
71 | |||
72 | expect(data.followings.instance.autoFollowBack.enabled).to.be.false | ||
73 | expect(data.followings.instance.autoFollowIndex.enabled).to.be.false | ||
74 | expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://instances.joinpeertube.org') | ||
71 | } | 75 | } |
72 | 76 | ||
73 | function checkUpdatedConfig (data: CustomConfig) { | 77 | function checkUpdatedConfig (data: CustomConfig) { |
@@ -119,6 +123,10 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
119 | 123 | ||
120 | expect(data.followers.instance.enabled).to.be.false | 124 | expect(data.followers.instance.enabled).to.be.false |
121 | expect(data.followers.instance.manualApproval).to.be.true | 125 | expect(data.followers.instance.manualApproval).to.be.true |
126 | |||
127 | expect(data.followings.instance.autoFollowBack.enabled).to.be.true | ||
128 | expect(data.followings.instance.autoFollowIndex.enabled).to.be.true | ||
129 | expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com') | ||
122 | } | 130 | } |
123 | 131 | ||
124 | describe('Test config', function () { | 132 | describe('Test config', function () { |
@@ -261,6 +269,17 @@ describe('Test config', function () { | |||
261 | enabled: false, | 269 | enabled: false, |
262 | manualApproval: true | 270 | manualApproval: true |
263 | } | 271 | } |
272 | }, | ||
273 | followings: { | ||
274 | instance: { | ||
275 | autoFollowBack: { | ||
276 | enabled: true | ||
277 | }, | ||
278 | autoFollowIndex: { | ||
279 | enabled: true, | ||
280 | indexUrl: 'https://updated.example.com' | ||
281 | } | ||
282 | } | ||
264 | } | 283 | } |
265 | } | 284 | } |
266 | await updateCustomConfig(server.url, server.accessToken, newCustomConfig) | 285 | await updateCustomConfig(server.url, server.accessToken, newCustomConfig) |
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index 3daeeb49a..08205b2c8 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './auto-follows' | ||
1 | import './config' | 2 | import './config' |
2 | import './contact-form' | 3 | import './contact-form' |
3 | import './email' | 4 | import './email' |
diff --git a/server/typings/models/account/actor-follow.ts b/server/typings/models/account/actor-follow.ts index 17a47b8df..1c66eb0a0 100644 --- a/server/typings/models/account/actor-follow.ts +++ b/server/typings/models/account/actor-follow.ts | |||
@@ -2,7 +2,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | |||
2 | import { | 2 | import { |
3 | MActor, | 3 | MActor, |
4 | MActorAccount, | 4 | MActorAccount, |
5 | MActorAccountChannel, | 5 | MActorDefaultAccountChannel, |
6 | MActorChannelAccountActor, | 6 | MActorChannelAccountActor, |
7 | MActorDefault, | 7 | MActorDefault, |
8 | MActorFormattable, | 8 | MActorFormattable, |
@@ -37,8 +37,8 @@ export type MActorFollowActorsDefault = MActorFollow & | |||
37 | Use<'ActorFollowing', MActorDefault> | 37 | Use<'ActorFollowing', MActorDefault> |
38 | 38 | ||
39 | export type MActorFollowFull = MActorFollow & | 39 | export type MActorFollowFull = MActorFollow & |
40 | Use<'ActorFollower', MActorAccountChannel> & | 40 | Use<'ActorFollower', MActorDefaultAccountChannel> & |
41 | Use<'ActorFollowing', MActorAccountChannel> | 41 | Use<'ActorFollowing', MActorDefaultAccountChannel> |
42 | 42 | ||
43 | // ############################################################################ | 43 | // ############################################################################ |
44 | 44 | ||
@@ -51,10 +51,6 @@ export type MActorFollowActorsDefaultSubscription = MActorFollow & | |||
51 | Use<'ActorFollower', MActorDefault> & | 51 | Use<'ActorFollower', MActorDefault> & |
52 | Use<'ActorFollowing', SubscriptionFollowing> | 52 | Use<'ActorFollowing', SubscriptionFollowing> |
53 | 53 | ||
54 | export type MActorFollowFollowingFullFollowerAccount = MActorFollow & | ||
55 | Use<'ActorFollower', MActorAccount> & | ||
56 | Use<'ActorFollowing', MActorAccountChannel> | ||
57 | |||
58 | export type MActorFollowSubscriptions = MActorFollow & | 54 | export type MActorFollowSubscriptions = MActorFollow & |
59 | Use<'ActorFollowing', MActorChannelAccountActor> | 55 | Use<'ActorFollowing', MActorChannelAccountActor> |
60 | 56 | ||
diff --git a/server/typings/models/account/actor.ts b/server/typings/models/account/actor.ts index d4bcac4a3..bcacb8351 100644 --- a/server/typings/models/account/actor.ts +++ b/server/typings/models/account/actor.ts | |||
@@ -58,7 +58,7 @@ export type MActorAccount = MActor & | |||
58 | export type MActorChannel = MActor & | 58 | export type MActorChannel = MActor & |
59 | Use<'VideoChannel', MChannel> | 59 | Use<'VideoChannel', MChannel> |
60 | 60 | ||
61 | export type MActorAccountChannel = MActorAccount & MActorChannel | 61 | export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel |
62 | 62 | ||
63 | export type MActorServer = MActor & | 63 | export type MActorServer = MActor & |
64 | Use<'Server', MServer> | 64 | Use<'Server', MServer> |
diff --git a/server/typings/models/user/user-notification.ts b/server/typings/models/user/user-notification.ts index f9daf5eb2..1cdc691b0 100644 --- a/server/typings/models/user/user-notification.ts +++ b/server/typings/models/user/user-notification.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { UserNotificationModel } from '../../../models/account/user-notification' | 1 | import { UserNotificationModel } from '../../../models/account/user-notification' |
2 | import { PickWith } from '../../utils' | 2 | import { PickWith, PickWithOpt } from '../../utils' |
3 | import { VideoModel } from '../../../models/video/video' | 3 | import { VideoModel } from '../../../models/video/video' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 4 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { ServerModel } from '../../../models/server/server' | 5 | import { ServerModel } from '../../../models/server/server' |
@@ -48,12 +48,13 @@ export namespace UserNotificationIncludes { | |||
48 | 48 | ||
49 | export type ActorFollower = Pick<ActorModel, 'preferredUsername' | 'getHost'> & | 49 | export type ActorFollower = Pick<ActorModel, 'preferredUsername' | 'getHost'> & |
50 | PickWith<ActorModel, 'Account', AccountInclude> & | 50 | PickWith<ActorModel, 'Account', AccountInclude> & |
51 | PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> & | 51 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & |
52 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> | 52 | PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> |
53 | 53 | ||
54 | export type ActorFollowing = Pick<ActorModel, 'preferredUsername'> & | 54 | export type ActorFollowing = Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & |
55 | PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> & | 55 | PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> & |
56 | PickWith<ActorModel, 'Account', AccountInclude> | 56 | PickWith<ActorModel, 'Account', AccountInclude> & |
57 | PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> | ||
57 | 58 | ||
58 | export type ActorFollowInclude = Pick<ActorFollowModel, 'id' | 'state'> & | 59 | export type ActorFollowInclude = Pick<ActorFollowModel, 'id' | 'state'> & |
59 | PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & | 60 | PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & |
diff --git a/server/typings/models/video/video-blacklist.ts b/server/typings/models/video/video-blacklist.ts index 1dedfa37f..e12880454 100644 --- a/server/typings/models/video/video-blacklist.ts +++ b/server/typings/models/video/video-blacklist.ts | |||
@@ -13,6 +13,9 @@ export type MVideoBlacklistUnfederated = Pick<MVideoBlacklist, 'unfederated'> | |||
13 | 13 | ||
14 | // ############################################################################ | 14 | // ############################################################################ |
15 | 15 | ||
16 | export type MVideoBlacklistLightVideo = MVideoBlacklistLight & | ||
17 | Use<'Video', MVideo> | ||
18 | |||
16 | export type MVideoBlacklistVideo = MVideoBlacklist & | 19 | export type MVideoBlacklistVideo = MVideoBlacklist & |
17 | Use<'Video', MVideo> | 20 | Use<'Video', MVideo> |
18 | 21 | ||
diff --git a/server/typings/utils.ts b/server/typings/utils.ts index 4b5cf4d7e..1abb4f73e 100644 --- a/server/typings/utils.ts +++ b/server/typings/utils.ts | |||
@@ -11,3 +11,12 @@ export type PickWith<T, KT extends keyof T, V> = { | |||
11 | export type PickWithOpt<T, KT extends keyof T, V> = { | 11 | export type PickWithOpt<T, KT extends keyof T, V> = { |
12 | [P in KT]?: T[P] extends V ? V : never | 12 | [P in KT]?: T[P] extends V ? V : never |
13 | } | 13 | } |
14 | |||
15 | // https://github.com/krzkaczor/ts-essentials Rocks! | ||
16 | export type DeepPartial<T> = { | ||
17 | [P in keyof T]?: T[P] extends Array<infer U> | ||
18 | ? Array<DeepPartial<U>> | ||
19 | : T[P] extends ReadonlyArray<infer U> | ||
20 | ? ReadonlyArray<DeepPartial<U>> | ||
21 | : DeepPartial<T[P]> | ||
22 | }; | ||