aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/actor.ts61
-rw-r--r--server/lib/activitypub/audience.ts10
-rw-r--r--server/lib/activitypub/cache-file.ts4
-rw-r--r--server/lib/activitypub/crawl.ts6
-rw-r--r--server/lib/activitypub/follow.ts3
-rw-r--r--server/lib/activitypub/index.ts9
-rw-r--r--server/lib/activitypub/playlist.ts6
-rw-r--r--server/lib/activitypub/process/process-announce.ts2
-rw-r--r--server/lib/activitypub/process/process-create.ts5
-rw-r--r--server/lib/activitypub/process/process-delete.ts2
-rw-r--r--server/lib/activitypub/process/process-dislike.ts2
-rw-r--r--server/lib/activitypub/process/process-flag.ts19
-rw-r--r--server/lib/activitypub/process/process-follow.ts4
-rw-r--r--server/lib/activitypub/process/process-like.ts2
-rw-r--r--server/lib/activitypub/process/process-reject.ts2
-rw-r--r--server/lib/activitypub/process/process-undo.ts2
-rw-r--r--server/lib/activitypub/process/process-update.ts5
-rw-r--r--server/lib/activitypub/process/process-view.ts3
-rw-r--r--server/lib/activitypub/send/send-accept.ts2
-rw-r--r--server/lib/activitypub/send/send-announce.ts2
-rw-r--r--server/lib/activitypub/send/send-create.ts18
-rw-r--r--server/lib/activitypub/send/send-delete.ts2
-rw-r--r--server/lib/activitypub/send/send-dislike.ts2
-rw-r--r--server/lib/activitypub/send/send-flag.ts2
-rw-r--r--server/lib/activitypub/send/send-like.ts2
-rw-r--r--server/lib/activitypub/send/send-reject.ts2
-rw-r--r--server/lib/activitypub/send/send-undo.ts10
-rw-r--r--server/lib/activitypub/send/send-update.ts7
-rw-r--r--server/lib/activitypub/send/send-view.ts6
-rw-r--r--server/lib/activitypub/send/utils.ts48
-rw-r--r--server/lib/activitypub/share.ts4
-rw-r--r--server/lib/activitypub/video-comments.ts16
-rw-r--r--server/lib/activitypub/video-rates.ts4
-rw-r--r--server/lib/activitypub/videos.ts220
-rw-r--r--server/lib/auth.ts286
-rw-r--r--server/lib/avatar.ts4
-rw-r--r--server/lib/blocklist.ts4
-rw-r--r--server/lib/client-html.ts26
-rw-r--r--server/lib/emailer.ts477
-rw-r--r--server/lib/emails/common/base.pug267
-rw-r--r--server/lib/emails/common/greetings.pug11
-rw-r--r--server/lib/emails/common/html.pug4
-rw-r--r--server/lib/emails/common/mixins.pug3
-rw-r--r--server/lib/emails/contact-form/html.pug9
-rw-r--r--server/lib/emails/follower-on-channel/html.pug9
-rw-r--r--server/lib/emails/password-create/html.pug10
-rw-r--r--server/lib/emails/password-reset/html.pug12
-rw-r--r--server/lib/emails/user-registered/html.pug10
-rw-r--r--server/lib/emails/verify-email/html.pug14
-rw-r--r--server/lib/emails/video-abuse-new/html.pug18
-rw-r--r--server/lib/emails/video-auto-blacklist-new/html.pug17
-rw-r--r--server/lib/emails/video-comment-mention/html.pug11
-rw-r--r--server/lib/emails/video-comment-new/html.pug11
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts7
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts13
-rw-r--r--server/lib/job-queue/handlers/activitypub-follow.ts13
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts13
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts10
-rw-r--r--server/lib/job-queue/handlers/email.ts5
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts22
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts6
-rw-r--r--server/lib/job-queue/handlers/video-import.ts58
-rw-r--r--server/lib/job-queue/handlers/video-redundancy.ts17
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts52
-rw-r--r--server/lib/job-queue/handlers/video-views.ts6
-rw-r--r--server/lib/job-queue/job-queue.ts82
-rw-r--r--server/lib/moderation.ts22
-rw-r--r--server/lib/notifier.ts69
-rw-r--r--server/lib/oauth-model.ts145
-rw-r--r--server/lib/plugins/hooks.ts2
-rw-r--r--server/lib/plugins/plugin-helpers.ts133
-rw-r--r--server/lib/plugins/plugin-index.ts8
-rw-r--r--server/lib/plugins/plugin-manager.ts307
-rw-r--r--server/lib/plugins/register-helpers-store.ts355
-rw-r--r--server/lib/redis.ts50
-rw-r--r--server/lib/redundancy.ts37
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts11
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts2
-rw-r--r--server/lib/schedulers/remove-old-views-scheduler.ts2
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts3
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts67
-rw-r--r--server/lib/thumbnail.ts18
-rw-r--r--server/lib/user.ts21
-rw-r--r--server/lib/video-blacklist.ts100
-rw-r--r--server/lib/video-channel.ts9
-rw-r--r--server/lib/video-comment.ts8
-rw-r--r--server/lib/video-paths.ts2
-rw-r--r--server/lib/video-playlist.ts2
-rw-r--r--server/lib/video-transcoding.ts6
-rw-r--r--server/lib/videos.ts11
92 files changed, 2408 insertions, 997 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 0b21de0ca..c743dcf3f 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,8 +1,8 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import * as url from 'url' 3import { URL } from 'url'
4import * as uuidv4 from 'uuid/v4' 4import { v4 as uuidv4 } from 'uuid'
5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 5import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' 8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
@@ -19,7 +19,6 @@ import { AvatarModel } from '../../models/avatar/avatar'
19import { ServerModel } from '../../models/server/server' 19import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel' 20import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue' 21import { JobQueue } from '../job-queue'
22import { getServerActor } from '../../helpers/utils'
23import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' 22import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
24import { sequelizeTypescript } from '../../initializers/database' 23import { sequelizeTypescript } from '../../initializers/database'
25import { 24import {
@@ -33,9 +32,10 @@ import {
33 MActorFull, 32 MActorFull,
34 MActorFullActor, 33 MActorFullActor,
35 MActorId, 34 MActorId,
36 MChannel, 35 MChannel
37 MChannelAccountDefault
38} from '../../typings/models' 36} from '../../typings/models'
37import { extname } from 'path'
38import { getServerActor } from '@server/models/application/application'
39 39
40// Set account keys, this could be long so process after the account creation and do not block the client 40// Set account keys, this could be long so process after the account creation and do not block the client
41function setAsyncActorKeys <T extends MActor> (actor: T) { 41function setAsyncActorKeys <T extends MActor> (actor: T) {
@@ -117,17 +117,17 @@ async function getOrCreateActorAndServerAndModel (
117 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor 117 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
118 118
119 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType) 119 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
120 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.') 120 if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
121 121
122 if ((created === true || refreshed === true) && updateCollections === true) { 122 if ((created === true || refreshed === true) && updateCollections === true) {
123 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } 123 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
124 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 124 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
125 } 125 }
126 126
127 // We created a new account: fetch the playlists 127 // We created a new account: fetch the playlists
128 if (created === true && actor.Account && accountPlaylistsUrl) { 128 if (created === true && actor.Account && accountPlaylistsUrl) {
129 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } 129 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
130 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 130 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
131 } 131 }
132 132
133 return actorRefreshed 133 return actorRefreshed
@@ -207,7 +207,7 @@ async function fetchActorTotalItems (url: string) {
207 } 207 }
208 208
209 try { 209 try {
210 const { body } = await doRequest(options) 210 const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options)
211 return body.totalItems ? body.totalItems : 0 211 return body.totalItems ? body.totalItems : 0
212 } catch (err) { 212 } catch (err) {
213 logger.warn('Cannot fetch remote actor count %s.', url, { err }) 213 logger.warn('Cannot fetch remote actor count %s.', url, { err })
@@ -215,20 +215,28 @@ async function fetchActorTotalItems (url: string) {
215 } 215 }
216} 216}
217 217
218async function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 218function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
219 if ( 219 const mimetypes = MIMETYPES.IMAGE
220 actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && 220 const icon = actorJSON.icon
221 isActivityPubUrlValid(actorJSON.icon.url)
222 ) {
223 const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
224 221
225 return { 222 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
226 name: uuidv4() + extension, 223
227 fileUrl: actorJSON.icon.url 224 let extension: string
228 } 225
226 if (icon.mediaType) {
227 extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
228 } else {
229 const tmp = extname(icon.url)
230
231 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
229 } 232 }
230 233
231 return undefined 234 if (!extension) return undefined
235
236 return {
237 name: uuidv4() + extension,
238 fileUrl: icon.url
239 }
232} 240}
233 241
234async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) { 242async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
@@ -271,7 +279,10 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
271 279
272 if (statusCode === 404) { 280 if (statusCode === 404) {
273 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) 281 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
274 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() 282 actor.Account
283 ? await actor.Account.destroy()
284 : await actor.VideoChannel.destroy()
285
275 return { actor: undefined, refreshed: false } 286 return { actor: undefined, refreshed: false }
276 } 287 }
277 288
@@ -337,14 +348,14 @@ function saveActorAndServerAndModelIfNotExist (
337 ownerActor?: MActorFullActor, 348 ownerActor?: MActorFullActor,
338 t?: Transaction 349 t?: Transaction
339): Bluebird<MActorFullActor> | Promise<MActorFullActor> { 350): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
340 let actor = result.actor 351 const actor = result.actor
341 352
342 if (t !== undefined) return save(t) 353 if (t !== undefined) return save(t)
343 354
344 return sequelizeTypescript.transaction(t => save(t)) 355 return sequelizeTypescript.transaction(t => save(t))
345 356
346 async function save (t: Transaction) { 357 async function save (t: Transaction) {
347 const actorHost = url.parse(actor.url).host 358 const actorHost = new URL(actor.url).host
348 359
349 const serverOptions = { 360 const serverOptions = {
350 where: { 361 where: {
@@ -402,7 +413,7 @@ type FetchRemoteActorResult = {
402 support?: string 413 support?: string
403 playlists?: string 414 playlists?: string
404 avatar?: { 415 avatar?: {
405 name: string, 416 name: string
406 fileUrl: string 417 fileUrl: string
407 } 418 }
408 attributedTo: ActivityPubAttributedTo[] 419 attributedTo: ActivityPubAttributedTo[]
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index f2ab54cf7..551d04ae3 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -4,11 +4,11 @@ import { ACTIVITY_PUB } from '../../initializers/constants'
4import { ActorModel } from '../../models/activitypub/actor' 4import { ActorModel } from '../../models/activitypub/actor'
5import { VideoModel } from '../../models/video/video' 5import { VideoModel } from '../../models/video/video'
6import { VideoShareModel } from '../../models/video/video-share' 6import { VideoShareModel } from '../../models/video/video-share'
7import { MActorFollowersUrl, MActorLight, MCommentOwner, MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../../typings/models' 7import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../typings/models'
8 8
9function getRemoteVideoAudience (video: MVideoAccountLight, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience { 9function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
10 return { 10 return {
11 to: [ video.VideoChannel.Account.Actor.url ], 11 to: [ accountActor.url ],
12 cc: actorsInvolvedInVideo.map(a => a.followersUrl) 12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
13 } 13 }
14} 14}
@@ -32,6 +32,8 @@ function getVideoCommentAudience (
32 32
33 // Send to actors we reply to 33 // Send to actors we reply to
34 for (const parentComment of threadParentComments) { 34 for (const parentComment of threadParentComments) {
35 if (parentComment.isDeleted()) continue
36
35 cc.push(parentComment.Account.Actor.url) 37 cc.push(parentComment.Account.Actor.url)
36 } 38 }
37 39
@@ -48,7 +50,7 @@ function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[
48 } 50 }
49} 51}
50 52
51async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) { 53async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t) 54 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
53 55
54 const videoAll = video as VideoModel 56 const videoAll = video as VideoModel
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts
index 65b2dcb49..8252e95e9 100644
--- a/server/lib/activitypub/cache-file.ts
+++ b/server/lib/activitypub/cache-file.ts
@@ -13,7 +13,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
13 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) 13 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url)
14 14
15 return { 15 return {
16 expiresOn: new Date(cacheFileObject.expires), 16 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
17 url: cacheFileObject.id, 17 url: cacheFileObject.id,
18 fileUrl: url.href, 18 fileUrl: url.href,
19 strategy: null, 19 strategy: null,
@@ -30,7 +30,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
30 if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) 30 if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`)
31 31
32 return { 32 return {
33 expiresOn: new Date(cacheFileObject.expires), 33 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
34 url: cacheFileObject.id, 34 url: cacheFileObject.id,
35 fileUrl: url.href, 35 fileUrl: url.href,
36 strategy: null, 36 strategy: null,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 9e469e3e6..eeafdf4ba 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -3,7 +3,7 @@ import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 4import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { parse } from 'url' 6import { URL } from 'url'
7 7
8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) 9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
@@ -24,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
25 const firstBody = response.body 25 const firstBody = response.body
26 26
27 let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
28 let i = 0 28 let i = 0
29 let nextLink = firstBody.first 29 let nextLink = firstBody.first
30 while (nextLink && i < limit) { 30 while (nextLink && i < limit) {
@@ -32,7 +32,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
32 32
33 if (typeof nextLink === 'string') { 33 if (typeof nextLink === 'string') {
34 // Don't crawl ourselves 34 // Don't crawl ourselves
35 const remoteHost = parse(nextLink).host 35 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 36 if (remoteHost === WEBSERVER.HOST) continue
37 37
38 options.uri = nextLink 38 options.uri = nextLink
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts
index 1abf43cd4..3b5ad47c9 100644
--- a/server/lib/activitypub/follow.ts
+++ b/server/lib/activitypub/follow.ts
@@ -3,8 +3,8 @@ import { CONFIG } from '../../initializers/config'
3import { SERVER_ACTOR_NAME } from '../../initializers/constants' 3import { SERVER_ACTOR_NAME } from '../../initializers/constants'
4import { JobQueue } from '../job-queue' 4import { JobQueue } from '../job-queue'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { getServerActor } from '../../helpers/utils'
7import { ServerModel } from '../../models/server/server' 6import { ServerModel } from '../../models/server/server'
7import { getServerActor } from '@server/models/application/application'
8 8
9async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { 9async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
10 if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return 10 if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return
@@ -27,7 +27,6 @@ async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
27 } 27 }
28 28
29 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 29 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
30 .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err))
31 } 30 }
32} 31}
33 32
diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts
deleted file mode 100644
index d8c7d83b7..000000000
--- a/server/lib/activitypub/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
1export * from './process'
2export * from './send'
3export * from './actor'
4export * from './share'
5export * from './playlist'
6export * from './videos'
7export * from './video-comments'
8export * from './video-rates'
9export * from './url'
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index c52b715ef..c1d932a68 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -20,7 +20,9 @@ import { MAccountDefault, MAccountId, MVideoId } from '../../typings/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist' 20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist'
21 21
22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { 22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
23 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED 23 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
24 ? VideoPlaylistPrivacy.PUBLIC
25 : VideoPlaylistPrivacy.UNLISTED
24 26
25 return { 27 return {
26 name: playlistObject.name, 28 name: playlistObject.name,
@@ -205,7 +207,7 @@ async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusC
205 207
206 logger.info('Fetching remote playlist %s.', playlistUrl) 208 logger.info('Fetching remote playlist %s.', playlistUrl)
207 209
208 const { response, body } = await doRequest(options) 210 const { response, body } = await doRequest<any>(options)
209 211
210 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { 212 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
211 logger.debug('Remote video playlist JSON is not valid.', { body }) 213 logger.debug('Remote video playlist JSON is not valid.', { body })
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index 7e22125d5..26427aaa1 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -1,6 +1,6 @@
1import { ActivityAnnounce } from '../../../../shared/models/activitypub' 1import { ActivityAnnounce } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { sequelizeTypescript } from '../../../initializers' 3import { sequelizeTypescript } from '../../../initializers/database'
4import { VideoShareModel } from '../../../models/video/video-share' 4import { VideoShareModel } from '../../../models/video/video-share'
5import { forwardVideoRelatedActivity } from '../send/utils' 5import { forwardVideoRelatedActivity } from '../send/utils'
6import { getOrCreateVideoAndAccountAndChannel } from '../videos' 6import { getOrCreateVideoAndAccountAndChannel } from '../videos'
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index bee853721..566bf6992 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -2,7 +2,7 @@ import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../..
2import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' 2import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { resolveThread } from '../video-comments' 6import { resolveThread } from '../video-comments'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { forwardVideoRelatedActivity } from '../send/utils' 8import { forwardVideoRelatedActivity } from '../send/utils'
@@ -12,6 +12,7 @@ import { PlaylistObject } from '../../../../shared/models/activitypub/objects/pl
12import { createOrUpdateVideoPlaylist } from '../playlist' 12import { createOrUpdateVideoPlaylist } from '../playlist'
13import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 13import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
14import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' 14import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models'
15import { isRedundancyAccepted } from '@server/lib/redundancy'
15 16
16async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { 17async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) {
17 const { activity, byActor } = options 18 const { activity, byActor } = options
@@ -60,6 +61,8 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
60} 61}
61 62
62async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { 63async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) {
64 if (await isRedundancyAccepted(activity, byActor) !== true) return
65
63 const cacheFile = activity.object as CacheFileObject 66 const cacheFile = activity.object as CacheFileObject
64 67
65 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) 68 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object })
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index e76132f91..7c8dc83e8 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -1,7 +1,7 @@
1import { ActivityDelete } from '../../../../shared/models/activitypub' 1import { ActivityDelete } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript } from '../../../initializers' 4import { sequelizeTypescript } from '../../../initializers/database'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts
index debd8a67c..fcdd0b86e 100644
--- a/server/lib/activitypub/process/process-dislike.ts
+++ b/server/lib/activitypub/process/process-dislike.ts
@@ -1,7 +1,7 @@
1import { ActivityCreate, ActivityDislike } from '../../../../shared' 1import { ActivityCreate, ActivityDislike } from '../../../../shared'
2import { DislikeObject } from '../../../../shared/models/activitypub/objects' 2import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { sequelizeTypescript } from '../../../initializers' 4import { sequelizeTypescript } from '../../../initializers/database'
5import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 5import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
6import { getOrCreateVideoAndAccountAndChannel } from '../videos' 6import { getOrCreateVideoAndAccountAndChannel } from '../videos'
7import { forwardVideoRelatedActivity } from '../send/utils' 7import { forwardVideoRelatedActivity } from '../send/utils'
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index e6e9084de..7337f337c 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -2,13 +2,14 @@ import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../share
2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' 2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { VideoAbuseModel } from '../../../models/video/video-abuse' 6import { VideoAbuseModel } from '../../../models/video/video-abuse'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { Notifier } from '../../notifier' 8import { Notifier } from '../../notifier'
9import { getAPId } from '../../../helpers/activitypub' 9import { getAPId } from '../../../helpers/activitypub'
10import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 10import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
11import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models' 11import { MActorSignature, MVideoAbuseAccountVideo } from '../../../typings/models'
12import { AccountModel } from '@server/models/account/account'
12 13
13async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 14async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
14 const { activity, byActor } = options 15 const { activity, byActor } = options
@@ -36,8 +37,9 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
36 logger.debug('Reporting remote abuse for video %s.', getAPId(object)) 37 logger.debug('Reporting remote abuse for video %s.', getAPId(object))
37 38
38 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) 39 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
40 const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
39 41
40 const videoAbuse = await sequelizeTypescript.transaction(async t => { 42 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
41 const videoAbuseData = { 43 const videoAbuseData = {
42 reporterAccountId: account.id, 44 reporterAccountId: account.id,
43 reason: flag.content, 45 reason: flag.content,
@@ -45,15 +47,22 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
45 state: VideoAbuseState.PENDING 47 state: VideoAbuseState.PENDING
46 } 48 }
47 49
48 const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) as MVideoAbuseVideo 50 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
49 videoAbuseInstance.Video = video 51 videoAbuseInstance.Video = video
52 videoAbuseInstance.Account = reporterAccount
50 53
51 logger.info('Remote abuse for video uuid %s created', flag.object) 54 logger.info('Remote abuse for video uuid %s created', flag.object)
52 55
53 return videoAbuseInstance 56 return videoAbuseInstance
54 }) 57 })
55 58
56 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse) 59 const videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
60
61 Notifier.Instance.notifyOnNewVideoAbuse({
62 videoAbuse: videoAbuseJSON,
63 videoAbuseInstance,
64 reporter: reporterAccount.Actor.getIdentifier()
65 })
57 } catch (err) { 66 } catch (err) {
58 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) 67 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
59 } 68 }
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index db7fb8568..950d421dd 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -1,17 +1,17 @@
1import { ActivityFollow } from '../../../../shared/models/activitypub' 1import { ActivityFollow } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript } from '../../../initializers' 4import { sequelizeTypescript } from '../../../initializers/database'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 6import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
7import { sendAccept, sendReject } from '../send' 7import { sendAccept, sendReject } from '../send'
8import { Notifier } from '../../notifier' 8import { Notifier } from '../../notifier'
9import { getAPId } from '../../../helpers/activitypub' 9import { getAPId } from '../../../helpers/activitypub'
10import { getServerActor } from '../../../helpers/utils'
11import { CONFIG } from '../../../initializers/config' 10import { CONFIG } from '../../../initializers/config'
12import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 11import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
13import { MActorFollowActors, MActorSignature } from '../../../typings/models' 12import { MActorFollowActors, MActorSignature } from '../../../typings/models'
14import { autoFollowBackIfNeeded } from '../follow' 13import { autoFollowBackIfNeeded } from '../follow'
14import { getServerActor } from '@server/models/application/application'
15 15
16async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { 16async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
17 const { activity, byActor } = options 17 const { activity, byActor } = options
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index 62be0de42..fba3c76a4 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -1,6 +1,6 @@
1import { ActivityLike } from '../../../../shared/models/activitypub' 1import { ActivityLike } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { sequelizeTypescript } from '../../../initializers' 3import { sequelizeTypescript } from '../../../initializers/database'
4import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 4import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
5import { forwardVideoRelatedActivity } from '../send/utils' 5import { forwardVideoRelatedActivity } from '../send/utils'
6import { getOrCreateVideoAndAccountAndChannel } from '../videos' 6import { getOrCreateVideoAndAccountAndChannel } from '../videos'
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts
index 00e9afa10..9804436a2 100644
--- a/server/lib/activitypub/process/process-reject.ts
+++ b/server/lib/activitypub/process/process-reject.ts
@@ -1,5 +1,5 @@
1import { ActivityReject } from '../../../../shared/models/activitypub/activity' 1import { ActivityReject } from '../../../../shared/models/activitypub/activity'
2import { sequelizeTypescript } from '../../../initializers' 2import { sequelizeTypescript } from '../../../initializers/database'
3import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 3import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
4import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 4import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
5import { MActor } from '../../../typings/models' 5import { MActor } from '../../../typings/models'
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index 10643b2e9..9ef6a8a97 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -2,7 +2,7 @@ import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFile
2import { DislikeObject } from '../../../../shared/models/activitypub/objects' 2import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 6import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 8import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index a47d605d8..98ab0f83d 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -2,7 +2,7 @@ import { ActivityUpdate, CacheFileObject, VideoTorrentObject } from '../../../..
2import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' 2import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
3import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 3import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
@@ -16,6 +16,7 @@ import { PlaylistObject } from '../../../../shared/models/activitypub/objects/pl
16import { createOrUpdateVideoPlaylist } from '../playlist' 16import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 17import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
18import { MActorSignature, MAccountIdActor } from '../../../typings/models' 18import { MActorSignature, MAccountIdActor } from '../../../typings/models'
19import { isRedundancyAccepted } from '@server/lib/redundancy'
19 20
20async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 21async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
21 const { activity, byActor } = options 22 const { activity, byActor } = options
@@ -78,6 +79,8 @@ async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpd
78} 79}
79 80
80async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { 81async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) {
82 if (await isRedundancyAccepted(activity, byActor) !== true) return
83
81 const cacheFileObject = activity.object as CacheFileObject 84 const cacheFileObject = activity.object as CacheFileObject
82 85
83 if (!isCacheFileObjectValid(cacheFileObject)) { 86 if (!isCacheFileObjectValid(cacheFileObject)) {
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
index df29ee968..b3b6c933d 100644
--- a/server/lib/activitypub/process/process-view.ts
+++ b/server/lib/activitypub/process/process-view.ts
@@ -23,7 +23,8 @@ async function processCreateView (activity: ActivityView | ActivityCreate, byAct
23 23
24 const options = { 24 const options = {
25 videoObject, 25 videoObject,
26 fetchType: 'only-video' as 'only-video' 26 fetchType: 'only-immutable-attributes' as 'only-immutable-attributes',
27 allowRefresh: false as false
27 } 28 }
28 const { video } = await getOrCreateVideoAndAccountAndChannel(options) 29 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
29 30
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
index 9f0225b64..c4c6b849b 100644
--- a/server/lib/activitypub/send/send-accept.ts
+++ b/server/lib/activitypub/send/send-accept.ts
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { MActor, MActorFollowActors } from '../../../typings/models' 6import { MActor, MActorFollowActors } from '../../../typings/models'
7 7
8async function sendAccept (actorFollow: MActorFollowActors) { 8function sendAccept (actorFollow: MActorFollowActors) {
9 const follower = actorFollow.ActorFollower 9 const follower = actorFollow.ActorFollower
10 const me = actorFollow.ActorFollowing 10 const me = actorFollow.ActorFollowing
11 11
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
index a0f33852c..d03b358f1 100644
--- a/server/lib/activitypub/send/send-announce.ts
+++ b/server/lib/activitypub/send/send-announce.ts
@@ -28,7 +28,7 @@ async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare,
28 logger.info('Creating job to send announce %s.', videoShare.url) 28 logger.info('Creating job to send announce %s.', videoShare.url)
29 29
30 const followersException = [ byActor ] 30 const followersException = [ byActor ]
31 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException) 31 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException, 'Announce')
32} 32}
33 33
34function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce { 34function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce {
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 1709d8348..e521cabbc 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -6,7 +6,6 @@ import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unic
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { getServerActor } from '../../../helpers/utils'
10import { 9import {
11 MActorLight, 10 MActorLight,
12 MCommentOwnerVideo, 11 MCommentOwnerVideo,
@@ -16,6 +15,8 @@ import {
16 MVideoRedundancyFileVideo, 15 MVideoRedundancyFileVideo,
17 MVideoRedundancyStreamingPlaylistVideo 16 MVideoRedundancyStreamingPlaylistVideo
18} from '../../../typings/models' 17} from '../../../typings/models'
18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context'
19 20
20async function sendCreateVideo (video: MVideoAP, t: Transaction) { 21async function sendCreateVideo (video: MVideoAP, t: Transaction) {
21 if (!video.hasPrivacyForFederation()) return undefined 22 if (!video.hasPrivacyForFederation()) return undefined
@@ -42,7 +43,8 @@ async function sendCreateCacheFile (
42 byActor, 43 byActor,
43 video, 44 video,
44 url: fileRedundancy.url, 45 url: fileRedundancy.url,
45 object: fileRedundancy.toActivityPubObject() 46 object: fileRedundancy.toActivityPubObject(),
47 contextType: 'CacheFile'
46 }) 48 })
47} 49}
48 50
@@ -78,7 +80,8 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transacti
78 // Add the actor that commented too 80 // Add the actor that commented too
79 actorsInvolvedInComment.push(byActor) 81 actorsInvolvedInComment.push(byActor)
80 82
81 const parentsCommentActors = threadParentComments.map(c => c.Account.Actor) 83 const parentsCommentActors = threadParentComments.filter(c => !c.isDeleted())
84 .map(c => c.Account.Actor)
82 85
83 let audience: ActivityAudience 86 let audience: ActivityAudience
84 if (isOrigin) { 87 if (isOrigin) {
@@ -130,11 +133,12 @@ export {
130// --------------------------------------------------------------------------- 133// ---------------------------------------------------------------------------
131 134
132async function sendVideoRelatedCreateActivity (options: { 135async function sendVideoRelatedCreateActivity (options: {
133 byActor: MActorLight, 136 byActor: MActorLight
134 video: MVideoAccountLight, 137 video: MVideoAccountLight
135 url: string, 138 url: string
136 object: any, 139 object: any
137 transaction?: Transaction 140 transaction?: Transaction
141 contextType?: ContextType
138}) { 142}) {
139 const activityBuilder = (audience: ActivityAudience) => { 143 const activityBuilder = (audience: ActivityAudience) => {
140 return buildCreateActivity(options.url, options.byActor, options.object, audience) 144 return buildCreateActivity(options.url, options.byActor, options.object, audience)
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
index 3225ebf32..fd3f06dec 100644
--- a/server/lib/activitypub/send/send-delete.ts
+++ b/server/lib/activitypub/send/send-delete.ts
@@ -7,9 +7,9 @@ import { getDeleteActivityPubUrl } from '../url'
7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
8import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' 8import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
9import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
10import { getServerActor } from '../../../helpers/utils'
11import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video' 10import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
12import { MActorUrl } from '../../../typings/models' 11import { MActorUrl } from '../../../typings/models'
12import { getServerActor } from '@server/models/application/application'
13 13
14async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) { 14async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
15 logger.info('Creating job to broadcast delete of video %s.', video.url) 15 logger.info('Creating job to broadcast delete of video %s.', video.url)
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts
index 6e41f241f..600469c71 100644
--- a/server/lib/activitypub/send/send-dislike.ts
+++ b/server/lib/activitypub/send/send-dislike.ts
@@ -6,7 +6,7 @@ import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' 7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
8 8
9async function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to dislike %s.', video.url) 10 logger.info('Creating job to dislike %s.', video.url)
11 11
12 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
index da7638a7b..e4e523631 100644
--- a/server/lib/activitypub/send/send-flag.ts
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -7,7 +7,7 @@ import { Transaction } from 'sequelize'
7import { MActor, MVideoFullLight } from '../../../typings/models' 7import { MActor, MVideoFullLight } from '../../../typings/models'
8import { MVideoAbuseVideo } from '../../../typings/models/video' 8import { MVideoAbuseVideo } from '../../../typings/models/video'
9 9
10async function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { 10function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) {
11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user 11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user
12 12
13 const url = getVideoAbuseActivityPubUrl(videoAbuse) 13 const url = getVideoAbuseActivityPubUrl(videoAbuse)
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
index e84a6f98b..5db252325 100644
--- a/server/lib/activitypub/send/send-like.ts
+++ b/server/lib/activitypub/send/send-like.ts
@@ -6,7 +6,7 @@ import { audiencify, getAudience } from '../audience'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' 7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
8 8
9async function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to like %s.', video.url) 10 logger.info('Creating job to like %s.', video.url)
11 11
12 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
index 4258a3c36..643c468a9 100644
--- a/server/lib/activitypub/send/send-reject.ts
+++ b/server/lib/activitypub/send/send-reject.ts
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { MActor } from '../../../typings/models' 6import { MActor } from '../../../typings/models'
7 7
8async function sendReject (follower: MActor, following: MActor) { 8function sendReject (follower: MActor, following: MActor) {
9 if (!follower.serverId) { // This should never happen 9 if (!follower.serverId) { // This should never happen
10 logger.warn('Do not sending reject to local follower.') 10 logger.warn('Do not sending reject to local follower.')
11 return 11 return
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index e9ab5b3c5..33f1d4921 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -28,7 +28,7 @@ import {
28 MVideoShare 28 MVideoShare
29} from '../../../typings/models' 29} from '../../../typings/models'
30 30
31async function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) { 31function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
32 const me = actorFollow.ActorFollower 32 const me = actorFollow.ActorFollower
33 const following = actorFollow.ActorFollowing 33 const following = actorFollow.ActorFollowing
34 34
@@ -118,10 +118,10 @@ function undoActivityData (
118} 118}
119 119
120async function sendUndoVideoRelatedActivity (options: { 120async function sendUndoVideoRelatedActivity (options: {
121 byActor: MActor, 121 byActor: MActor
122 video: MVideoAccountLight, 122 video: MVideoAccountLight
123 url: string, 123 url: string
124 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, 124 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce
125 transaction: Transaction 125 transaction: Transaction
126}) { 126}) {
127 const activityBuilder = (audience: ActivityAudience) => { 127 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index 9c76671b5..7a4cf3f56 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -8,9 +8,7 @@ import { getUpdateActivityPubUrl } from '../url'
8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils' 8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience' 9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
10import { logger } from '../../../helpers/logger' 10import { logger } from '../../../helpers/logger'
11import { VideoCaptionModel } from '../../../models/video/video-caption'
12import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 11import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
13import { getServerActor } from '../../../helpers/utils'
14import { 12import {
15 MAccountDefault, 13 MAccountDefault,
16 MActor, 14 MActor,
@@ -21,6 +19,7 @@ import {
21 MVideoPlaylistFull, 19 MVideoPlaylistFull,
22 MVideoRedundancyVideo 20 MVideoRedundancyVideo
23} from '../../../typings/models' 21} from '../../../typings/models'
22import { getServerActor } from '@server/models/application/application'
24 23
25async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { 24async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
26 const video = videoArg as MVideoAP 25 const video = videoArg as MVideoAP
@@ -29,7 +28,7 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction
29 28
30 logger.info('Creating job to update video %s.', video.url) 29 logger.info('Creating job to update video %s.', video.url)
31 30
32 const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor 31 const byActor = overrodeByActor || video.VideoChannel.Account.Actor
33 32
34 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) 33 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
35 34
@@ -85,7 +84,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide
85 return buildUpdateActivity(url, byActor, redundancyObject, audience) 84 return buildUpdateActivity(url, byActor, redundancyObject, audience)
86 } 85 }
87 86
88 return sendVideoRelatedActivity(activityBuilder, { byActor, video }) 87 return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' })
89} 88}
90 89
91async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) { 90async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) {
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
index 8809417f9..1f864ea52 100644
--- a/server/lib/activitypub/send/send-view.ts
+++ b/server/lib/activitypub/send/send-view.ts
@@ -5,9 +5,9 @@ import { getVideoLikeActivityPubUrl } from '../url'
5import { sendVideoRelatedActivity } from './utils' 5import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { MActorAudience, MVideoAccountLight, MVideoUrl } from '@server/typings/models' 8import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/typings/models'
9 9
10async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Transaction) { 10async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
11 logger.info('Creating job to send view of %s.', video.url) 11 logger.info('Creating job to send view of %s.', video.url)
12 12
13 const activityBuilder = (audience: ActivityAudience) => { 13 const activityBuilder = (audience: ActivityAudience) => {
@@ -16,7 +16,7 @@ async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Tran
16 return buildViewActivity(url, byActor, video, audience) 16 return buildViewActivity(url, byActor, video, audience)
17 } 17 }
18 18
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t, contextType: 'View' })
20} 20}
21 21
22function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView { 22function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView {
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts
index 77b723479..44a8926e5 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/utils.ts
@@ -5,26 +5,30 @@ import { ActorModel } from '../../../models/activitypub/actor'
5import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 5import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
6import { JobQueue } from '../../job-queue' 6import { JobQueue } from '../../job-queue'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' 7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
8import { getServerActor } from '../../../helpers/utils'
9import { afterCommitIfTransaction } from '../../../helpers/database-utils' 8import { afterCommitIfTransaction } from '../../../helpers/database-utils'
10import { MActorWithInboxes, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models' 9import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models'
10import { getServerActor } from '@server/models/application/application'
11import { ContextType } from '@shared/models/activitypub/context'
11 12
12async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { 13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
13 byActor: MActorLight, 14 byActor: MActorLight
14 video: MVideoAccountLight, 15 video: MVideoImmutable | MVideoAccountLight
15 transaction?: Transaction 16 transaction?: Transaction
17 contextType?: ContextType
16}) { 18}) {
17 const { byActor, video, transaction } = options 19 const { byActor, video, transaction, contextType } = options
18 20
19 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction) 21 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction)
20 22
21 // Send to origin 23 // Send to origin
22 if (video.isOwned() === false) { 24 if (video.isOwned() === false) {
23 const audience = getRemoteVideoAudience(video, actorsInvolvedInVideo) 25 const accountActor = (video as MVideoAccountLight).VideoChannel?.Account?.Actor || await ActorModel.loadAccountActorByVideoId(video.id)
26
27 const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo)
24 const activity = activityBuilder(audience) 28 const activity = activityBuilder(audience)
25 29
26 return afterCommitIfTransaction(transaction, () => { 30 return afterCommitIfTransaction(transaction, () => {
27 return unicastTo(activity, byActor, video.VideoChannel.Account.Actor.getSharedInbox()) 31 return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
28 }) 32 })
29 } 33 }
30 34
@@ -34,14 +38,14 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
34 38
35 const actorsException = [ byActor ] 39 const actorsException = [ byActor ]
36 40
37 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException) 41 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType)
38} 42}
39 43
40async function forwardVideoRelatedActivity ( 44async function forwardVideoRelatedActivity (
41 activity: Activity, 45 activity: Activity,
42 t: Transaction, 46 t: Transaction,
43 followersException: MActorWithInboxes[] = [], 47 followersException: MActorWithInboxes[],
44 video: MVideo 48 video: MVideoId
45) { 49) {
46 // Mastodon does not add our announces in audience, so we forward to them manually 50 // Mastodon does not add our announces in audience, so we forward to them manually
47 const additionalActors = await getActorsInvolvedInVideo(video, t) 51 const additionalActors = await getActorsInvolvedInVideo(video, t)
@@ -90,11 +94,12 @@ async function broadcastToFollowers (
90 byActor: MActorId, 94 byActor: MActorId,
91 toFollowersOf: MActorId[], 95 toFollowersOf: MActorId[],
92 t: Transaction, 96 t: Transaction,
93 actorsException: MActorWithInboxes[] = [] 97 actorsException: MActorWithInboxes[] = [],
98 contextType?: ContextType
94) { 99) {
95 const uris = await computeFollowerUris(toFollowersOf, actorsException, t) 100 const uris = await computeFollowerUris(toFollowersOf, actorsException, t)
96 101
97 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) 102 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType))
98} 103}
99 104
100async function broadcastToActors ( 105async function broadcastToActors (
@@ -102,13 +107,14 @@ async function broadcastToActors (
102 byActor: MActorId, 107 byActor: MActorId,
103 toActors: MActor[], 108 toActors: MActor[],
104 t?: Transaction, 109 t?: Transaction,
105 actorsException: MActorWithInboxes[] = [] 110 actorsException: MActorWithInboxes[] = [],
111 contextType?: ContextType
106) { 112) {
107 const uris = await computeUris(toActors, actorsException) 113 const uris = await computeUris(toActors, actorsException)
108 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) 114 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType))
109} 115}
110 116
111function broadcastTo (uris: string[], data: any, byActor: MActorId) { 117function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType?: ContextType) {
112 if (uris.length === 0) return undefined 118 if (uris.length === 0) return undefined
113 119
114 logger.debug('Creating broadcast job.', { uris }) 120 logger.debug('Creating broadcast job.', { uris })
@@ -116,19 +122,21 @@ function broadcastTo (uris: string[], data: any, byActor: MActorId) {
116 const payload = { 122 const payload = {
117 uris, 123 uris,
118 signatureActorId: byActor.id, 124 signatureActorId: byActor.id,
119 body: data 125 body: data,
126 contextType
120 } 127 }
121 128
122 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) 129 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })
123} 130}
124 131
125function unicastTo (data: any, byActor: MActorId, toActorUrl: string) { 132function unicastTo (data: any, byActor: MActorId, toActorUrl: string, contextType?: ContextType) {
126 logger.debug('Creating unicast job.', { uri: toActorUrl }) 133 logger.debug('Creating unicast job.', { uri: toActorUrl })
127 134
128 const payload = { 135 const payload = {
129 uri: toActorUrl, 136 uri: toActorUrl,
130 signatureActorId: byActor.id, 137 signatureActorId: byActor.id,
131 body: data 138 body: data,
139 contextType
132 } 140 }
133 141
134 JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) 142 JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload })
@@ -153,7 +161,7 @@ async function computeFollowerUris (toFollowersOf: MActorId[], actorsException:
153 const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) 161 const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
154 const sharedInboxesException = await buildSharedInboxesException(actorsException) 162 const sharedInboxesException = await buildSharedInboxesException(actorsException)
155 163
156 return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) 164 return result.data.filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false)
157} 165}
158 166
159async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) { 167async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) {
@@ -166,7 +174,7 @@ async function computeUris (toActors: MActor[], actorsException: MActorWithInbox
166 174
167 const sharedInboxesException = await buildSharedInboxesException(actorsException) 175 const sharedInboxesException = await buildSharedInboxesException(actorsException)
168 return Array.from(toActorSharedInboxesSet) 176 return Array.from(toActorSharedInboxesSet)
169 .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) 177 .filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false)
170} 178}
171 179
172async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) { 180async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) {
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index e847c4b7d..d2cbc59a8 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,5 +1,4 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { getServerActor } from '../../helpers/utils'
3import { VideoShareModel } from '../../models/video/video-share' 2import { VideoShareModel } from '../../models/video/video-share'
4import { sendUndoAnnounce, sendVideoAnnounce } from './send' 3import { sendUndoAnnounce, sendVideoAnnounce } from './send'
5import { getVideoAnnounceActivityPubUrl } from './url' 4import { getVideoAnnounceActivityPubUrl } from './url'
@@ -10,6 +9,7 @@ import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 9import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
12import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video' 11import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video'
12import { getServerActor } from '@server/models/application/application'
13 13
14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
15 if (!video.hasPrivacyForFederation()) return undefined 15 if (!video.hasPrivacyForFederation()) return undefined
@@ -36,7 +36,7 @@ async function addVideoShares (shareUrls: string[], video: MVideoId) {
36 await Bluebird.map(shareUrls, async shareUrl => { 36 await Bluebird.map(shareUrls, async shareUrl => {
37 try { 37 try {
38 // Fetch url 38 // Fetch url
39 const { body } = await doRequest({ 39 const { body } = await doRequest<any>({
40 uri: shareUrl, 40 uri: shareUrl,
41 json: true, 41 json: true,
42 activityPub: true 42 activityPub: true
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d5c078a29..3aee6799e 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -10,9 +10,9 @@ import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video' 10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string, 13 url: string
14 comments?: MCommentOwner[], 14 comments?: MCommentOwner[]
15 isVideo?: boolean, 15 isVideo?: boolean
16 commentCreated?: boolean 16 commentCreated?: boolean
17} 17}
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
@@ -28,7 +28,7 @@ async function resolveThread (params: ResolveThreadParams): ResolveThreadResult
28 if (params.commentCreated === undefined) params.commentCreated = false 28 if (params.commentCreated === undefined) params.commentCreated = false
29 if (params.comments === undefined) params.comments = [] 29 if (params.comments === undefined) params.comments = []
30 30
31 // Already have this comment? 31 // Already have this comment?
32 if (isVideo !== true) { 32 if (isVideo !== true) {
33 const result = await resolveCommentFromDB(params) 33 const result = await resolveCommentFromDB(params)
34 if (result) return result 34 if (result) return result
@@ -87,7 +87,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
87 87
88 let resultComment: MCommentOwnerVideo 88 let resultComment: MCommentOwnerVideo
89 if (comments.length !== 0) { 89 if (comments.length !== 0) {
90 const firstReply = comments[ comments.length - 1 ] as MCommentOwnerVideo 90 const firstReply = comments[comments.length - 1] as MCommentOwnerVideo
91 firstReply.inReplyToCommentId = null 91 firstReply.inReplyToCommentId = null
92 firstReply.originCommentId = null 92 firstReply.originCommentId = null
93 firstReply.videoId = video.id 93 firstReply.videoId = video.id
@@ -97,9 +97,9 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
97 comments[comments.length - 1] = await firstReply.save() 97 comments[comments.length - 1] = await firstReply.save()
98 98
99 for (let i = comments.length - 2; i >= 0; i--) { 99 for (let i = comments.length - 2; i >= 0; i--) {
100 const comment = comments[ i ] as MCommentOwnerVideo 100 const comment = comments[i] as MCommentOwnerVideo
101 comment.originCommentId = firstReply.id 101 comment.originCommentId = firstReply.id
102 comment.inReplyToCommentId = comments[ i + 1 ].id 102 comment.inReplyToCommentId = comments[i + 1].id
103 comment.videoId = video.id 103 comment.videoId = video.id
104 comment.changed('updatedAt', true) 104 comment.changed('updatedAt', true)
105 comment.Video = video 105 comment.Video = video
@@ -120,7 +120,7 @@ async function resolveParentComment (params: ResolveThreadParams) {
120 throw new Error('Recursion limit reached when resolving a thread') 120 throw new Error('Recursion limit reached when resolving a thread')
121 } 121 }
122 122
123 const { body } = await doRequest({ 123 const { body } = await doRequest<any>({
124 uri: url, 124 uri: url,
125 json: true, 125 json: true,
126 activityPub: true 126 activityPub: true
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 6bd46bb58..202368c8f 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -18,7 +18,7 @@ async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateTy
18 await Bluebird.map(ratesUrl, async rateUrl => { 18 await Bluebird.map(ratesUrl, async rateUrl => {
19 try { 19 try {
20 // Fetch url 20 // Fetch url
21 const { body } = await doRequest({ 21 const { body } = await doRequest<any>({
22 uri: rateUrl, 22 uri: rateUrl,
23 json: true, 23 json: true,
24 activityPub: true 24 activityPub: true
@@ -58,8 +58,6 @@ async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateTy
58 const field = rate === 'like' ? 'likes' : 'dislikes' 58 const field = rate === 'like' ? 'likes' : 'dislikes'
59 await video.increment(field, { by: rateCounts }) 59 await video.increment(field, { by: rateCounts })
60 } 60 }
61
62 return
63} 61}
64 62
65async function sendVideoRateChange ( 63async function sendVideoRateChange (
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index ade93150f..7d16bd390 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -6,25 +6,27 @@ import {
6 ActivityHashTagObject, 6 ActivityHashTagObject,
7 ActivityMagnetUrlObject, 7 ActivityMagnetUrlObject,
8 ActivityPlaylistSegmentHashesObject, 8 ActivityPlaylistSegmentHashesObject,
9 ActivityPlaylistUrlObject, ActivityTagObject, 9 ActivityPlaylistUrlObject, ActivitypubHttpFetcherPayload,
10 ActivityTagObject,
10 ActivityUrlObject, 11 ActivityUrlObject,
11 ActivityVideoUrlObject, 12 ActivityVideoUrlObject,
12 VideoState 13 VideoState
13} from '../../../shared/index' 14} from '../../../shared/index'
14import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 15import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
15import { VideoPrivacy } from '../../../shared/models/videos' 16import { VideoPrivacy } from '../../../shared/models/videos'
16import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 17import { isAPVideoFileMetadataObject, sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
17import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 18import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
18import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 19import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
19import { logger } from '../../helpers/logger' 20import { logger } from '../../helpers/logger'
20import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 21import { doRequest } from '../../helpers/requests'
21import { 22import {
22 ACTIVITY_PUB, 23 ACTIVITY_PUB,
23 MIMETYPES, 24 MIMETYPES,
24 P2P_MEDIA_LOADER_PEER_VERSION, 25 P2P_MEDIA_LOADER_PEER_VERSION,
25 PREVIEWS_SIZE, 26 PREVIEWS_SIZE,
26 REMOTE_SCHEME, 27 REMOTE_SCHEME,
27 STATIC_PATHS 28 STATIC_PATHS,
29 THUMBNAILS_SIZE
28} from '../../initializers/constants' 30} from '../../initializers/constants'
29import { TagModel } from '../../models/video/tag' 31import { TagModel } from '../../models/video/tag'
30import { VideoModel } from '../../models/video/video' 32import { VideoModel } from '../../models/video/video'
@@ -36,11 +38,10 @@ import { sendCreateVideo, sendUpdateVideo } from './send'
36import { isArray } from '../../helpers/custom-validators/misc' 38import { isArray } from '../../helpers/custom-validators/misc'
37import { VideoCaptionModel } from '../../models/video/video-caption' 39import { VideoCaptionModel } from '../../models/video/video-caption'
38import { JobQueue } from '../job-queue' 40import { JobQueue } from '../job-queue'
39import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
40import { createRates } from './video-rates' 41import { createRates } from './video-rates'
41import { addVideoShares, shareVideoByServerAndChannel } from './share' 42import { addVideoShares, shareVideoByServerAndChannel } from './share'
42import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 43import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
43import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 44import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
44import { Notifier } from '../notifier' 45import { Notifier } from '../notifier'
45import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' 46import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
46import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 47import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -68,9 +69,11 @@ import {
68 MVideoFile, 69 MVideoFile,
69 MVideoFullLight, 70 MVideoFullLight,
70 MVideoId, 71 MVideoId,
72 MVideoImmutable,
71 MVideoThumbnail 73 MVideoThumbnail
72} from '../../typings/models' 74} from '../../typings/models'
73import { MThumbnail } from '../../typings/models/video/thumbnail' 75import { MThumbnail } from '../../typings/models/video/thumbnail'
76import { maxBy, minBy } from 'lodash'
74 77
75async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { 78async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
76 const video = videoArg as MVideoAP 79 const video = videoArg as MVideoAP
@@ -109,7 +112,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
109 112
110 logger.info('Fetching remote video %s.', videoUrl) 113 logger.info('Fetching remote video %s.', videoUrl)
111 114
112 const { response, body } = await doRequest(options) 115 const { response, body } = await doRequest<any>(options)
113 116
114 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { 117 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
115 logger.debug('Remote video JSON is not valid.', { body }) 118 logger.debug('Remote video JSON is not valid.', { body })
@@ -127,23 +130,10 @@ async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
127 json: true 130 json: true
128 } 131 }
129 132
130 const { body } = await doRequest(options) 133 const { body } = await doRequest<any>(options)
131 return body.description ? body.description : '' 134 return body.description ? body.description : ''
132} 135}
133 136
134function fetchRemoteVideoStaticFile (video: MVideoAccountLight, path: string, destPath: string) {
135 const url = buildRemoteBaseUrl(video, path)
136
137 // We need to provide a callback, if no we could have an uncaught exception
138 return doRequestAndSaveToFile({ uri: url }, destPath)
139}
140
141function buildRemoteBaseUrl (video: MVideoAccountLight, path: string) {
142 const host = video.VideoChannel.Account.Actor.Server.host
143
144 return REMOTE_SCHEME.HTTP + '://' + host + path
145}
146
147function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 137function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
148 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 138 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
149 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 139 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
@@ -173,7 +163,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
173 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate) 163 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
174 164
175 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner) 165 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
176 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err })) 166 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.likes }))
177 } else { 167 } else {
178 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) 168 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
179 } 169 }
@@ -183,7 +173,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
183 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate) 173 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
184 174
185 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner) 175 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
186 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err })) 176 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.dislikes }))
187 } else { 177 } else {
188 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) 178 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
189 } 179 }
@@ -193,7 +183,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
193 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) 183 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
194 184
195 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner) 185 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
196 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err })) 186 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: fetchedVideo.shares }))
197 } else { 187 } else {
198 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) 188 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
199 } 189 }
@@ -203,32 +193,49 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
203 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) 193 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
204 194
205 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner) 195 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
206 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err })) 196 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: fetchedVideo.comments }))
207 } else { 197 } else {
208 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' }) 198 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
209 } 199 }
210 200
211 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })) 201 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }))
212} 202}
213 203
214function getOrCreateVideoAndAccountAndChannel (options: { 204type GetVideoResult <T> = Promise<{
215 videoObject: { id: string } | string, 205 video: T
216 syncParam?: SyncParam, 206 created: boolean
217 fetchType?: 'all', 207 autoBlacklisted?: boolean
208}>
209
210type GetVideoParamAll = {
211 videoObject: { id: string } | string
212 syncParam?: SyncParam
213 fetchType?: 'all'
218 allowRefresh?: boolean 214 allowRefresh?: boolean
219}): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }> 215}
220function getOrCreateVideoAndAccountAndChannel (options: { 216
221 videoObject: { id: string } | string, 217type GetVideoParamImmutable = {
222 syncParam?: SyncParam, 218 videoObject: { id: string } | string
223 fetchType?: VideoFetchByUrlType, 219 syncParam?: SyncParam
220 fetchType: 'only-immutable-attributes'
221 allowRefresh: false
222}
223
224type GetVideoParamOther = {
225 videoObject: { id: string } | string
226 syncParam?: SyncParam
227 fetchType?: 'all' | 'only-video'
224 allowRefresh?: boolean 228 allowRefresh?: boolean
225}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> 229}
226async function getOrCreateVideoAndAccountAndChannel (options: { 230
227 videoObject: { id: string } | string, 231function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
228 syncParam?: SyncParam, 232function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
229 fetchType?: VideoFetchByUrlType, 233function getOrCreateVideoAndAccountAndChannel (
230 allowRefresh?: boolean // true by default 234 options: GetVideoParamOther
231}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> { 235): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
236async function getOrCreateVideoAndAccountAndChannel (
237 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
238): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
232 // Default params 239 // Default params
233 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 240 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
234 const fetchType = options.fetchType || 'all' 241 const fetchType = options.fetchType || 'all'
@@ -236,18 +243,25 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
236 243
237 // Get video url 244 // Get video url
238 const videoUrl = getAPId(options.videoObject) 245 const videoUrl = getAPId(options.videoObject)
239
240 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 246 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
247
241 if (videoFromDatabase) { 248 if (videoFromDatabase) {
242 if (videoFromDatabase.isOutdated() && allowRefresh === true) { 249 // If allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type
250 if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) {
243 const refreshOptions = { 251 const refreshOptions = {
244 video: videoFromDatabase, 252 video: videoFromDatabase as MVideoThumbnail,
245 fetchedType: fetchType, 253 fetchedType: fetchType,
246 syncParam 254 syncParam
247 } 255 }
248 256
249 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) 257 if (syncParam.refreshVideo === true) {
250 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) 258 videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
259 } else {
260 await JobQueue.Instance.createJobWithPromise({
261 type: 'activitypub-refresher',
262 payload: { type: 'video', url: videoFromDatabase.url }
263 })
264 }
251 } 265 }
252 266
253 return { video: videoFromDatabase, created: false } 267 return { video: videoFromDatabase, created: false }
@@ -266,10 +280,10 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
266} 280}
267 281
268async function updateVideoFromAP (options: { 282async function updateVideoFromAP (options: {
269 video: MVideoAccountLightBlacklistAllFiles, 283 video: MVideoAccountLightBlacklistAllFiles
270 videoObject: VideoTorrentObject, 284 videoObject: VideoTorrentObject
271 account: MAccountIdActor, 285 account: MAccountIdActor
272 channel: MChannelDefault, 286 channel: MChannelDefault
273 overrideTo?: string[] 287 overrideTo?: string[]
274}) { 288}) {
275 const { video, videoObject, account, channel, overrideTo } = options 289 const { video, videoObject, account, channel, overrideTo } = options
@@ -284,7 +298,7 @@ async function updateVideoFromAP (options: {
284 let thumbnailModel: MThumbnail 298 let thumbnailModel: MThumbnail
285 299
286 try { 300 try {
287 thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 301 thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
288 } catch (err) { 302 } catch (err) {
289 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) 303 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
290 } 304 }
@@ -300,7 +314,7 @@ async function updateVideoFromAP (options: {
300 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url) 314 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
301 } 315 }
302 316
303 const to = overrideTo ? overrideTo : videoObject.to 317 const to = overrideTo || videoObject.to
304 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to) 318 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to)
305 video.name = videoData.name 319 video.name = videoData.name
306 video.uuid = videoData.uuid 320 video.uuid = videoData.uuid
@@ -327,10 +341,11 @@ async function updateVideoFromAP (options: {
327 341
328 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 342 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
329 343
330 // FIXME: use icon URL instead 344 if (videoUpdated.getPreview()) {
331 const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename)) 345 const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated)
332 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 346 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
333 await videoUpdated.addAndSaveThumbnail(previewModel, t) 347 await videoUpdated.addAndSaveThumbnail(previewModel, t)
348 }
334 349
335 { 350 {
336 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) 351 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url)
@@ -391,7 +406,7 @@ async function updateVideoFromAP (options: {
391 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) 406 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
392 407
393 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 408 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
394 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t) 409 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, c.url, t)
395 }) 410 })
396 await Promise.all(videoCaptionsPromises) 411 await Promise.all(videoCaptionsPromises)
397 } 412 }
@@ -424,8 +439,8 @@ async function updateVideoFromAP (options: {
424} 439}
425 440
426async function refreshVideoIfNeeded (options: { 441async function refreshVideoIfNeeded (options: {
427 video: MVideoThumbnail, 442 video: MVideoThumbnail
428 fetchedType: VideoFetchByUrlType, 443 fetchedType: VideoFetchByUrlType
429 syncParam: SyncParam 444 syncParam: SyncParam
430}): Promise<MVideoThumbnail> { 445}): Promise<MVideoThumbnail> {
431 if (!options.video.isOutdated()) return options.video 446 if (!options.video.isOutdated()) return options.video
@@ -483,7 +498,6 @@ export {
483 federateVideoIfNeeded, 498 federateVideoIfNeeded,
484 fetchRemoteVideo, 499 fetchRemoteVideo,
485 getOrCreateVideoAndAccountAndChannel, 500 getOrCreateVideoAndAccountAndChannel,
486 fetchRemoteVideoStaticFile,
487 fetchRemoteVideoDescription, 501 fetchRemoteVideoDescription,
488 getOrCreateVideoChannelFromVideoObject 502 getOrCreateVideoChannelFromVideoObject
489} 503}
@@ -494,7 +508,7 @@ function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
494 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) 508 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
495 509
496 const urlMediaType = url.mediaType 510 const urlMediaType = url.mediaType
497 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') 511 return mimeTypes.includes(urlMediaType) && urlMediaType.startsWith('video/')
498} 512}
499 513
500function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { 514function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
@@ -519,7 +533,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
519 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) 533 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
520 const video = VideoModel.build(videoData) as MVideoThumbnail 534 const video = VideoModel.build(videoData) as MVideoThumbnail
521 535
522 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 536 const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
537 .catch(err => {
538 logger.error('Cannot create miniature from url.', { err })
539 return undefined
540 })
523 541
524 let thumbnailModel: MThumbnail 542 let thumbnailModel: MThumbnail
525 if (waitThumbnail === true) { 543 if (waitThumbnail === true) {
@@ -534,9 +552,12 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
534 552
535 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 553 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
536 554
537 // FIXME: use icon URL instead 555 const previewIcon = getPreviewFromIcons(videoObject)
538 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) 556 const previewUrl = previewIcon
539 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 557 ? previewIcon.url
558 : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
559 const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
560
540 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 561 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
541 562
542 // Process files 563 // Process files
@@ -567,7 +588,7 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
567 588
568 // Process captions 589 // Process captions
569 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 590 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
570 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t) 591 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, c.url, t)
571 }) 592 })
572 await Promise.all(videoCaptionsPromises) 593 await Promise.all(videoCaptionsPromises)
573 594
@@ -588,7 +609,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
588 }) 609 })
589 610
590 if (waitThumbnail === false) { 611 if (waitThumbnail === false) {
612 // Error is already caught above
613 // eslint-disable-next-line @typescript-eslint/no-floating-promises
591 promiseThumbnail.then(thumbnailModel => { 614 promiseThumbnail.then(thumbnailModel => {
615 if (!thumbnailModel) return
616
592 thumbnailModel = videoCreated.id 617 thumbnailModel = videoCreated.id
593 618
594 return thumbnailModel.save() 619 return thumbnailModel.save()
@@ -598,24 +623,21 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
598 return { autoBlacklisted, videoCreated } 623 return { autoBlacklisted, videoCreated }
599} 624}
600 625
601async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) { 626function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) {
602 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED 627 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
603 const duration = videoObject.duration.replace(/[^\d]+/, '') 628 ? VideoPrivacy.PUBLIC
629 : VideoPrivacy.UNLISTED
604 630
605 let language: string | undefined 631 const duration = videoObject.duration.replace(/[^\d]+/, '')
606 if (videoObject.language) { 632 const language = videoObject.language?.identifier
607 language = videoObject.language.identifier
608 }
609 633
610 let category: number | undefined 634 const category = videoObject.category
611 if (videoObject.category) { 635 ? parseInt(videoObject.category.identifier, 10)
612 category = parseInt(videoObject.category.identifier, 10) 636 : undefined
613 }
614 637
615 let licence: number | undefined 638 const licence = videoObject.licence
616 if (videoObject.licence) { 639 ? parseInt(videoObject.licence.identifier, 10)
617 licence = parseInt(videoObject.licence.identifier, 10) 640 : undefined
618 }
619 641
620 const description = videoObject.content || null 642 const description = videoObject.content || null
621 const support = videoObject.support || null 643 const support = videoObject.support || null
@@ -638,8 +660,11 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide
638 duration: parseInt(duration, 10), 660 duration: parseInt(duration, 10),
639 createdAt: new Date(videoObject.published), 661 createdAt: new Date(videoObject.published),
640 publishedAt: new Date(videoObject.published), 662 publishedAt: new Date(videoObject.published),
641 originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null, 663
642 // FIXME: updatedAt does not seems to be considered by Sequelize 664 originallyPublishedAt: videoObject.originallyPublishedAt
665 ? new Date(videoObject.originallyPublishedAt)
666 : null,
667
643 updatedAt: new Date(videoObject.updated), 668 updatedAt: new Date(videoObject.updated),
644 views: videoObject.views, 669 views: videoObject.views,
645 likes: 0, 670 likes: 0,
@@ -670,13 +695,22 @@ function videoFileActivityUrlToDBAttributes (
670 throw new Error('Cannot parse magnet URI ' + magnet.href) 695 throw new Error('Cannot parse magnet URI ' + magnet.href)
671 } 696 }
672 697
698 // Fetch associated metadata url, if any
699 const metadata = urls.filter(isAPVideoFileMetadataObject)
700 .find(u => {
701 return u.height === fileUrl.height &&
702 u.fps === fileUrl.fps &&
703 u.rel.includes(fileUrl.mediaType)
704 })
705
673 const mediaType = fileUrl.mediaType 706 const mediaType = fileUrl.mediaType
674 const attribute = { 707 const attribute = {
675 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], 708 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[mediaType],
676 infoHash: parsed.infoHash, 709 infoHash: parsed.infoHash,
677 resolution: fileUrl.height, 710 resolution: fileUrl.height,
678 size: fileUrl.size, 711 size: fileUrl.size,
679 fps: fileUrl.fps || -1, 712 fps: fileUrl.fps || -1,
713 metadataUrl: metadata?.href,
680 714
681 // This is a video file owned by a video or by a streaming playlist 715 // This is a video file owned by a video or by a streaming playlist
682 videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id, 716 videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id,
@@ -722,3 +756,19 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
722 756
723 return attributes 757 return attributes
724} 758}
759
760function getThumbnailFromIcons (videoObject: VideoTorrentObject) {
761 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
762 // Fallback if there are not valid icons
763 if (validIcons.length === 0) validIcons = videoObject.icon
764
765 return minBy(validIcons, 'width')
766}
767
768function getPreviewFromIcons (videoObject: VideoTorrentObject) {
769 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
770
771 // FIXME: don't put a fallback here for compatibility with PeerTube <2.2
772
773 return maxBy(validIcons, 'width')
774}
diff --git a/server/lib/auth.ts b/server/lib/auth.ts
new file mode 100644
index 000000000..8579bdbb4
--- /dev/null
+++ b/server/lib/auth.ts
@@ -0,0 +1,286 @@
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
8import { UserRole } from '@shared/models'
9import {
10 RegisterServerAuthenticatedResult,
11 RegisterServerAuthPassOptions,
12 RegisterServerExternalAuthenticatedResult
13} from '@shared/models/plugins/register-server-auth.model'
14import * as express from 'express'
15import * as OAuthServer from 'express-oauth-server'
16
17const oAuthServer = new OAuthServer({
18 useErrorHandler: true,
19 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
20 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
21 continueMiddleware: true,
22 model: require('./oauth-model')
23})
24
25// Token is the key, expiration date is the value
26const authBypassTokens = new Map<string, {
27 expires: Date
28 user: {
29 username: string
30 email: string
31 displayName: string
32 role: UserRole
33 }
34 authName: string
35 npmName: string
36}>()
37
38async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
39 const grantType = req.body.grant_type
40
41 if (grantType === 'password') {
42 if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
43 else await proxifyPasswordGrant(req, res)
44 } else if (grantType === 'refresh_token') {
45 await proxifyRefreshGrant(req, res)
46 }
47
48 return forwardTokenReq(req, res, next)
49}
50
51async function handleTokenRevocation (req: express.Request, res: express.Response) {
52 const token = res.locals.oauth.token
53
54 res.locals.explicitLogout = true
55 await revokeToken(token)
56
57 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
58 // oAuthServer.revoke(req, res, err => {
59 // if (err) {
60 // logger.warn('Error in revoke token handler.', { err })
61 //
62 // return res.status(err.status)
63 // .json({
64 // error: err.message,
65 // code: err.name
66 // })
67 // .end()
68 // }
69 // })
70
71 return res.json()
72}
73
74async function onExternalUserAuthenticated (options: {
75 npmName: string
76 authName: string
77 authResult: RegisterServerExternalAuthenticatedResult
78}) {
79 const { npmName, authName, authResult } = options
80
81 if (!authResult.req || !authResult.res) {
82 logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName)
83 return
84 }
85
86 const { res } = authResult
87
88 if (!isAuthResultValid(npmName, authName, authResult)) {
89 res.redirect('/login?externalAuthError=true')
90 return
91 }
92
93 logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
94
95 const bypassToken = await generateRandomString(32)
96
97 const expires = new Date()
98 expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME)
99
100 const user = buildUserResult(authResult)
101 authBypassTokens.set(bypassToken, {
102 expires,
103 user,
104 npmName,
105 authName
106 })
107
108 // Cleanup
109 const now = new Date()
110 for (const [ key, value ] of authBypassTokens) {
111 if (value.expires.getTime() < now.getTime()) {
112 authBypassTokens.delete(key)
113 }
114 }
115
116 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
117}
118
119// ---------------------------------------------------------------------------
120
121export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
122
123// ---------------------------------------------------------------------------
124
125function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
126 return oAuthServer.token()(req, res, err => {
127 if (err) {
128 logger.warn('Login error.', { err })
129
130 return res.status(err.status)
131 .json({
132 error: err.message,
133 code: err.name
134 })
135 }
136
137 if (next) return next()
138 })
139}
140
141async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
142 const refreshToken = req.body.refresh_token
143 if (!refreshToken) return
144
145 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
146 if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName
147}
148
149async function proxifyPasswordGrant (req: express.Request, res: express.Response) {
150 const plugins = PluginManager.Instance.getIdAndPassAuths()
151 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
152
153 for (const plugin of plugins) {
154 const auths = plugin.idAndPassAuths
155
156 for (const auth of auths) {
157 pluginAuths.push({
158 npmName: plugin.npmName,
159 registerAuthOptions: auth
160 })
161 }
162 }
163
164 pluginAuths.sort((a, b) => {
165 const aWeight = a.registerAuthOptions.getWeight()
166 const bWeight = b.registerAuthOptions.getWeight()
167
168 // DESC weight order
169 if (aWeight === bWeight) return 0
170 if (aWeight < bWeight) return 1
171 return -1
172 })
173
174 const loginOptions = {
175 id: req.body.username,
176 password: req.body.password
177 }
178
179 for (const pluginAuth of pluginAuths) {
180 const authOptions = pluginAuth.registerAuthOptions
181 const authName = authOptions.authName
182 const npmName = pluginAuth.npmName
183
184 logger.debug(
185 'Using auth method %s of plugin %s to login %s with weight %d.',
186 authName, npmName, loginOptions.id, authOptions.getWeight()
187 )
188
189 try {
190 const loginResult = await authOptions.login(loginOptions)
191
192 if (!loginResult) continue
193 if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
194
195 logger.info(
196 'Login success with auth method %s of plugin %s for %s.',
197 authName, npmName, loginOptions.id
198 )
199
200 res.locals.bypassLogin = {
201 bypass: true,
202 pluginName: pluginAuth.npmName,
203 authName: authOptions.authName,
204 user: buildUserResult(loginResult)
205 }
206
207 return
208 } catch (err) {
209 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
210 }
211 }
212}
213
214function proxifyExternalAuthBypass (req: express.Request, res: express.Response) {
215 const obj = authBypassTokens.get(req.body.externalAuthToken)
216 if (!obj) {
217 logger.error('Cannot authenticate user with unknown bypass token')
218 return res.sendStatus(400)
219 }
220
221 const { expires, user, authName, npmName } = obj
222
223 const now = new Date()
224 if (now.getTime() > expires.getTime()) {
225 logger.error('Cannot authenticate user with an expired external auth token')
226 return res.sendStatus(400)
227 }
228
229 if (user.username !== req.body.username) {
230 logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username)
231 return res.sendStatus(400)
232 }
233
234 // Bypass oauth library validation
235 req.body.password = 'fake'
236
237 logger.info(
238 'Auth success with external auth method %s of plugin %s for %s.',
239 authName, npmName, user.email
240 )
241
242 res.locals.bypassLogin = {
243 bypass: true,
244 pluginName: npmName,
245 authName: authName,
246 user
247 }
248}
249
250function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
251 if (!isUserUsernameValid(result.username)) {
252 logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username })
253 return false
254 }
255
256 if (!result.email) {
257 logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email })
258 return false
259 }
260
261 // role is optional
262 if (result.role && !isUserRoleValid(result.role)) {
263 logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role })
264 return false
265 }
266
267 // display name is optional
268 if (result.displayName && !isUserDisplayNameValid(result.displayName)) {
269 logger.error(
270 'Auth method %s of plugin %s did not provide a valid display name.',
271 authName, npmName, { displayName: result.displayName }
272 )
273 return false
274 }
275
276 return true
277}
278
279function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
280 return {
281 username: pluginResult.username,
282 email: pluginResult.email,
283 role: pluginResult.role ?? UserRole.USER,
284 displayName: pluginResult.displayName || pluginResult.username
285 }
286}
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
index ad4cdd3ab..282d834a2 100644
--- a/server/lib/avatar.ts
+++ b/server/lib/avatar.ts
@@ -1,11 +1,11 @@
1import 'multer' 1import 'multer'
2import { sendUpdateActor } from './activitypub/send' 2import { sendUpdateActor } from './activitypub/send'
3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' 3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
4import { updateActorAvatarInstance } from './activitypub' 4import { updateActorAvatarInstance } from './activitypub/actor'
5import { processImage } from '../helpers/image-utils' 5import { processImage } from '../helpers/image-utils'
6import { extname, join } from 'path' 6import { extname, join } from 'path'
7import { retryTransactionWrapper } from '../helpers/database-utils' 7import { retryTransactionWrapper } from '../helpers/database-utils'
8import * as uuidv4 from 'uuid/v4' 8import { v4 as uuidv4 } from 'uuid'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import { sequelizeTypescript } from '../initializers/database' 10import { sequelizeTypescript } from '../initializers/database'
11import * as LRUCache from 'lru-cache' 11import * as LRUCache from 'lru-cache'
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
index 28c69b46e..842eecb5b 100644
--- a/server/lib/blocklist.ts
+++ b/server/lib/blocklist.ts
@@ -1,7 +1,7 @@
1import { sequelizeTypescript } from '../initializers' 1import { sequelizeTypescript } from '@server/initializers/database'
2import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models'
2import { AccountBlocklistModel } from '../models/account/account-blocklist' 3import { AccountBlocklistModel } from '../models/account/account-blocklist'
3import { ServerBlocklistModel } from '../models/server/server-blocklist' 4import { ServerBlocklistModel } from '../models/server/server-blocklist'
4import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models'
5 5
6function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { 6function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
7 return sequelizeTypescript.transaction(async t => { 7 return sequelizeTypescript.transaction(async t => {
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 1d8a08ed0..4a4b0d12f 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -17,7 +17,7 @@ import { MAccountActor, MChannelActor, MVideo } from '../typings/models'
17 17
18export class ClientHtml { 18export class ClientHtml {
19 19
20 private static htmlCache: { [ path: string ]: string } = {} 20 private static htmlCache: { [path: string]: string } = {}
21 21
22 static invalidCache () { 22 static invalidCache () {
23 logger.info('Cleaning HTML cache.') 23 logger.info('Cleaning HTML cache.')
@@ -94,7 +94,7 @@ export class ClientHtml {
94 94
95 private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { 95 private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) {
96 const path = ClientHtml.getIndexPath(req, res, paramLang) 96 const path = ClientHtml.getIndexPath(req, res, paramLang)
97 if (ClientHtml.htmlCache[ path ]) return ClientHtml.htmlCache[ path ] 97 if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
98 98
99 const buffer = await readFile(path) 99 const buffer = await readFile(path)
100 100
@@ -104,7 +104,7 @@ export class ClientHtml {
104 html = ClientHtml.addCustomCSS(html) 104 html = ClientHtml.addCustomCSS(html)
105 html = await ClientHtml.addAsyncPluginCSS(html) 105 html = await ClientHtml.addAsyncPluginCSS(html)
106 106
107 ClientHtml.htmlCache[ path ] = html 107 ClientHtml.htmlCache[path] = html
108 108
109 return html 109 return html
110 } 110 }
@@ -119,7 +119,7 @@ export class ClientHtml {
119 // Save locale in cookies 119 // Save locale in cookies
120 res.cookie('clientLanguage', lang, { 120 res.cookie('clientLanguage', lang, {
121 secure: WEBSERVER.SCHEME === 'https', 121 secure: WEBSERVER.SCHEME === 'https',
122 sameSite: true, 122 sameSite: 'none',
123 maxAge: 1000 * 3600 * 24 * 90 // 3 months 123 maxAge: 1000 * 3600 * 24 * 90 // 3 months
124 }) 124 })
125 125
@@ -214,21 +214,21 @@ export class ClientHtml {
214 const schemaTags = { 214 const schemaTags = {
215 '@context': 'http://schema.org', 215 '@context': 'http://schema.org',
216 '@type': 'VideoObject', 216 '@type': 'VideoObject',
217 name: videoNameEscaped, 217 'name': videoNameEscaped,
218 description: videoDescriptionEscaped, 218 'description': videoDescriptionEscaped,
219 thumbnailUrl: previewUrl, 219 'thumbnailUrl': previewUrl,
220 uploadDate: video.createdAt.toISOString(), 220 'uploadDate': video.createdAt.toISOString(),
221 duration: getActivityStreamDuration(video.duration), 221 'duration': getActivityStreamDuration(video.duration),
222 contentUrl: videoUrl, 222 'contentUrl': videoUrl,
223 embedUrl: embedUrl, 223 'embedUrl': embedUrl,
224 interactionCount: video.views 224 'interactionCount': video.views
225 } 225 }
226 226
227 let tagsString = '' 227 let tagsString = ''
228 228
229 // Opengraph 229 // Opengraph
230 Object.keys(openGraphMetaTags).forEach(tagName => { 230 Object.keys(openGraphMetaTags).forEach(tagName => {
231 const tagValue = openGraphMetaTags[ tagName ] 231 const tagValue = openGraphMetaTags[tagName]
232 232
233 tagsString += `<meta property="${tagName}" content="${tagValue}" />` 233 tagsString += `<meta property="${tagName}" content="${tagValue}" />`
234 }) 234 })
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 7484524a4..935c9e882 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,9 +1,8 @@
1import { createTransport, Transporter } from 'nodemailer' 1import { createTransport, Transporter } from 'nodemailer'
2import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance, root } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 3import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG } from '../initializers/config' 4import { CONFIG, isEmailEnabled } from '../initializers/config'
5import { JobQueue } from './job-queue' 5import { JobQueue } from './job-queue'
6import { EmailPayload } from './job-queue/handlers/email'
7import { readFileSync } from 'fs-extra' 6import { readFileSync } from 'fs-extra'
8import { WEBSERVER } from '../initializers/constants' 7import { WEBSERVER } from '../initializers/constants'
9import { 8import {
@@ -16,15 +15,13 @@ import {
16} from '../typings/models/video' 15} from '../typings/models/video'
17import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' 16import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
18import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' 17import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
19 18import { EmailPayload } from '@shared/models'
20type SendEmailOptions = { 19import { join } from 'path'
21 to: string[] 20import { VideoAbuse } from '../../shared/models/videos'
22 subject: string 21import { SendEmailOptions } from '../../shared/models/server/emailer.model'
23 text: string 22import { merge } from 'lodash'
24 23import { VideoChannelModel } from '@server/models/video/video-channel'
25 fromDisplayName?: string 24const Email = require('email-templates')
26 replyTo?: string
27}
28 25
29class Emailer { 26class Emailer {
30 27
@@ -32,41 +29,52 @@ class Emailer {
32 private initialized = false 29 private initialized = false
33 private transporter: Transporter 30 private transporter: Transporter
34 31
35 private constructor () {} 32 private constructor () {
33 }
36 34
37 init () { 35 init () {
38 // Already initialized 36 // Already initialized
39 if (this.initialized === true) return 37 if (this.initialized === true) return
40 this.initialized = true 38 this.initialized = true
41 39
42 if (Emailer.isEnabled()) { 40 if (isEmailEnabled()) {
43 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) 41 if (CONFIG.SMTP.TRANSPORT === 'smtp') {
42 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
44 43
45 let tls 44 let tls
46 if (CONFIG.SMTP.CA_FILE) { 45 if (CONFIG.SMTP.CA_FILE) {
47 tls = { 46 tls = {
48 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ] 47 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
48 }
49 } 49 }
50 }
51 50
52 let auth 51 let auth
53 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) { 52 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
54 auth = { 53 auth = {
55 user: CONFIG.SMTP.USERNAME, 54 user: CONFIG.SMTP.USERNAME,
56 pass: CONFIG.SMTP.PASSWORD 55 pass: CONFIG.SMTP.PASSWORD
56 }
57 } 57 }
58 }
59 58
60 this.transporter = createTransport({ 59 this.transporter = createTransport({
61 host: CONFIG.SMTP.HOSTNAME, 60 host: CONFIG.SMTP.HOSTNAME,
62 port: CONFIG.SMTP.PORT, 61 port: CONFIG.SMTP.PORT,
63 secure: CONFIG.SMTP.TLS, 62 secure: CONFIG.SMTP.TLS,
64 debug: CONFIG.LOG.LEVEL === 'debug', 63 debug: CONFIG.LOG.LEVEL === 'debug',
65 logger: bunyanLogger as any, 64 logger: bunyanLogger as any,
66 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS, 65 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
67 tls, 66 tls,
68 auth 67 auth
69 }) 68 })
69 } else { // sendmail
70 logger.info('Using sendmail to send emails')
71
72 this.transporter = createTransport({
73 sendmail: true,
74 newline: 'unix',
75 path: CONFIG.SMTP.SENDMAIL
76 })
77 }
70 } else { 78 } else {
71 if (!isTestInstance()) { 79 if (!isTestInstance()) {
72 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') 80 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@@ -75,11 +83,17 @@ class Emailer {
75 } 83 }
76 84
77 static isEnabled () { 85 static isEnabled () {
78 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT 86 if (CONFIG.SMTP.TRANSPORT === 'sendmail') {
87 return !!CONFIG.SMTP.SENDMAIL
88 } else if (CONFIG.SMTP.TRANSPORT === 'smtp') {
89 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
90 } else {
91 return false
92 }
79 } 93 }
80 94
81 async checkConnectionOrDie () { 95 async checkConnectionOrDie () {
82 if (!this.transporter) return 96 if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return
83 97
84 logger.info('Testing SMTP server...') 98 logger.info('Testing SMTP server...')
85 99
@@ -97,37 +111,36 @@ class Emailer {
97 const channelName = video.VideoChannel.getDisplayName() 111 const channelName = video.VideoChannel.getDisplayName()
98 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 112 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
99 113
100 const text = `Hi dear user,\n\n` +
101 `Your subscription ${channelName} just published a new video: ${video.name}` +
102 `\n\n` +
103 `You can view it on ${videoUrl} ` +
104 `\n\n` +
105 `Cheers,\n` +
106 `${CONFIG.EMAIL.BODY.SIGNATURE}`
107
108 const emailPayload: EmailPayload = { 114 const emailPayload: EmailPayload = {
109 to, 115 to,
110 subject: CONFIG.EMAIL.SUBJECT.PREFIX + channelName + ' just published a new video', 116 subject: channelName + ' just published a new video',
111 text 117 text: `Your subscription ${channelName} just published a new video: "${video.name}".`,
118 locals: {
119 title: 'New content ',
120 action: {
121 text: 'View video',
122 url: videoUrl
123 }
124 }
112 } 125 }
113 126
114 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 127 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
115 } 128 }
116 129
117 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { 130 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
118 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
119 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 131 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
120 132
121 const text = `Hi dear user,\n\n` +
122 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
123 `\n\n` +
124 `Cheers,\n` +
125 `${CONFIG.EMAIL.BODY.SIGNATURE}`
126
127 const emailPayload: EmailPayload = { 133 const emailPayload: EmailPayload = {
134 template: 'follower-on-channel',
128 to, 135 to,
129 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New follower on your channel ' + followingName, 136 subject: `New follower on your channel ${followingName}`,
130 text 137 locals: {
138 followerName: actorFollow.ActorFollower.Account.getDisplayName(),
139 followerUrl: actorFollow.ActorFollower.url,
140 followingName,
141 followingUrl: actorFollow.ActorFollowing.url,
142 followType
143 }
131 } 144 }
132 145
133 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 146 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -136,32 +149,28 @@ class Emailer {
136 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { 149 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
137 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' 150 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
138 151
139 const text = `Hi dear admin,\n\n` +
140 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
141 `\n\n` +
142 `Cheers,\n` +
143 `${CONFIG.EMAIL.BODY.SIGNATURE}`
144
145 const emailPayload: EmailPayload = { 152 const emailPayload: EmailPayload = {
146 to, 153 to,
147 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New instance follower', 154 subject: 'New instance follower',
148 text 155 text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`,
156 locals: {
157 title: 'New instance follower',
158 action: {
159 text: 'Review followers',
160 url: WEBSERVER.URL + '/admin/follows/followers-list'
161 }
162 }
149 } 163 }
150 164
151 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 165 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
152 } 166 }
153 167
154 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { 168 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
155 const text = `Hi dear admin,\n\n` + 169 const instanceUrl = actorFollow.ActorFollowing.url
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 = { 170 const emailPayload: EmailPayload = {
162 to, 171 to,
163 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following', 172 subject: 'Auto instance following',
164 text 173 text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
165 } 174 }
166 175
167 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 176 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -170,18 +179,17 @@ class Emailer {
170 myVideoPublishedNotification (to: string[], video: MVideo) { 179 myVideoPublishedNotification (to: string[], video: MVideo) {
171 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 180 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
172 181
173 const text = `Hi dear user,\n\n` +
174 `Your video ${video.name} has been published.` +
175 `\n\n` +
176 `You can view it on ${videoUrl} ` +
177 `\n\n` +
178 `Cheers,\n` +
179 `${CONFIG.EMAIL.BODY.SIGNATURE}`
180
181 const emailPayload: EmailPayload = { 182 const emailPayload: EmailPayload = {
182 to, 183 to,
183 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video ${video.name} is published`, 184 subject: `Your video ${video.name} has been published`,
184 text 185 text: `Your video "${video.name}" has been published.`,
186 locals: {
187 title: 'You video is live',
188 action: {
189 text: 'View video',
190 url: videoUrl
191 }
192 }
185 } 193 }
186 194
187 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 195 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -190,18 +198,17 @@ class Emailer {
190 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { 198 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
191 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() 199 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
192 200
193 const text = `Hi dear user,\n\n` +
194 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
195 `\n\n` +
196 `You can view the imported video on ${videoUrl} ` +
197 `\n\n` +
198 `Cheers,\n` +
199 `${CONFIG.EMAIL.BODY.SIGNATURE}`
200
201 const emailPayload: EmailPayload = { 201 const emailPayload: EmailPayload = {
202 to, 202 to,
203 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`, 203 subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`,
204 text 204 text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`,
205 locals: {
206 title: 'Import complete',
207 action: {
208 text: 'View video',
209 url: videoUrl
210 }
211 }
205 } 212 }
206 213
207 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 214 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -210,40 +217,47 @@ class Emailer {
210 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { 217 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
211 const importUrl = WEBSERVER.URL + '/my-account/video-imports' 218 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
212 219
213 const text = `Hi dear user,\n\n` + 220 const text =
214 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + 221 `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` +
215 `\n\n` + 222 '\n\n' +
216 `See your videos import dashboard for more information: ${importUrl}` + 223 `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`
217 `\n\n` +
218 `Cheers,\n` +
219 `${CONFIG.EMAIL.BODY.SIGNATURE}`
220 224
221 const emailPayload: EmailPayload = { 225 const emailPayload: EmailPayload = {
222 to, 226 to,
223 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, 227 subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`,
224 text 228 text,
229 locals: {
230 title: 'Import failed',
231 action: {
232 text: 'Review imports',
233 url: importUrl
234 }
235 }
225 } 236 }
226 237
227 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 238 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
228 } 239 }
229 240
230 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { 241 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
231 const accountName = comment.Account.getDisplayName()
232 const video = comment.Video 242 const video = comment.Video
243 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
233 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 244 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
234 245
235 const text = `Hi dear user,\n\n` +
236 `A new comment has been posted by ${accountName} on your video ${video.name}` +
237 `\n\n` +
238 `You can view it on ${commentUrl} ` +
239 `\n\n` +
240 `Cheers,\n` +
241 `${CONFIG.EMAIL.BODY.SIGNATURE}`
242
243 const emailPayload: EmailPayload = { 246 const emailPayload: EmailPayload = {
247 template: 'video-comment-new',
244 to, 248 to,
245 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New comment on your video ' + video.name, 249 subject: 'New comment on your video ' + video.name,
246 text 250 locals: {
251 accountName: comment.Account.getDisplayName(),
252 accountUrl: comment.Account.Actor.url,
253 comment,
254 video,
255 videoUrl,
256 action: {
257 text: 'View comment',
258 url: commentUrl
259 }
260 }
247 } 261 }
248 262
249 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 263 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -252,75 +266,88 @@ class Emailer {
252 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { 266 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
253 const accountName = comment.Account.getDisplayName() 267 const accountName = comment.Account.getDisplayName()
254 const video = comment.Video 268 const video = comment.Video
269 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
255 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 270 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
256 271
257 const text = `Hi dear user,\n\n` +
258 `${accountName} mentioned you on video ${video.name}` +
259 `\n\n` +
260 `You can view the comment on ${commentUrl} ` +
261 `\n\n` +
262 `Cheers,\n` +
263 `${CONFIG.EMAIL.BODY.SIGNATURE}`
264
265 const emailPayload: EmailPayload = { 272 const emailPayload: EmailPayload = {
273 template: 'video-comment-mention',
266 to, 274 to,
267 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Mention on video ' + video.name, 275 subject: 'Mention on video ' + video.name,
268 text 276 locals: {
277 comment,
278 video,
279 videoUrl,
280 accountName,
281 action: {
282 text: 'View comment',
283 url: commentUrl
284 }
285 }
269 } 286 }
270 287
271 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
272 } 289 }
273 290
274 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) { 291 addVideoAbuseModeratorsNotification (to: string[], parameters: {
275 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() 292 videoAbuse: VideoAbuse
276 293 videoAbuseInstance: MVideoAbuseVideo
277 const text = `Hi,\n\n` + 294 reporter: string
278 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + 295 }) {
279 `Cheers,\n` + 296 const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
280 `${CONFIG.EMAIL.BODY.SIGNATURE}` 297 const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
281 298
282 const emailPayload: EmailPayload = { 299 const emailPayload: EmailPayload = {
300 template: 'video-abuse-new',
283 to, 301 to,
284 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Received a video abuse', 302 subject: `New video abuse report from ${parameters.reporter}`,
285 text 303 locals: {
304 videoUrl,
305 videoAbuseUrl,
306 videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(),
307 videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(),
308 videoAbuse: parameters.videoAbuse,
309 reporter: parameters.reporter,
310 action: {
311 text: 'View report #' + parameters.videoAbuse.id,
312 url: videoAbuseUrl
313 }
314 }
286 } 315 }
287 316
288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 317 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
289 } 318 }
290 319
291 addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 320 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
292 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 321 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
293 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 322 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
294 323 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
295 const text = `Hi,\n\n` +
296 `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
297 `\n\n` +
298 `You can view it and take appropriate action on ${videoUrl}` +
299 `\n\n` +
300 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
301 `\n\n` +
302 `Cheers,\n` +
303 `${CONFIG.EMAIL.BODY.SIGNATURE}`
304 324
305 const emailPayload: EmailPayload = { 325 const emailPayload: EmailPayload = {
326 template: 'video-auto-blacklist-new',
306 to, 327 to,
307 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'An auto-blacklisted video is awaiting review', 328 subject: 'A new video is pending moderation',
308 text 329 locals: {
330 channel,
331 videoUrl,
332 videoName: videoBlacklist.Video.name,
333 action: {
334 text: 'Review autoblacklist',
335 url: VIDEO_AUTO_BLACKLIST_URL
336 }
337 }
309 } 338 }
310 339
311 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 340 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
312 } 341 }
313 342
314 addNewUserRegistrationNotification (to: string[], user: MUser) { 343 addNewUserRegistrationNotification (to: string[], user: MUser) {
315 const text = `Hi,\n\n` +
316 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
317 `Cheers,\n` +
318 `${CONFIG.EMAIL.BODY.SIGNATURE}`
319
320 const emailPayload: EmailPayload = { 344 const emailPayload: EmailPayload = {
345 template: 'user-registered',
321 to, 346 to,
322 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST, 347 subject: `a new user registered on ${WEBSERVER.HOST}: ${user.username}`,
323 text 348 locals: {
349 user
350 }
324 } 351 }
325 352
326 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 353 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -333,16 +360,13 @@ class Emailer {
333 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' 360 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
334 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.` 361 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`
335 362
336 const text = 'Hi,\n\n' +
337 blockedString +
338 '\n\n' +
339 'Cheers,\n' +
340 `${CONFIG.EMAIL.BODY.SIGNATURE}`
341
342 const emailPayload: EmailPayload = { 363 const emailPayload: EmailPayload = {
343 to, 364 to,
344 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${videoName} blacklisted`, 365 subject: `Video ${videoName} blacklisted`,
345 text 366 text: blockedString,
367 locals: {
368 title: 'Your video was blacklisted'
369 }
346 } 370 }
347 371
348 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 372 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -351,50 +375,53 @@ class Emailer {
351 addVideoUnblacklistNotification (to: string[], video: MVideo) { 375 addVideoUnblacklistNotification (to: string[], video: MVideo) {
352 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 376 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
353 377
354 const text = 'Hi,\n\n' +
355 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
356 '\n\n' +
357 'Cheers,\n' +
358 `${CONFIG.EMAIL.BODY.SIGNATURE}`
359
360 const emailPayload: EmailPayload = { 378 const emailPayload: EmailPayload = {
361 to, 379 to,
362 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${video.name} unblacklisted`, 380 subject: `Video ${video.name} unblacklisted`,
363 text 381 text: `Your video "${video.name}" (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.`,
382 locals: {
383 title: 'Your video was unblacklisted'
384 }
364 } 385 }
365 386
366 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 387 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
367 } 388 }
368 389
369 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { 390 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
370 const text = `Hi dear user,\n\n` + 391 const emailPayload: EmailPayload = {
371 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` + 392 template: 'password-reset',
372 `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` + 393 to: [ to ],
373 `If you are not the person who initiated this request, please ignore this email.\n\n` + 394 subject: 'Reset your account password',
374 `Cheers,\n` + 395 locals: {
375 `${CONFIG.EMAIL.BODY.SIGNATURE}` 396 resetPasswordUrl
397 }
398 }
376 399
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
401 }
402
403 addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
377 const emailPayload: EmailPayload = { 404 const emailPayload: EmailPayload = {
405 template: 'password-create',
378 to: [ to ], 406 to: [ to ],
379 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Reset your password', 407 subject: 'Create your account password',
380 text 408 locals: {
409 username,
410 createPasswordUrl
411 }
381 } 412 }
382 413
383 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 414 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
384 } 415 }
385 416
386 addVerifyEmailJob (to: string, verifyEmailUrl: string) { 417 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
387 const text = `Welcome to PeerTube,\n\n` +
388 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
389 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
390 `If you are not the person who initiated this request, please ignore this email.\n\n` +
391 `Cheers,\n` +
392 `${CONFIG.EMAIL.BODY.SIGNATURE}`
393
394 const emailPayload: EmailPayload = { 418 const emailPayload: EmailPayload = {
419 template: 'verify-email',
395 to: [ to ], 420 to: [ to ],
396 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Verify your email', 421 subject: `Verify your email on ${WEBSERVER.HOST}`,
397 text 422 locals: {
423 verifyEmailUrl
424 }
398 } 425 }
399 426
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 427 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -403,61 +430,76 @@ class Emailer {
403 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) { 430 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
404 const reasonString = reason ? ` for the following reason: ${reason}` : '' 431 const reasonString = reason ? ` for the following reason: ${reason}` : ''
405 const blockedWord = blocked ? 'blocked' : 'unblocked' 432 const blockedWord = blocked ? 'blocked' : 'unblocked'
406 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
407
408 const text = 'Hi,\n\n' +
409 blockedString +
410 '\n\n' +
411 'Cheers,\n' +
412 `${CONFIG.EMAIL.BODY.SIGNATURE}`
413 433
414 const to = user.email 434 const to = user.email
415 const emailPayload: EmailPayload = { 435 const emailPayload: EmailPayload = {
416 to: [ to ], 436 to: [ to ],
417 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Account ' + blockedWord, 437 subject: 'Account ' + blockedWord,
418 text 438 text: `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
419 } 439 }
420 440
421 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 441 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
422 } 442 }
423 443
424 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) { 444 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
425 const text = 'Hello dear admin,\n\n' +
426 fromName + ' sent you a message' +
427 '\n\n---------------------------------------\n\n' +
428 body +
429 '\n\n---------------------------------------\n\n' +
430 'Cheers,\n' +
431 'PeerTube.'
432
433 const emailPayload: EmailPayload = { 445 const emailPayload: EmailPayload = {
434 fromDisplayName: fromEmail, 446 template: 'contact-form',
435 replyTo: fromEmail,
436 to: [ CONFIG.ADMIN.EMAIL ], 447 to: [ CONFIG.ADMIN.EMAIL ],
437 subject: CONFIG.EMAIL.SUBJECT.PREFIX + subject, 448 replyTo: `"${fromName}" <${fromEmail}>`,
438 text 449 subject: `(contact form) ${subject}`,
450 locals: {
451 fromName,
452 fromEmail,
453 body
454 }
439 } 455 }
440 456
441 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 457 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
442 } 458 }
443 459
444 async sendMail (options: EmailPayload) { 460 async sendMail (options: EmailPayload) {
445 if (!Emailer.isEnabled()) { 461 if (!isEmailEnabled()) {
446 throw new Error('Cannot send mail because SMTP is not configured.') 462 throw new Error('Cannot send mail because SMTP is not configured.')
447 } 463 }
448 464
449 const fromDisplayName = options.fromDisplayName 465 const fromDisplayName = options.from
450 ? options.fromDisplayName 466 ? options.from
451 : WEBSERVER.HOST 467 : WEBSERVER.HOST
452 468
469 const email = new Email({
470 send: true,
471 message: {
472 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`
473 },
474 transport: this.transporter,
475 views: {
476 root: join(root(), 'server', 'lib', 'emails')
477 },
478 subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
479 })
480
453 for (const to of options.to) { 481 for (const to of options.to) {
454 await this.transporter.sendMail({ 482 await email
455 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`, 483 .send(merge(
456 replyTo: options.replyTo, 484 {
457 to, 485 template: 'common',
458 subject: options.subject, 486 message: {
459 text: options.text 487 to,
460 }) 488 from: options.from,
489 subject: options.subject,
490 replyTo: options.replyTo
491 },
492 locals: { // default variables available in all templates
493 WEBSERVER,
494 EMAIL: CONFIG.EMAIL,
495 text: options.text,
496 subject: options.subject
497 }
498 },
499 options // overriden/new variables given for a specific template in the payload
500 ) as SendEmailOptions)
501 .then(logger.info)
502 .catch(logger.error)
461 } 503 }
462 } 504 }
463 505
@@ -474,6 +516,5 @@ class Emailer {
474// --------------------------------------------------------------------------- 516// ---------------------------------------------------------------------------
475 517
476export { 518export {
477 Emailer, 519 Emailer
478 SendEmailOptions
479} 520}
diff --git a/server/lib/emails/common/base.pug b/server/lib/emails/common/base.pug
new file mode 100644
index 000000000..9a1894cab
--- /dev/null
+++ b/server/lib/emails/common/base.pug
@@ -0,0 +1,267 @@
1//-
2 The email background color is defined in three places:
3 1. body tag: for most email clients
4 2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
5 3. mso conditional: For Windows 10 Mail
6- var backgroundColor = "#fff";
7- var mainColor = "#f2690d";
8doctype html
9head
10 // This template is heavily adapted from the Cerberus Fluid template. Kudos to them!
11 meta(charset='utf-8')
12 //- utf-8 works for most cases
13 meta(name='viewport' content='width=device-width')
14 //- Forcing initial-scale shouldn't be necessary
15 meta(http-equiv='X-UA-Compatible' content='IE=edge')
16 //- Use the latest (edge) version of IE rendering engine
17 meta(name='x-apple-disable-message-reformatting')
18 //- Disable auto-scale in iOS 10 Mail entirely
19 meta(name='format-detection' content='telephone=no,address=no,email=no,date=no,url=no')
20 //- Tell iOS not to automatically link certain text strings.
21 meta(name='color-scheme' content='light')
22 meta(name='supported-color-schemes' content='light')
23 //- The title tag shows in email notifications, like Android 4.4.
24 title #{subject}
25 //- What it does: Makes background images in 72ppi Outlook render at correct size.
26 //if gte mso 9
27 xml
28 o:officedocumentsettings
29 o:allowpng
30 o:pixelsperinch 96
31 //- CSS Reset : BEGIN
32 style.
33 /* What it does: Tells the email client that only light styles are provided but the client can transform them to dark. A duplicate of meta color-scheme meta tag above. */
34 :root {
35 color-scheme: light;
36 supported-color-schemes: light;
37 }
38 /* What it does: Remove spaces around the email design added by some email clients. */
39 /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
40 html,
41 body {
42 margin: 0 auto !important;
43 padding: 0 !important;
44 height: 100% !important;
45 width: 100% !important;
46 }
47 /* What it does: Stops email clients resizing small text. */
48 * {
49 -ms-text-size-adjust: 100%;
50 -webkit-text-size-adjust: 100%;
51 }
52 /* What it does: Centers email on Android 4.4 */
53 div[style*="margin: 16px 0"] {
54 margin: 0 !important;
55 }
56 /* What it does: forces Samsung Android mail clients to use the entire viewport */
57 #MessageViewBody, #MessageWebViewDiv{
58 width: 100% !important;
59 }
60 /* What it does: Stops Outlook from adding extra spacing to tables. */
61 table,
62 td {
63 mso-table-lspace: 0pt !important;
64 mso-table-rspace: 0pt !important;
65 }
66 /* What it does: Fixes webkit padding issue. */
67 table {
68 border-spacing: 0 !important;
69 border-collapse: collapse !important;
70 table-layout: fixed !important;
71 margin: 0 auto !important;
72 }
73 /* What it does: Uses a better rendering method when resizing images in IE. */
74 img {
75 -ms-interpolation-mode:bicubic;
76 }
77 /* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
78 a {
79 text-decoration: none;
80 }
81 a:not(.nocolor) {
82 color: #{mainColor};
83 }
84 a.nocolor {
85 color: inherit !important;
86 }
87 /* What it does: A work-around for email clients meddling in triggered links. */
88 a[x-apple-data-detectors], /* iOS */
89 .unstyle-auto-detected-links a,
90 .aBn {
91 border-bottom: 0 !important;
92 cursor: default !important;
93 color: inherit !important;
94 text-decoration: none !important;
95 font-size: inherit !important;
96 font-family: inherit !important;
97 font-weight: inherit !important;
98 line-height: inherit !important;
99 }
100 /* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
101 .a6S {
102 display: none !important;
103 opacity: 0.01 !important;
104 }
105 /* What it does: Prevents Gmail from changing the text color in conversation threads. */
106 .im {
107 color: inherit !important;
108 }
109 /* If the above doesn't work, add a .g-img class to any image in question. */
110 img.g-img + div {
111 display: none !important;
112 }
113 /* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
114 /* Create one of these media queries for each additional viewport size you'd like to fix */
115 /* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
116 @media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
117 u ~ div .email-container {
118 min-width: 320px !important;
119 }
120 }
121 /* iPhone 6, 6S, 7, 8, and X */
122 @media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
123 u ~ div .email-container {
124 min-width: 375px !important;
125 }
126 }
127 /* iPhone 6+, 7+, and 8+ */
128 @media only screen and (min-device-width: 414px) {
129 u ~ div .email-container {
130 min-width: 414px !important;
131 }
132 }
133 //- CSS Reset : END
134 //- CSS for PeerTube : START
135 style.
136 blockquote {
137 margin-left: 0;
138 padding-left: 20px;
139 border-left: 2px solid #f2690d;
140 }
141 //- CSS for PeerTube : END
142 //- Progressive Enhancements : BEGIN
143 style.
144 /* What it does: Hover styles for buttons */
145 .button-td,
146 .button-a {
147 transition: all 100ms ease-in;
148 }
149 .button-td-primary:hover,
150 .button-a-primary:hover {
151 background: #555555 !important;
152 border-color: #555555 !important;
153 }
154 /* Media Queries */
155 @media screen and (max-width: 600px) {
156 /* What it does: Adjust typography on small screens to improve readability */
157 .email-container p {
158 font-size: 17px !important;
159 }
160 }
161 //- Progressive Enhancements : END
162
163body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #{backgroundColor};")
164 center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{backgroundColor};')
165 //if mso | IE
166 table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;')
167 tr
168 td
169 //- Visually Hidden Preheader Text : BEGIN
170 div(style='max-height:0; overflow:hidden; mso-hide:all;' aria-hidden='true')
171 block preheader
172 //- Visually Hidden Preheader Text : END
173
174 //- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary.
175 //- Preview Text Spacing Hack : BEGIN
176 div(style='display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;')
177 | &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
178 //- Preview Text Spacing Hack : END
179
180 //-
181 Set the email width. Defined in two places:
182 1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
183 2. MSO tags for Desktop Windows Outlook enforce a 600px width.
184 .email-container(style='max-width: 600px; margin: 0 auto;')
185 //if mso
186 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='600')
187 tr
188 td
189 //- Email Body : BEGIN
190 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
191 //- 1 Column Text + Button : BEGIN
192 tr
193 td(style='background-color: #ffffff;')
194 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
195 tr
196 td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
197 table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
198 tr
199 td(width="40px")
200 img(src=`${WEBSERVER.URL}/client/assets/images/icons/icon-192x192.png` width="auto" height="30px" alt="icon" border="0" style="height: 30px; background: #ffffff; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;")
201 td
202 h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;')
203 block title
204 if title
205 | #{title}
206 else
207 | Something requires your attention
208 p(style='margin: 0;')
209 block body
210 if action
211 tr
212 td(style='padding: 0 20px;')
213 //- Button : BEGIN
214 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' style='margin: auto;')
215 tr
216 td.button-td.button-td-primary(style='border-radius: 4px; background: #222222;')
217 a.button-a.button-a-primary(href=action.url style='background: #222222; border: 1px solid #000000; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;') #{action.text}
218 //- Button : END
219 //- 1 Column Text + Button : END
220 //- Clear Spacer : BEGIN
221 tr
222 td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
223 br
224 //- Clear Spacer : END
225 //- 1 Column Text : BEGIN
226 if username
227 tr
228 td(style='background-color: #cccccc;')
229 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
230 tr
231 td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
232 p(style='margin: 0;')
233 | You are receiving this email as part of your notification settings on #{WEBSERVER.HOST} for your account #{username}.
234 //- 1 Column Text : END
235 //- Email Body : END
236 //- Email Footer : BEGIN
237 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
238 tr
239 td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
240 webversion
241 a.nocolor(href=`${WEBSERVER.URL}/my-account/notifications` style='color: #cccccc; font-weight: bold;') View in your notifications
242 br
243 tr
244 td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
245 unsubscribe
246 a.nocolor(href=`${WEBSERVER.URL}/my-account/settings#notifications` style='color: #888888;') Manage your notification preferences in your profile
247 br
248 //- Email Footer : END
249 //if mso
250 //- Full Bleed Background Section : BEGIN
251 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style=`background-color: ${mainColor};`)
252 tr
253 td
254 .email-container(align='center' style='max-width: 600px; margin: auto;')
255 //if mso
256 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='600' align='center')
257 tr
258 td
259 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
260 tr
261 td(style='padding: 20px; text-align: left; font-family: sans-serif; font-size: 12px; line-height: 20px; color: #ffffff;')
262 table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
263 tr
264 td(valign="top") #[a(href="https://github.com/Chocobozzz/PeerTube" style="color: white !important") PeerTube © 2015-#{new Date().getFullYear()}] #[a(href="https://github.com/Chocobozzz/PeerTube/blob/master/CREDITS.md" style="color: white !important") PeerTube Contributors]
265 //if mso
266 //- Full Bleed Background Section : END
267 //if mso | IE
diff --git a/server/lib/emails/common/greetings.pug b/server/lib/emails/common/greetings.pug
new file mode 100644
index 000000000..5efe29dfb
--- /dev/null
+++ b/server/lib/emails/common/greetings.pug
@@ -0,0 +1,11 @@
1extends base
2
3block body
4 if username
5 p Hi #{username},
6 else
7 p Hi,
8 block content
9 p
10 | Cheers,#[br]
11 | #{EMAIL.BODY.SIGNATURE} \ No newline at end of file
diff --git a/server/lib/emails/common/html.pug b/server/lib/emails/common/html.pug
new file mode 100644
index 000000000..d76168b85
--- /dev/null
+++ b/server/lib/emails/common/html.pug
@@ -0,0 +1,4 @@
1extends greetings
2
3block content
4 p !{text} \ No newline at end of file
diff --git a/server/lib/emails/common/mixins.pug b/server/lib/emails/common/mixins.pug
new file mode 100644
index 000000000..76b805a24
--- /dev/null
+++ b/server/lib/emails/common/mixins.pug
@@ -0,0 +1,3 @@
1mixin channel(channel)
2 - var handle = `${channel.name}@${channel.host}`
3 | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file
diff --git a/server/lib/emails/contact-form/html.pug b/server/lib/emails/contact-form/html.pug
new file mode 100644
index 000000000..0073ff78e
--- /dev/null
+++ b/server/lib/emails/contact-form/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | Someone just used the contact form
5
6block content
7 p #{fromName} sent you a message via the contact form on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}]:
8 blockquote(style='white-space: pre-wrap') #{body}
9 p You can contact them at #[a(href=`mailto:${fromEmail}`) #{fromEmail}], or simply reply to this email to get in touch. \ No newline at end of file
diff --git a/server/lib/emails/follower-on-channel/html.pug b/server/lib/emails/follower-on-channel/html.pug
new file mode 100644
index 000000000..8a352e90f
--- /dev/null
+++ b/server/lib/emails/follower-on-channel/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New follower on your channel
5
6block content
7 p.
8 Your #{followType} #[a(href=followingUrl) #{followingName}] has a new subscriber:
9 #[a(href=followerUrl) #{followerName}]. \ No newline at end of file
diff --git a/server/lib/emails/password-create/html.pug b/server/lib/emails/password-create/html.pug
new file mode 100644
index 000000000..45ff3078a
--- /dev/null
+++ b/server/lib/emails/password-create/html.pug
@@ -0,0 +1,10 @@
1extends ../common/greetings
2
3block title
4 | Password creation for your account
5
6block content
7 p.
8 Welcome to #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your PeerTube instance. Your username is: #{username}.
9 Please set your password by following #[a(href=createPasswordUrl) this link]: #[a(href=createPasswordUrl) #{createPasswordUrl}]
10 (this link will expire within seven days). \ No newline at end of file
diff --git a/server/lib/emails/password-reset/html.pug b/server/lib/emails/password-reset/html.pug
new file mode 100644
index 000000000..bb6a9d16b
--- /dev/null
+++ b/server/lib/emails/password-reset/html.pug
@@ -0,0 +1,12 @@
1extends ../common/greetings
2
3block title
4 | Password reset for your account
5
6block content
7 p.
8 A reset password procedure for your account ${to} has been requested on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}].
9 Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
10 (the link will expire within 1 hour)
11 p.
12 If you are not the person who initiated this request, please ignore this email. \ No newline at end of file
diff --git a/server/lib/emails/user-registered/html.pug b/server/lib/emails/user-registered/html.pug
new file mode 100644
index 000000000..20f62125e
--- /dev/null
+++ b/server/lib/emails/user-registered/html.pug
@@ -0,0 +1,10 @@
1extends ../common/greetings
2
3block title
4 | A new user registered
5
6block content
7 - var mail = user.email || user.pendingEmail;
8 p
9 | User #[a(href=`${WEBSERVER.URL}/accounts/${user.username}`) #{user.username}] just registered.
10 | You might want to contact them at #[a(href=`mailto:${mail}`) #{mail}]. \ No newline at end of file
diff --git a/server/lib/emails/verify-email/html.pug b/server/lib/emails/verify-email/html.pug
new file mode 100644
index 000000000..8a4a77703
--- /dev/null
+++ b/server/lib/emails/verify-email/html.pug
@@ -0,0 +1,14 @@
1extends ../common/greetings
2
3block title
4 | Account verification
5
6block content
7 p Welcome to PeerTube!
8 p.
9 You just created an account #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your new PeerTube instance.
10 Your username there is: #{username}.
11 p.
12 To start using PeerTube on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] you must verify your email first!
13 Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you: #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
14 If you are not the person who initiated this request, please ignore this email. \ No newline at end of file
diff --git a/server/lib/emails/video-abuse-new/html.pug b/server/lib/emails/video-abuse-new/html.pug
new file mode 100644
index 000000000..999c89d26
--- /dev/null
+++ b/server/lib/emails/video-abuse-new/html.pug
@@ -0,0 +1,18 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | A video is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video "
10 a(href=videoUrl) #{videoAbuse.video.name}
11 | " by #[+channel(videoAbuse.video.channel)]
12 if videoPublishedAt
13 | , published the #{videoPublishedAt}.
14 else
15 | , uploaded the #{videoCreatedAt} but not yet published.
16 p The reporter, #{reporter}, cited the following reason(s):
17 blockquote #{videoAbuse.reason}
18 br(style="display: none;")
diff --git a/server/lib/emails/video-auto-blacklist-new/html.pug b/server/lib/emails/video-auto-blacklist-new/html.pug
new file mode 100644
index 000000000..07c8dfd16
--- /dev/null
+++ b/server/lib/emails/video-auto-blacklist-new/html.pug
@@ -0,0 +1,17 @@
1extends ../common/greetings
2include ../common/mixins
3
4block title
5 | A video is pending moderation
6
7block content
8 p
9 | A recently added video was auto-blacklisted and requires moderator review before going public:
10 |
11 a(href=videoUrl) #{videoName}
12 |
13 | by #[+channel(channel)].
14 p.
15 Apart from the publisher and the moderation team, no one will be able to see the video until you
16 unblacklist it. If you trust the publisher, any admin can whitelist the user for later videos so
17 that they don't require approval before going public.
diff --git a/server/lib/emails/video-comment-mention/html.pug b/server/lib/emails/video-comment-mention/html.pug
new file mode 100644
index 000000000..9e9ced62d
--- /dev/null
+++ b/server/lib/emails/video-comment-mention/html.pug
@@ -0,0 +1,11 @@
1extends ../common/greetings
2
3block title
4 | Someone mentioned you
5
6block content
7 p.
8 #[a(href=accountUrl title=handle) #{accountName}] mentioned you in a comment on video
9 "#[a(href=videoUrl) #{video.name}]":
10 blockquote #{comment.text}
11 br(style="display: none;") \ No newline at end of file
diff --git a/server/lib/emails/video-comment-new/html.pug b/server/lib/emails/video-comment-new/html.pug
new file mode 100644
index 000000000..075af5717
--- /dev/null
+++ b/server/lib/emails/video-comment-new/html.pug
@@ -0,0 +1,11 @@
1extends ../common/greetings
2
3block title
4 | Someone commented your video
5
6block content
7 p.
8 #[a(href=accountUrl title=handle) #{accountName}] added a comment on your video
9 "#[a(href=videoUrl) #{video.name}]":
10 blockquote #{comment.text}
11 br(style="display: none;") \ No newline at end of file
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts
index 440c3fde8..26ab3bd0d 100644
--- a/server/lib/files-cache/videos-caption-cache.ts
+++ b/server/lib/files-cache/videos-caption-cache.ts
@@ -5,7 +5,7 @@ import { VideoCaptionModel } from '../../models/video/video-caption'
5import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 5import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
6import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../helpers/logger'
8import { fetchRemoteVideoStaticFile } from '../activitypub' 8import { doRequestAndSaveToFile } from '@server/helpers/requests'
9 9
10type GetPathParam = { videoId: string, language: string } 10type GetPathParam = { videoId: string, language: string }
11 11
@@ -46,11 +46,10 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
46 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 46 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
47 if (!video) return undefined 47 if (!video) return undefined
48 48
49 // FIXME: use URL 49 const remoteUrl = videoCaption.getFileUrl(video)
50 const remoteStaticPath = videoCaption.getCaptionStaticPath()
51 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) 50 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
52 51
53 await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath) 52 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
54 53
55 return { isOwned: false, path: destPath } 54 return { isOwned: false, path: destPath }
56 } 55 }
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index 3da6bb138..d0d4fc5b5 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -1,9 +1,8 @@
1import { join } from 'path' 1import { join } from 'path'
2import { FILES_CACHE, STATIC_PATHS } from '../../initializers/constants' 2import { FILES_CACHE } from '../../initializers/constants'
3import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
4import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 4import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
5import { CONFIG } from '../../initializers/config' 5import { doRequestAndSaveToFile } from '@server/helpers/requests'
6import { fetchRemoteVideoStaticFile } from '../activitypub'
7 6
8class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { 7class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
9 8
@@ -32,11 +31,11 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
32 31
33 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') 32 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
34 33
35 // FIXME: use URL 34 const preview = video.getPreview()
36 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename) 35 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
37 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename)
38 36
39 await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath) 37 const remoteUrl = preview.getFileUrl(video)
38 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
40 39
41 return { isOwned: false, path: destPath } 40 return { isOwned: false, path: destPath }
42 } 41 }
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts
index 4a7cda0a2..7034c10d0 100644
--- a/server/lib/job-queue/handlers/activitypub-follow.ts
+++ b/server/lib/job-queue/handlers/activitypub-follow.ts
@@ -11,13 +11,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
11import { Notifier } from '../../notifier' 11import { Notifier } from '../../notifier'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models' 13import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models'
14 14import { ActivitypubFollowPayload } from '@shared/models'
15export type ActivitypubFollowPayload = {
16 followerActorId: number
17 name: string
18 host: string
19 isAutoFollow?: boolean
20}
21 15
22async function processActivityPubFollow (job: Bull.Job) { 16async function processActivityPubFollow (job: Bull.Job) {
23 const payload = job.data as ActivitypubFollowPayload 17 const payload = job.data as ActivitypubFollowPayload
@@ -34,6 +28,11 @@ async function processActivityPubFollow (job: Bull.Job) {
34 targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') 28 targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all')
35 } 29 }
36 30
31 if (payload.assertIsChannel && !targetActor.VideoChannel) {
32 logger.warn('Do not follow %s@%s because it is not a channel.', payload.name, host)
33 return
34 }
35
37 const fromActor = await ActorModel.load(payload.followerActorId) 36 const fromActor = await ActorModel.load(payload.followerActorId)
38 37
39 return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow) 38 return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow)
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
index 0ff7b44a0..e4d3dbbff 100644
--- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
@@ -5,12 +5,7 @@ import { doRequest } from '../../../helpers/requests'
5import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' 5import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
6import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers/constants' 6import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers/constants'
7import { ActorFollowScoreCache } from '../../files-cache' 7import { ActorFollowScoreCache } from '../../files-cache'
8 8import { ActivitypubHttpBroadcastPayload } from '@shared/models'
9export type ActivitypubHttpBroadcastPayload = {
10 uris: string[]
11 signatureActorId?: number
12 body: any
13}
14 9
15async function processActivityPubHttpBroadcast (job: Bull.Job) { 10async function processActivityPubHttpBroadcast (job: Bull.Job) {
16 logger.info('Processing ActivityPub broadcast in job %d.', job.id) 11 logger.info('Processing ActivityPub broadcast in job %d.', job.id)
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
index 0182c5169..524aadc27 100644
--- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
@@ -5,22 +5,15 @@ import { processActivities } from '../../activitypub/process'
5import { addVideoComments } from '../../activitypub/video-comments' 5import { addVideoComments } from '../../activitypub/video-comments'
6import { crawlCollectionPage } from '../../activitypub/crawl' 6import { crawlCollectionPage } from '../../activitypub/crawl'
7import { VideoModel } from '../../../models/video/video' 7import { VideoModel } from '../../../models/video/video'
8import { addVideoShares, createRates } from '../../activitypub' 8import { addVideoShares } from '../../activitypub/share'
9import { createRates } from '../../activitypub/video-rates'
9import { createAccountPlaylists } from '../../activitypub/playlist' 10import { createAccountPlaylists } from '../../activitypub/playlist'
10import { AccountModel } from '../../../models/account/account' 11import { AccountModel } from '../../../models/account/account'
11import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 12import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
12import { VideoShareModel } from '../../../models/video/video-share' 13import { VideoShareModel } from '../../../models/video/video-share'
13import { VideoCommentModel } from '../../../models/video/video-comment' 14import { VideoCommentModel } from '../../../models/video/video-comment'
14import { MAccountDefault, MVideoFullLight } from '../../../typings/models' 15import { MAccountDefault, MVideoFullLight } from '../../../typings/models'
15 16import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models'
16type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
17
18export type ActivitypubHttpFetcherPayload = {
19 uri: string
20 type: FetchType
21 videoId?: number
22 accountId?: number
23}
24 17
25async function processActivityPubHttpFetcher (job: Bull.Job) { 18async function processActivityPubHttpFetcher (job: Bull.Job) {
26 logger.info('Processing ActivityPub fetcher in job %d.', job.id) 19 logger.info('Processing ActivityPub fetcher in job %d.', job.id)
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
index c70ce3be9..b65eeb677 100644
--- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
@@ -4,12 +4,7 @@ import { doRequest } from '../../../helpers/requests'
4import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' 4import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
5import { JOB_REQUEST_TIMEOUT } from '../../../initializers/constants' 5import { JOB_REQUEST_TIMEOUT } from '../../../initializers/constants'
6import { ActorFollowScoreCache } from '../../files-cache' 6import { ActorFollowScoreCache } from '../../files-cache'
7 7import { ActivitypubHttpUnicastPayload } from '@shared/models'
8export type ActivitypubHttpUnicastPayload = {
9 uri: string
10 signatureActorId?: number
11 body: any
12}
13 8
14async function processActivityPubHttpUnicast (job: Bull.Job) { 9async function processActivityPubHttpUnicast (job: Bull.Job) {
15 logger.info('Processing ActivityPub unicast in job %d.', job.id) 10 logger.info('Processing ActivityPub unicast in job %d.', job.id)
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
index 4d6c38cfa..666e56868 100644
--- a/server/lib/job-queue/handlers/activitypub-refresher.ts
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -1,14 +1,12 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { fetchVideoByUrl } from '../../../helpers/video' 3import { fetchVideoByUrl } from '../../../helpers/video'
4import { refreshActorIfNeeded, refreshVideoIfNeeded, refreshVideoPlaylistIfNeeded } from '../../activitypub' 4import { refreshActorIfNeeded } from '../../activitypub/actor'
5import { refreshVideoIfNeeded } from '../../activitypub/videos'
5import { ActorModel } from '../../../models/activitypub/actor' 6import { ActorModel } from '../../../models/activitypub/actor'
6import { VideoPlaylistModel } from '../../../models/video/video-playlist' 7import { VideoPlaylistModel } from '../../../models/video/video-playlist'
7 8import { RefreshPayload } from '@shared/models'
8export type RefreshPayload = { 9import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist'
9 type: 'video' | 'video-playlist' | 'actor'
10 url: string
11}
12 10
13async function refreshAPObject (job: Bull.Job) { 11async function refreshAPObject (job: Bull.Job) {
14 const payload = job.data as RefreshPayload 12 const payload = job.data as RefreshPayload
diff --git a/server/lib/job-queue/handlers/email.ts b/server/lib/job-queue/handlers/email.ts
index 62701222c..3157731e2 100644
--- a/server/lib/job-queue/handlers/email.ts
+++ b/server/lib/job-queue/handlers/email.ts
@@ -1,8 +1,7 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { Emailer, SendEmailOptions } from '../../emailer' 3import { Emailer } from '../../emailer'
4 4import { EmailPayload } from '@shared/models'
5export type EmailPayload = SendEmailOptions
6 5
7async function processEmail (job: Bull.Job) { 6async function processEmail (job: Bull.Job) {
8 const payload = job.data as EmailPayload 7 const payload = job.data as EmailPayload
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
index d3bde6e6a..bcb49a731 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -1,11 +1,12 @@
1import { buildSignedActivity } from '../../../../helpers/activitypub' 1import { buildSignedActivity } from '../../../../helpers/activitypub'
2import { getServerActor } from '../../../../helpers/utils'
3import { ActorModel } from '../../../../models/activitypub/actor' 2import { ActorModel } from '../../../../models/activitypub/actor'
4import { sha256 } from '../../../../helpers/core-utils' 3import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants'
5import { HTTP_SIGNATURE } from '../../../../initializers/constants'
6import { MActor } from '../../../../typings/models' 4import { MActor } from '../../../../typings/models'
5import { getServerActor } from '@server/models/application/application'
6import { buildDigest } from '@server/helpers/peertube-crypto'
7import { ContextType } from '@shared/models/activitypub/context'
7 8
8type Payload = { body: any, signatureActorId?: number } 9type Payload = { body: any, contextType?: ContextType, signatureActorId?: number }
9 10
10async function computeBody (payload: Payload) { 11async function computeBody (payload: Payload) {
11 let body = payload.body 12 let body = payload.body
@@ -13,7 +14,7 @@ async function computeBody (payload: Payload) {
13 if (payload.signatureActorId) { 14 if (payload.signatureActorId) {
14 const actorSignature = await ActorModel.load(payload.signatureActorId) 15 const actorSignature = await ActorModel.load(payload.signatureActorId)
15 if (!actorSignature) throw new Error('Unknown signature actor id.') 16 if (!actorSignature) throw new Error('Unknown signature actor id.')
16 body = await buildSignedActivity(actorSignature, payload.body) 17 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType)
17 } 18 }
18 19
19 return body 20 return body
@@ -42,18 +43,13 @@ async function buildSignedRequestOptions (payload: Payload) {
42 43
43function buildGlobalHeaders (body: any) { 44function buildGlobalHeaders (body: any) {
44 return { 45 return {
45 'Digest': buildDigest(body) 46 'Digest': buildDigest(body),
47 'Content-Type': 'application/activity+json',
48 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
46 } 49 }
47} 50}
48 51
49function buildDigest (body: any) {
50 const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
51
52 return 'SHA-256=' + sha256(rawBody, 'base64')
53}
54
55export { 52export {
56 buildDigest,
57 buildGlobalHeaders, 53 buildGlobalHeaders,
58 computeBody, 54 computeBody,
59 buildSignedRequestOptions 55 buildSignedRequestOptions
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 99c991e72..ae11f1de3 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -9,11 +9,7 @@ import { extname } from 'path'
9import { MVideoFile, MVideoWithFile } from '@server/typings/models' 9import { MVideoFile, MVideoWithFile } from '@server/typings/models'
10import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 10import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
11import { getVideoFilePath } from '@server/lib/video-paths' 11import { getVideoFilePath } from '@server/lib/video-paths'
12 12import { VideoFileImportPayload } from '@shared/models'
13export type VideoFileImportPayload = {
14 videoUUID: string,
15 filePath: string
16}
17 13
18async function processVideoFileImport (job: Bull.Job) { 14async function processVideoFileImport (job: Bull.Job) {
19 const payload = job.data as VideoFileImportPayload 15 const payload = job.data as VideoFileImportPayload
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 1fca17584..ad549c6fc 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -7,9 +7,8 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro
7import { extname } from 'path' 7import { extname } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' 9import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
10import { VideoState } from '../../../../shared' 10import { VideoImportPayload, VideoImportTorrentPayload, VideoImportYoutubeDLPayload, VideoState } from '../../../../shared'
11import { JobQueue } from '../index' 11import { federateVideoIfNeeded } from '../../activitypub/videos'
12import { federateVideoIfNeeded } from '../../activitypub'
13import { VideoModel } from '../../../models/video/video' 12import { VideoModel } from '../../../models/video/video'
14import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' 13import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
15import { getSecureTorrentName } from '../../../helpers/utils' 14import { getSecureTorrentName } from '../../../helpers/utils'
@@ -17,27 +16,12 @@ import { move, remove, stat } from 'fs-extra'
17import { Notifier } from '../../notifier' 16import { Notifier } from '../../notifier'
18import { CONFIG } from '../../../initializers/config' 17import { CONFIG } from '../../../initializers/config'
19import { sequelizeTypescript } from '../../../initializers/database' 18import { sequelizeTypescript } from '../../../initializers/database'
20import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' 19import { generateVideoMiniature } from '../../thumbnail'
21import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 20import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
22import { MThumbnail } from '../../../typings/models/video/thumbnail' 21import { MThumbnail } from '../../../typings/models/video/thumbnail'
23import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' 22import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
24import { getVideoFilePath } from '@server/lib/video-paths' 23import { getVideoFilePath } from '@server/lib/video-paths'
25 24import { addOptimizeOrMergeAudioJob } from '@server/helpers/video'
26type VideoImportYoutubeDLPayload = {
27 type: 'youtube-dl'
28 videoImportId: number
29
30 thumbnailUrl: string
31 downloadThumbnail: boolean
32 downloadPreview: boolean
33}
34
35type VideoImportTorrentPayload = {
36 type: 'magnet-uri' | 'torrent-file'
37 videoImportId: number
38}
39
40export type VideoImportPayload = VideoImportYoutubeDLPayload | VideoImportTorrentPayload
41 25
42async function processVideoImport (job: Bull.Job) { 26async function processVideoImport (job: Bull.Job) {
43 const payload = job.data as VideoImportPayload 27 const payload = job.data as VideoImportPayload
@@ -62,9 +46,6 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP
62 const options = { 46 const options = {
63 videoImportId: payload.videoImportId, 47 videoImportId: payload.videoImportId,
64 48
65 downloadThumbnail: false,
66 downloadPreview: false,
67
68 generateThumbnail: true, 49 generateThumbnail: true,
69 generatePreview: true 50 generatePreview: true
70 } 51 }
@@ -82,15 +63,11 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
82 const options = { 63 const options = {
83 videoImportId: videoImport.id, 64 videoImportId: videoImport.id,
84 65
85 downloadThumbnail: payload.downloadThumbnail, 66 generateThumbnail: payload.generateThumbnail,
86 downloadPreview: payload.downloadPreview, 67 generatePreview: payload.generatePreview
87 thumbnailUrl: payload.thumbnailUrl,
88
89 generateThumbnail: false,
90 generatePreview: false
91 } 68 }
92 69
93 return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, VIDEO_IMPORT_TIMEOUT), videoImport, options) 70 return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options)
94} 71}
95 72
96async function getVideoImportOrDie (videoImportId: number) { 73async function getVideoImportOrDie (videoImportId: number) {
@@ -105,10 +82,6 @@ async function getVideoImportOrDie (videoImportId: number) {
105type ProcessFileOptions = { 82type ProcessFileOptions = {
106 videoImportId: number 83 videoImportId: number
107 84
108 downloadThumbnail: boolean
109 downloadPreview: boolean
110 thumbnailUrl?: string
111
112 generateThumbnail: boolean 85 generateThumbnail: boolean
113 generatePreview: boolean 86 generatePreview: boolean
114} 87}
@@ -153,17 +126,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
153 126
154 // Process thumbnail 127 // Process thumbnail
155 let thumbnailModel: MThumbnail 128 let thumbnailModel: MThumbnail
156 if (options.downloadThumbnail && options.thumbnailUrl) { 129 if (options.generateThumbnail) {
157 thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE)
158 } else if (options.generateThumbnail || options.downloadThumbnail) {
159 thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) 130 thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
160 } 131 }
161 132
162 // Process preview 133 // Process preview
163 let previewModel: MThumbnail 134 let previewModel: MThumbnail
164 if (options.downloadPreview && options.thumbnailUrl) { 135 if (options.generatePreview) {
165 previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW)
166 } else if (options.generatePreview || options.downloadPreview) {
167 previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) 136 previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
168 } 137 }
169 138
@@ -214,14 +183,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
214 183
215 // Create transcoding jobs? 184 // Create transcoding jobs?
216 if (video.state === VideoState.TO_TRANSCODE) { 185 if (video.state === VideoState.TO_TRANSCODE) {
217 // Put uuid because we don't have id auto incremented for now 186 await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile)
218 const dataInput = {
219 type: 'optimize' as 'optimize',
220 videoUUID: videoImportUpdated.Video.uuid,
221 isNewVideo: true
222 }
223
224 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
225 } 187 }
226 188
227 } catch (err) { 189 } catch (err) {
diff --git a/server/lib/job-queue/handlers/video-redundancy.ts b/server/lib/job-queue/handlers/video-redundancy.ts
new file mode 100644
index 000000000..6296dab05
--- /dev/null
+++ b/server/lib/job-queue/handlers/video-redundancy.ts
@@ -0,0 +1,17 @@
1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { VideosRedundancyScheduler } from '@server/lib/schedulers/videos-redundancy-scheduler'
4import { VideoRedundancyPayload } from '@shared/models'
5
6async function processVideoRedundancy (job: Bull.Job) {
7 const payload = job.data as VideoRedundancyPayload
8 logger.info('Processing video redundancy in job %d.', job.id)
9
10 return VideosRedundancyScheduler.Instance.createManualRedundancy(payload.videoId)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 processVideoRedundancy
17}
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 39b9fac98..46d52e1cf 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -1,48 +1,22 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { VideoResolution } from '../../../../shared' 2import {
3 MergeAudioTranscodingPayload,
4 NewResolutionTranscodingPayload,
5 OptimizeTranscodingPayload,
6 VideoTranscodingPayload
7} from '../../../../shared'
3import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
4import { VideoModel } from '../../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
5import { JobQueue } from '../job-queue' 10import { JobQueue } from '../job-queue'
6import { federateVideoIfNeeded } from '../../activitypub' 11import { federateVideoIfNeeded } from '../../activitypub/videos'
7import { retryTransactionWrapper } from '../../../helpers/database-utils' 12import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 13import { sequelizeTypescript } from '../../../initializers/database'
9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 14import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' 15import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
12import { Notifier } from '../../notifier' 16import { Notifier } from '../../notifier'
13import { CONFIG } from '../../../initializers/config' 17import { CONFIG } from '../../../initializers/config'
14import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models' 18import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models'
15 19
16interface BaseTranscodingPayload {
17 videoUUID: string
18 isNewVideo?: boolean
19}
20
21interface HLSTranscodingPayload extends BaseTranscodingPayload {
22 type: 'hls'
23 isPortraitMode?: boolean
24 resolution: VideoResolution
25 copyCodecs: boolean
26}
27
28interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {
29 type: 'new-resolution'
30 isPortraitMode?: boolean
31 resolution: VideoResolution
32}
33
34interface MergeAudioTranscodingPayload extends BaseTranscodingPayload {
35 type: 'merge-audio'
36 resolution: VideoResolution
37}
38
39interface OptimizeTranscodingPayload extends BaseTranscodingPayload {
40 type: 'optimize'
41}
42
43export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload
44 | OptimizeTranscodingPayload | MergeAudioTranscodingPayload
45
46async function processVideoTranscoding (job: Bull.Job) { 20async function processVideoTranscoding (job: Bull.Job) {
47 const payload = job.data as VideoTranscodingPayload 21 const payload = job.data as VideoTranscodingPayload
48 logger.info('Processing video file in job %d.', job.id) 22 logger.info('Processing video file in job %d.', job.id)
@@ -105,7 +79,7 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
105 79
106 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { 80 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
107 // Maybe the video changed in database, refresh it 81 // Maybe the video changed in database, refresh it
108 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) 82 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t)
109 // Video does not exist anymore 83 // Video does not exist anymore
110 if (!videoDatabase) return undefined 84 if (!videoDatabase) return undefined
111 85
@@ -118,12 +92,11 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
118 92
119 let videoPublished = false 93 let videoPublished = false
120 94
95 // Generate HLS version of the max quality file
121 const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution }) 96 const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution })
122 await createHlsJobIfEnabled(hlsPayload) 97 await createHlsJobIfEnabled(hlsPayload)
123 98
124 if (resolutionsEnabled.length !== 0) { 99 if (resolutionsEnabled.length !== 0) {
125 const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = []
126
127 for (const resolution of resolutionsEnabled) { 100 for (const resolution of resolutionsEnabled) {
128 let dataInput: VideoTranscodingPayload 101 let dataInput: VideoTranscodingPayload
129 102
@@ -143,12 +116,9 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
143 } 116 }
144 } 117 }
145 118
146 const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) 119 JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
147 tasks.push(p)
148 } 120 }
149 121
150 await Promise.all(tasks)
151
152 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) 122 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
153 } else { 123 } else {
154 // No transcoding to do, it's now published 124 // No transcoding to do, it's now published
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts
index 73fa5ed04..7211df237 100644
--- a/server/lib/job-queue/handlers/video-views.ts
+++ b/server/lib/job-queue/handlers/video-views.ts
@@ -3,7 +3,7 @@ import { logger } from '../../../helpers/logger'
3import { VideoModel } from '../../../models/video/video' 3import { VideoModel } from '../../../models/video/video'
4import { VideoViewModel } from '../../../models/video/video-views' 4import { VideoViewModel } from '../../../models/video/video-views'
5import { isTestInstance } from '../../../helpers/core-utils' 5import { isTestInstance } from '../../../helpers/core-utils'
6import { federateVideoIfNeeded } from '../../activitypub' 6import { federateVideoIfNeeded } from '../../activitypub/videos'
7 7
8async function processVideosViews () { 8async function processVideosViews () {
9 const lastHour = new Date() 9 const lastHour = new Date()
@@ -23,6 +23,8 @@ async function processVideosViews () {
23 for (const videoId of videoIds) { 23 for (const videoId of videoIds) {
24 try { 24 try {
25 const views = await Redis.Instance.getVideoViews(videoId, hour) 25 const views = await Redis.Instance.getVideoViews(videoId, hour)
26 await Redis.Instance.deleteVideoViews(videoId, hour)
27
26 if (views) { 28 if (views) {
27 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) 29 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour)
28 30
@@ -52,8 +54,6 @@ async function processVideosViews () {
52 logger.error('Cannot create video views for video %d in hour %d.', videoId, hour, { err }) 54 logger.error('Cannot create video views for video %d in hour %d.', videoId, hour, { err })
53 } 55 }
54 } 56 }
55
56 await Redis.Instance.deleteVideoViews(videoId, hour)
57 } catch (err) { 57 } catch (err) {
58 logger.error('Cannot update video views of video %d in hour %d.', videoId, hour, { err }) 58 logger.error('Cannot update video views of video %d in hour %d.', videoId, hour, { err })
59 } 59 }
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index ec601e9ea..14e181835 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -1,18 +1,32 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { JobState, JobType } from '../../../shared/models' 2import {
3 ActivitypubFollowPayload,
4 ActivitypubHttpBroadcastPayload,
5 ActivitypubHttpFetcherPayload,
6 ActivitypubHttpUnicastPayload,
7 EmailPayload,
8 JobState,
9 JobType,
10 RefreshPayload,
11 VideoFileImportPayload,
12 VideoImportPayload,
13 VideoRedundancyPayload,
14 VideoTranscodingPayload
15} from '../../../shared/models'
3import { logger } from '../../helpers/logger' 16import { logger } from '../../helpers/logger'
4import { Redis } from '../redis' 17import { Redis } from '../redis'
5import { JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants' 18import { JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants'
6import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' 19import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast'
7import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' 20import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher'
8import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' 21import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast'
9import { EmailPayload, processEmail } from './handlers/email' 22import { processEmail } from './handlers/email'
10import { processVideoTranscoding, VideoTranscodingPayload } from './handlers/video-transcoding' 23import { processVideoTranscoding } from './handlers/video-transcoding'
11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' 24import { processActivityPubFollow } from './handlers/activitypub-follow'
12import { processVideoImport, VideoImportPayload } from './handlers/video-import' 25import { processVideoImport } from './handlers/video-import'
13import { processVideosViews } from './handlers/video-views' 26import { processVideosViews } from './handlers/video-views'
14import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' 27import { refreshAPObject } from './handlers/activitypub-refresher'
15import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import' 28import { processVideoFileImport } from './handlers/video-file-import'
29import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy'
16 30
17type CreateJobArgument = 31type CreateJobArgument =
18 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 32 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -24,20 +38,21 @@ type CreateJobArgument =
24 { type: 'email', payload: EmailPayload } | 38 { type: 'email', payload: EmailPayload } |
25 { type: 'video-import', payload: VideoImportPayload } | 39 { type: 'video-import', payload: VideoImportPayload } |
26 { type: 'activitypub-refresher', payload: RefreshPayload } | 40 { type: 'activitypub-refresher', payload: RefreshPayload } |
27 { type: 'videos-views', payload: {} } 41 { type: 'videos-views', payload: {} } |
42 { type: 'video-redundancy', payload: VideoRedundancyPayload }
28 43
29const handlers: { [ id in (JobType | 'video-file') ]: (job: Bull.Job) => Promise<any>} = { 44const handlers: { [id in JobType]: (job: Bull.Job) => Promise<any> } = {
30 'activitypub-http-broadcast': processActivityPubHttpBroadcast, 45 'activitypub-http-broadcast': processActivityPubHttpBroadcast,
31 'activitypub-http-unicast': processActivityPubHttpUnicast, 46 'activitypub-http-unicast': processActivityPubHttpUnicast,
32 'activitypub-http-fetcher': processActivityPubHttpFetcher, 47 'activitypub-http-fetcher': processActivityPubHttpFetcher,
33 'activitypub-follow': processActivityPubFollow, 48 'activitypub-follow': processActivityPubFollow,
34 'video-file-import': processVideoFileImport, 49 'video-file-import': processVideoFileImport,
35 'video-transcoding': processVideoTranscoding, 50 'video-transcoding': processVideoTranscoding,
36 'video-file': processVideoTranscoding, // TODO: remove it (changed in 1.3)
37 'email': processEmail, 51 'email': processEmail,
38 'video-import': processVideoImport, 52 'video-import': processVideoImport,
39 'videos-views': processVideosViews, 53 'videos-views': processVideosViews,
40 'activitypub-refresher': refreshAPObject 54 'activitypub-refresher': refreshAPObject,
55 'video-redundancy': processVideoRedundancy
41} 56}
42 57
43const jobTypes: JobType[] = [ 58const jobTypes: JobType[] = [
@@ -50,20 +65,22 @@ const jobTypes: JobType[] = [
50 'video-file-import', 65 'video-file-import',
51 'video-import', 66 'video-import',
52 'videos-views', 67 'videos-views',
53 'activitypub-refresher' 68 'activitypub-refresher',
69 'video-redundancy'
54] 70]
55 71
56class JobQueue { 72class JobQueue {
57 73
58 private static instance: JobQueue 74 private static instance: JobQueue
59 75
60 private queues: { [ id in JobType ]?: Bull.Queue } = {} 76 private queues: { [id in JobType]?: Bull.Queue } = {}
61 private initialized = false 77 private initialized = false
62 private jobRedisPrefix: string 78 private jobRedisPrefix: string
63 79
64 private constructor () {} 80 private constructor () {
81 }
65 82
66 async init () { 83 init () {
67 // Already initialized 84 // Already initialized
68 if (this.initialized === true) return 85 if (this.initialized === true) return
69 this.initialized = true 86 this.initialized = true
@@ -105,11 +122,16 @@ class JobQueue {
105 } 122 }
106 } 123 }
107 124
108 createJob (obj: CreateJobArgument) { 125 createJob (obj: CreateJobArgument): void {
126 this.createJobWithPromise(obj)
127 .catch(err => logger.error('Cannot create job.', { err, obj }))
128 }
129
130 createJobWithPromise (obj: CreateJobArgument) {
109 const queue = this.queues[obj.type] 131 const queue = this.queues[obj.type]
110 if (queue === undefined) { 132 if (queue === undefined) {
111 logger.error('Unknown queue %s: cannot create job.', obj.type) 133 logger.error('Unknown queue %s: cannot create job.', obj.type)
112 throw Error('Unknown queue, cannot create job') 134 return
113 } 135 }
114 136
115 const jobArgs: Bull.JobOptions = { 137 const jobArgs: Bull.JobOptions = {
@@ -122,10 +144,10 @@ class JobQueue {
122 } 144 }
123 145
124 async listForApi (options: { 146 async listForApi (options: {
125 state: JobState, 147 state: JobState
126 start: number, 148 start: number
127 count: number, 149 count: number
128 asc?: boolean, 150 asc?: boolean
129 jobType: JobType 151 jobType: JobType
130 }): Promise<Bull.Job[]> { 152 }): Promise<Bull.Job[]> {
131 const { state, start, count, asc, jobType } = options 153 const { state, start, count, asc, jobType } = options
@@ -133,16 +155,14 @@ class JobQueue {
133 155
134 const filteredJobTypes = this.filterJobTypes(jobType) 156 const filteredJobTypes = this.filterJobTypes(jobType)
135 157
136 // TODO: optimize
137 for (const jobType of filteredJobTypes) { 158 for (const jobType of filteredJobTypes) {
138 const queue = this.queues[ jobType ] 159 const queue = this.queues[jobType]
139 if (queue === undefined) { 160 if (queue === undefined) {
140 logger.error('Unknown queue %s to list jobs.', jobType) 161 logger.error('Unknown queue %s to list jobs.', jobType)
141 continue 162 continue
142 } 163 }
143 164
144 // FIXME: Bull queue typings does not have getJobs method 165 const jobs = await queue.getJobs([ state ], 0, start + count, asc)
145 const jobs = await (queue as any).getJobs(state, 0, start + count, asc)
146 results = results.concat(jobs) 166 results = results.concat(jobs)
147 } 167 }
148 168
@@ -164,7 +184,7 @@ class JobQueue {
164 const filteredJobTypes = this.filterJobTypes(jobType) 184 const filteredJobTypes = this.filterJobTypes(jobType)
165 185
166 for (const type of filteredJobTypes) { 186 for (const type of filteredJobTypes) {
167 const queue = this.queues[ type ] 187 const queue = this.queues[type]
168 if (queue === undefined) { 188 if (queue === undefined) {
169 logger.error('Unknown queue %s to count jobs.', type) 189 logger.error('Unknown queue %s to count jobs.', type)
170 continue 190 continue
@@ -172,7 +192,7 @@ class JobQueue {
172 192
173 const counts = await queue.getJobCounts() 193 const counts = await queue.getJobCounts()
174 194
175 total += counts[ state ] 195 total += counts[state]
176 } 196 }
177 197
178 return total 198 return total
@@ -188,7 +208,7 @@ class JobQueue {
188 private addRepeatableJobs () { 208 private addRepeatableJobs () {
189 this.queues['videos-views'].add({}, { 209 this.queues['videos-views'].add({}, {
190 repeat: REPEAT_JOBS['videos-views'] 210 repeat: REPEAT_JOBS['videos-views']
191 }) 211 }).catch(err => logger.error('Cannot add repeatable job.', { err }))
192 } 212 }
193 213
194 private filterJobTypes (jobType?: JobType) { 214 private filterJobTypes (jobType?: JobType) {
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts
index b609f4585..55f7a985d 100644
--- a/server/lib/moderation.ts
+++ b/server/lib/moderation.ts
@@ -15,41 +15,41 @@ export type AcceptResult = {
15 15
16// Can be filtered by plugins 16// Can be filtered by plugins
17function isLocalVideoAccepted (object: { 17function isLocalVideoAccepted (object: {
18 videoBody: VideoCreate, 18 videoBody: VideoCreate
19 videoFile: Express.Multer.File & { duration?: number }, 19 videoFile: Express.Multer.File & { duration?: number }
20 user: UserModel 20 user: UserModel
21}): AcceptResult { 21}): AcceptResult {
22 return { accepted: true } 22 return { accepted: true }
23} 23}
24 24
25function isLocalVideoThreadAccepted (_object: { 25function isLocalVideoThreadAccepted (_object: {
26 commentBody: VideoCommentCreate, 26 commentBody: VideoCommentCreate
27 video: VideoModel, 27 video: VideoModel
28 user: UserModel 28 user: UserModel
29}): AcceptResult { 29}): AcceptResult {
30 return { accepted: true } 30 return { accepted: true }
31} 31}
32 32
33function isLocalVideoCommentReplyAccepted (_object: { 33function isLocalVideoCommentReplyAccepted (_object: {
34 commentBody: VideoCommentCreate, 34 commentBody: VideoCommentCreate
35 parentComment: VideoCommentModel, 35 parentComment: VideoCommentModel
36 video: VideoModel, 36 video: VideoModel
37 user: UserModel 37 user: UserModel
38}): AcceptResult { 38}): AcceptResult {
39 return { accepted: true } 39 return { accepted: true }
40} 40}
41 41
42function isRemoteVideoAccepted (_object: { 42function isRemoteVideoAccepted (_object: {
43 activity: ActivityCreate, 43 activity: ActivityCreate
44 videoAP: VideoTorrentObject, 44 videoAP: VideoTorrentObject
45 byActor: ActorModel 45 byActor: ActorModel
46}): AcceptResult { 46}): AcceptResult {
47 return { accepted: true } 47 return { accepted: true }
48} 48}
49 49
50function isRemoteVideoCommentAccepted (_object: { 50function isRemoteVideoCommentAccepted (_object: {
51 activity: ActivityCreate, 51 activity: ActivityCreate
52 commentAP: VideoCommentObject, 52 commentAP: VideoCommentObject
53 byActor: ActorModel 53 byActor: ActorModel
54}): AcceptResult { 54}): AcceptResult {
55 return { accepted: true } 55 return { accepted: true }
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 679b9bcf6..017739523 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -5,8 +5,7 @@ import { UserNotificationModel } from '../models/account/user-notification'
5import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
6import { PeerTubeSocket } from './peertube-socket' 6import { PeerTubeSocket } from './peertube-socket'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { VideoPrivacy, VideoState } from '../../shared/models/videos' 8import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos'
9import * as Bluebird from 'bluebird'
10import { AccountBlocklistModel } from '../models/account/account-blocklist' 9import { AccountBlocklistModel } from '../models/account/account-blocklist'
11import { 10import {
12 MCommentOwnerVideo, 11 MCommentOwnerVideo,
@@ -17,7 +16,8 @@ import {
17 MVideoFullLight 16 MVideoFullLight
18} from '../typings/models/video' 17} from '../typings/models/video'
19import { 18import {
20 MUser, MUserAccount, 19 MUser,
20 MUserAccount,
21 MUserDefault, 21 MUserDefault,
22 MUserNotifSettingAccount, 22 MUserNotifSettingAccount,
23 MUserWithNotificationSetting, 23 MUserWithNotificationSetting,
@@ -26,20 +26,21 @@ import {
26import { MAccountDefault, MActorFollowFull } from '../typings/models' 26import { MAccountDefault, MActorFollowFull } from '../typings/models'
27import { MVideoImportVideo } from '@server/typings/models/video/video-import' 27import { MVideoImportVideo } from '@server/typings/models/video/video-import'
28import { ServerBlocklistModel } from '@server/models/server/server-blocklist' 28import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
29import { getServerActor } from '@server/helpers/utils' 29import { getServerActor } from '@server/models/application/application'
30 30
31class Notifier { 31class Notifier {
32 32
33 private static instance: Notifier 33 private static instance: Notifier
34 34
35 private constructor () {} 35 private constructor () {
36 }
36 37
37 notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void { 38 notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void {
38 // Only notify on public and published videos which are not blacklisted 39 // Only notify on public and published videos which are not blacklisted
39 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return 40 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return
40 41
41 this.notifySubscribersOfNewVideo(video) 42 this.notifySubscribersOfNewVideo(video)
42 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) 43 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
43 } 44 }
44 45
45 notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void { 46 notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void {
@@ -63,7 +64,9 @@ class Notifier {
63 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return 64 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
64 65
65 this.notifyOwnedVideoHasBeenPublished(video) 66 this.notifyOwnedVideoHasBeenPublished(video)
66 .catch(err => logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })) // tslint:disable-line:max-line-length 67 .catch(err => {
68 logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })
69 })
67 } 70 }
68 71
69 notifyOnNewComment (comment: MCommentOwnerVideo): void { 72 notifyOnNewComment (comment: MCommentOwnerVideo): void {
@@ -74,19 +77,19 @@ class Notifier {
74 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) 77 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
75 } 78 }
76 79
77 notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void { 80 notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
78 this.notifyModeratorsOfNewVideoAbuse(videoAbuse) 81 this.notifyModeratorsOfNewVideoAbuse(parameters)
79 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) 82 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
80 } 83 }
81 84
82 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { 85 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
83 this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist) 86 this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
84 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) 87 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
85 } 88 }
86 89
87 notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { 90 notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
88 this.notifyVideoOwnerOfBlacklist(videoBlacklist) 91 this.notifyVideoOwnerOfBlacklist(videoBlacklist)
89 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) 92 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
90 } 93 }
91 94
92 notifyOnVideoUnblacklist (video: MVideoFullLight): void { 95 notifyOnVideoUnblacklist (video: MVideoFullLight): void {
@@ -96,7 +99,7 @@ class Notifier {
96 99
97 notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void { 100 notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void {
98 this.notifyOwnerVideoImportIsFinished(videoImport, success) 101 this.notifyOwnerVideoImportIsFinished(videoImport, success)
99 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) 102 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
100 } 103 }
101 104
102 notifyOnNewUserRegistration (user: MUserDefault): void { 105 notifyOnNewUserRegistration (user: MUserDefault): void {
@@ -106,14 +109,14 @@ class Notifier {
106 109
107 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { 110 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
108 this.notifyUserOfNewActorFollow(actorFollow) 111 this.notifyUserOfNewActorFollow(actorFollow)
109 .catch(err => { 112 .catch(err => {
110 logger.error( 113 logger.error(
111 'Cannot notify owner of channel %s of a new follow by %s.', 114 'Cannot notify owner of channel %s of a new follow by %s.',
112 actorFollow.ActorFollowing.VideoChannel.getDisplayName(), 115 actorFollow.ActorFollowing.VideoChannel.getDisplayName(),
113 actorFollow.ActorFollower.Account.getDisplayName(), 116 actorFollow.ActorFollower.Account.getDisplayName(),
114 { err } 117 { err }
115 ) 118 )
116 }) 119 })
117 } 120 }
118 121
119 notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { 122 notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
@@ -347,11 +350,15 @@ class Notifier {
347 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) 350 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
348 } 351 }
349 352
350 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) { 353 private async notifyModeratorsOfNewVideoAbuse (parameters: {
354 videoAbuse: VideoAbuse
355 videoAbuseInstance: MVideoAbuseVideo
356 reporter: string
357 }) {
351 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) 358 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
352 if (moderators.length === 0) return 359 if (moderators.length === 0) return
353 360
354 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) 361 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url)
355 362
356 function settingGetter (user: MUserWithNotificationSetting) { 363 function settingGetter (user: MUserWithNotificationSetting) {
357 return user.NotificationSetting.videoAbuseAsModerator 364 return user.NotificationSetting.videoAbuseAsModerator
@@ -361,15 +368,15 @@ class Notifier {
361 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ 368 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
362 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 369 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
363 userId: user.id, 370 userId: user.id,
364 videoAbuseId: videoAbuse.id 371 videoAbuseId: parameters.videoAbuse.id
365 }) 372 })
366 notification.VideoAbuse = videoAbuse 373 notification.VideoAbuse = parameters.videoAbuseInstance
367 374
368 return notification 375 return notification
369 } 376 }
370 377
371 function emailSender (emails: string[]) { 378 function emailSender (emails: string[]) {
372 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse) 379 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
373 } 380 }
374 381
375 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 382 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
@@ -548,10 +555,10 @@ class Notifier {
548 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 555 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
549 } 556 }
550 557
551 private async notify <T extends MUserWithNotificationSetting> (options: { 558 private async notify<T extends MUserWithNotificationSetting> (options: {
552 users: T[], 559 users: T[]
553 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>, 560 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
554 emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, 561 emailSender: (emails: string[]) => void
555 settingGetter: (user: T) => UserNotificationSettingValue 562 settingGetter: (user: T) => UserNotificationSettingValue
556 }) { 563 }) {
557 const emails: string[] = [] 564 const emails: string[] = []
@@ -569,7 +576,7 @@ class Notifier {
569 } 576 }
570 577
571 if (emails.length !== 0) { 578 if (emails.length !== 0) {
572 await options.emailSender(emails) 579 options.emailSender(emails)
573 } 580 }
574 } 581 }
575 582
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index 086856f41..dbcba897a 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -1,4 +1,4 @@
1import * as Bluebird from 'bluebird' 1import * as express from 'express'
2import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4import { UserModel } from '../models/account/user' 4import { UserModel } from '../models/account/user'
@@ -9,6 +9,11 @@ import { Transaction } from 'sequelize'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import * as LRUCache from 'lru-cache' 10import * as LRUCache from 'lru-cache'
11import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' 11import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
12import { MUser } from '@server/typings/models/user/user'
13import { UserAdminFlag } from '@shared/models/users/user-flag.model'
14import { createUserAccountAndChannelAndPlaylist } from './user'
15import { UserRole } from '@shared/models/users/user-role'
16import { PluginManager } from '@server/lib/plugins/plugin-manager'
12 17
13type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 18type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
14 19
@@ -41,22 +46,33 @@ function clearCacheByToken (token: string) {
41 } 46 }
42} 47}
43 48
44function getAccessToken (bearerToken: string) { 49async function getAccessToken (bearerToken: string) {
45 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') 50 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
46 51
47 if (!bearerToken) return Bluebird.resolve(undefined) 52 if (!bearerToken) return undefined
48 53
49 if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken)) 54 let tokenModel: MOAuthTokenUser
50 55
51 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 56 if (accessTokenCache.has(bearerToken)) {
52 .then(tokenModel => { 57 tokenModel = accessTokenCache.get(bearerToken)
53 if (tokenModel) { 58 } else {
54 accessTokenCache.set(bearerToken, tokenModel) 59 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
55 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
56 }
57 60
58 return tokenModel 61 if (tokenModel) {
59 }) 62 accessTokenCache.set(bearerToken, tokenModel)
63 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
64 }
65 }
66
67 if (!tokenModel) return undefined
68
69 if (tokenModel.User.pluginAuth) {
70 const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access')
71
72 if (valid !== true) return undefined
73 }
74
75 return tokenModel
60} 76}
61 77
62function getClient (clientId: string, clientSecret: string) { 78function getClient (clientId: string, clientSecret: string) {
@@ -65,20 +81,52 @@ function getClient (clientId: string, clientSecret: string) {
65 return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) 81 return OAuthClientModel.getByIdAndSecret(clientId, clientSecret)
66} 82}
67 83
68function getRefreshToken (refreshToken: string) { 84async function getRefreshToken (refreshToken: string) {
69 logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') 85 logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').')
70 86
71 return OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) 87 const tokenInfo = await OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken)
88 if (!tokenInfo) return undefined
89
90 const tokenModel = tokenInfo.token
91
92 if (tokenModel.User.pluginAuth) {
93 const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh')
94
95 if (valid !== true) return undefined
96 }
97
98 return tokenInfo
72} 99}
73 100
74async function getUser (usernameOrEmail: string, password: string) { 101async function getUser (usernameOrEmail?: string, password?: string) {
102 const res: express.Response = this.request.res
103
104 // Special treatment coming from a plugin
105 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) {
106 const obj = res.locals.bypassLogin
107 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
108
109 let user = await UserModel.loadByEmail(obj.user.email)
110 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user)
111
112 // If the user does not belongs to a plugin, it was created before its installation
113 // Then we just go through a regular login process
114 if (user.pluginAuth !== null) {
115 // This user does not belong to this plugin, skip it
116 if (user.pluginAuth !== obj.pluginName) return null
117
118 return user
119 }
120 }
121
75 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') 122 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).')
76 123
77 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) 124 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail)
78 if (!user) return null 125 // If we don't find the user, or if the user belongs to a plugin
126 if (!user || user.pluginAuth !== null) return null
79 127
80 const passwordMatch = await user.isPasswordMatch(password) 128 const passwordMatch = await user.isPasswordMatch(password)
81 if (passwordMatch === false) return null 129 if (passwordMatch !== true) return null
82 130
83 if (user.blocked) throw new AccessDeniedError('User is blocked.') 131 if (user.blocked) throw new AccessDeniedError('User is blocked.')
84 132
@@ -89,29 +137,36 @@ async function getUser (usernameOrEmail: string, password: string) {
89 return user 137 return user
90} 138}
91 139
92async function revokeToken (tokenInfo: TokenInfo) { 140async function revokeToken (tokenInfo: { refreshToken: string }) {
141 const res: express.Response = this.request.res
93 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 142 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
143
94 if (token) { 144 if (token) {
145 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) {
146 PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User)
147 }
148
95 clearCacheByToken(token.accessToken) 149 clearCacheByToken(token.accessToken)
96 150
97 token.destroy() 151 token.destroy()
98 .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) 152 .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
153
154 return true
99 } 155 }
100 156
101 /* 157 return false
102 * Thanks to https://github.com/manjeshpv/node-oauth2-server-implementation/blob/master/components/oauth/mongo-models.js
103 * "As per the discussion we need set older date
104 * revokeToken will expected return a boolean in future version
105 * https://github.com/oauthjs/node-oauth2-server/pull/274
106 * https://github.com/oauthjs/node-oauth2-server/issues/290"
107 */
108 const expiredToken = token
109 expiredToken.refreshTokenExpiresAt = new Date('2015-05-28T06:59:53.000Z')
110
111 return expiredToken
112} 158}
113 159
114async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 160async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
161 const res: express.Response = this.request.res
162
163 let authName: string = null
164 if (res.locals.bypassLogin?.bypass === true) {
165 authName = res.locals.bypassLogin.authName
166 } else if (res.locals.refreshTokenAuthName) {
167 authName = res.locals.refreshTokenAuthName
168 }
169
115 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') 170 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
116 171
117 const tokenToCreate = { 172 const tokenToCreate = {
@@ -119,11 +174,16 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
119 accessTokenExpiresAt: token.accessTokenExpiresAt, 174 accessTokenExpiresAt: token.accessTokenExpiresAt,
120 refreshToken: token.refreshToken, 175 refreshToken: token.refreshToken,
121 refreshTokenExpiresAt: token.refreshTokenExpiresAt, 176 refreshTokenExpiresAt: token.refreshTokenExpiresAt,
177 authName,
122 oAuthClientId: client.id, 178 oAuthClientId: client.id,
123 userId: user.id 179 userId: user.id
124 } 180 }
125 181
126 const tokenCreated = await OAuthTokenModel.create(tokenToCreate) 182 const tokenCreated = await OAuthTokenModel.create(tokenToCreate)
183
184 user.lastLoginDate = new Date()
185 await user.save()
186
127 return Object.assign(tokenCreated, { client, user }) 187 return Object.assign(tokenCreated, { client, user })
128} 188}
129 189
@@ -141,3 +201,30 @@ export {
141 revokeToken, 201 revokeToken,
142 saveToken 202 saveToken
143} 203}
204
205async function createUserFromExternal (pluginAuth: string, options: {
206 username: string
207 email: string
208 role: UserRole
209 displayName: string
210}) {
211 const userToCreate = new UserModel({
212 username: options.username,
213 password: null,
214 email: options.email,
215 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
216 autoPlayVideo: true,
217 role: options.role,
218 videoQuota: CONFIG.USER.VIDEO_QUOTA,
219 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
220 adminFlags: UserAdminFlag.NONE,
221 pluginAuth
222 }) as MUser
223
224 const { user } = await createUserAccountAndChannelAndPlaylist({
225 userToCreate,
226 userDisplayName: options.displayName
227 })
228
229 return user
230}
diff --git a/server/lib/plugins/hooks.ts b/server/lib/plugins/hooks.ts
index bcc8c674e..aa92f03cc 100644
--- a/server/lib/plugins/hooks.ts
+++ b/server/lib/plugins/hooks.ts
@@ -25,7 +25,7 @@ const Hooks = {
25 }, 25 },
26 26
27 runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => { 27 runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => {
28 PluginManager.Instance.runHook(hookName, params) 28 PluginManager.Instance.runHook(hookName, undefined, params)
29 .catch(err => logger.error('Fatal hook error.', { err })) 29 .catch(err => logger.error('Fatal hook error.', { err }))
30 } 30 }
31} 31}
diff --git a/server/lib/plugins/plugin-helpers.ts b/server/lib/plugins/plugin-helpers.ts
new file mode 100644
index 000000000..de82b4918
--- /dev/null
+++ b/server/lib/plugins/plugin-helpers.ts
@@ -0,0 +1,133 @@
1import { PeerTubeHelpers } from '@server/typings/plugins'
2import { sequelizeTypescript } from '@server/initializers/database'
3import { buildLogger } from '@server/helpers/logger'
4import { VideoModel } from '@server/models/video/video'
5import { WEBSERVER } from '@server/initializers/constants'
6import { ServerModel } from '@server/models/server/server'
7import { getServerActor } from '@server/models/application/application'
8import { addServerInBlocklist, removeServerFromBlocklist, addAccountInBlocklist, removeAccountFromBlocklist } from '../blocklist'
9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
10import { AccountModel } from '@server/models/account/account'
11import { VideoBlacklistCreate } from '@shared/models'
12import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
13import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
14import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
15
16function buildPluginHelpers (npmName: string): PeerTubeHelpers {
17 const logger = buildPluginLogger(npmName)
18
19 const database = buildDatabaseHelpers()
20 const videos = buildVideosHelpers()
21
22 const config = buildConfigHelpers()
23
24 const server = buildServerHelpers()
25
26 const moderation = buildModerationHelpers()
27
28 return {
29 logger,
30 database,
31 videos,
32 config,
33 moderation,
34 server
35 }
36}
37
38export {
39 buildPluginHelpers
40}
41
42// ---------------------------------------------------------------------------
43
44function buildPluginLogger (npmName: string) {
45 return buildLogger(npmName)
46}
47
48function buildDatabaseHelpers () {
49 return {
50 query: sequelizeTypescript.query.bind(sequelizeTypescript)
51 }
52}
53
54function buildServerHelpers () {
55 return {
56 getServerActor: () => getServerActor()
57 }
58}
59
60function buildVideosHelpers () {
61 return {
62 loadByUrl: (url: string) => {
63 return VideoModel.loadByUrl(url)
64 },
65
66 removeVideo: (id: number) => {
67 return sequelizeTypescript.transaction(async t => {
68 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id, t)
69
70 await video.destroy({ transaction: t })
71 })
72 }
73 }
74}
75
76function buildModerationHelpers () {
77 return {
78 blockServer: async (options: { byAccountId: number, hostToBlock: string }) => {
79 const serverToBlock = await ServerModel.loadOrCreateByHost(options.hostToBlock)
80
81 await addServerInBlocklist(options.byAccountId, serverToBlock.id)
82 },
83
84 unblockServer: async (options: { byAccountId: number, hostToUnblock: string }) => {
85 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(options.byAccountId, options.hostToUnblock)
86 if (!serverBlock) return
87
88 await removeServerFromBlocklist(serverBlock)
89 },
90
91 blockAccount: async (options: { byAccountId: number, handleToBlock: string }) => {
92 const accountToBlock = await AccountModel.loadByNameWithHost(options.handleToBlock)
93 if (!accountToBlock) return
94
95 await addAccountInBlocklist(options.byAccountId, accountToBlock.id)
96 },
97
98 unblockAccount: async (options: { byAccountId: number, handleToUnblock: string }) => {
99 const targetAccount = await AccountModel.loadByNameWithHost(options.handleToUnblock)
100 if (!targetAccount) return
101
102 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(options.byAccountId, targetAccount.id)
103 if (!accountBlock) return
104
105 await removeAccountFromBlocklist(accountBlock)
106 },
107
108 blacklistVideo: async (options: { videoIdOrUUID: number | string, createOptions: VideoBlacklistCreate }) => {
109 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID)
110 if (!video) return
111
112 await blacklistVideo(video, options.createOptions)
113 },
114
115 unblacklistVideo: async (options: { videoIdOrUUID: number | string }) => {
116 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID)
117 if (!video) return
118
119 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(video.id)
120 if (!videoBlacklist) return
121
122 await unblacklistVideo(videoBlacklist, video)
123 }
124 }
125}
126
127function buildConfigHelpers () {
128 return {
129 getWebserverUrl () {
130 return WEBSERVER.URL
131 }
132 }
133}
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts
index 25b4f3c61..170f0c7e2 100644
--- a/server/lib/plugins/plugin-index.ts
+++ b/server/lib/plugins/plugin-index.ts
@@ -27,11 +27,11 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
27 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' 27 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins'
28 28
29 try { 29 try {
30 const { body } = await doRequest({ uri, qs, json: true }) 30 const { body } = await doRequest<any>({ uri, qs, json: true })
31 31
32 logger.debug('Got result from PeerTube index.', { body }) 32 logger.debug('Got result from PeerTube index.', { body })
33 33
34 await addInstanceInformation(body) 34 addInstanceInformation(body)
35 35
36 return body as ResultList<PeerTubePluginIndex> 36 return body as ResultList<PeerTubePluginIndex>
37 } catch (err) { 37 } catch (err) {
@@ -40,7 +40,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
40 } 40 }
41} 41}
42 42
43async function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) { 43function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) {
44 for (const d of result.data) { 44 for (const d of result.data) {
45 d.installed = PluginManager.Instance.isRegistered(d.npmName) 45 d.installed = PluginManager.Instance.isRegistered(d.npmName)
46 d.name = PluginModel.normalizePluginName(d.npmName) 46 d.name = PluginModel.normalizePluginName(d.npmName)
@@ -57,7 +57,7 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu
57 57
58 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version' 58 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version'
59 59
60 const { body } = await doRequest({ uri, body: bodyRequest, json: true, method: 'POST' }) 60 const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' })
61 61
62 return body 62 return body
63} 63}
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 7ebdabd34..950acf7ad 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -9,23 +9,20 @@ import {
9 PluginTranslationPaths as PackagePluginTranslations 9 PluginTranslationPaths as PackagePluginTranslations
10} from '../../../shared/models/plugins/plugin-package-json.model' 10} from '../../../shared/models/plugins/plugin-package-json.model'
11import { createReadStream, createWriteStream } from 'fs' 11import { createReadStream, createWriteStream } from 'fs'
12import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' 12import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
13import { PluginType } from '../../../shared/models/plugins/plugin.type' 13import { PluginType } from '../../../shared/models/plugins/plugin.type'
14import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' 14import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
15import { outputFile, readJSON } from 'fs-extra' 15import { outputFile, readJSON } from 'fs-extra'
16import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' 16import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model'
17import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model'
18import { ServerHook, ServerHookName, serverHookObject } from '../../../shared/models/plugins/server-hook.model'
19import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' 17import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
20import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model' 18import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model'
21import { PluginLibrary } from '../../typings/plugins' 19import { PluginLibrary } from '../../typings/plugins'
22import { ClientHtml } from '../client-html' 20import { ClientHtml } from '../client-html'
23import { RegisterServerHookOptions } from '../../../shared/models/plugins/register-server-hook.model'
24import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
25import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
26import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model'
27import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
28import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' 21import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
22import { RegisterHelpersStore } from './register-helpers-store'
23import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
24import { MOAuthTokenUser, MUser } from '@server/typings/models'
25import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
29 26
30export interface RegisteredPlugin { 27export interface RegisteredPlugin {
31 npmName: string 28 npmName: string
@@ -44,6 +41,7 @@ export interface RegisteredPlugin {
44 css: string[] 41 css: string[]
45 42
46 // Only if this is a plugin 43 // Only if this is a plugin
44 registerHelpersStore?: RegisterHelpersStore
47 unregister?: Function 45 unregister?: Function
48} 46}
49 47
@@ -54,35 +52,18 @@ export interface HookInformationValue {
54 priority: number 52 priority: number
55} 53}
56 54
57type AlterableVideoConstant = 'language' | 'licence' | 'category'
58type VideoConstant = { [ key in number | string ]: string }
59type UpdatedVideoConstant = {
60 [ name in AlterableVideoConstant ]: {
61 [ npmName: string ]: {
62 added: { key: number | string, label: string }[],
63 deleted: { key: number | string, label: string }[]
64 }
65 }
66}
67
68type PluginLocalesTranslations = { 55type PluginLocalesTranslations = {
69 [ locale: string ]: PluginTranslation 56 [locale: string]: PluginTranslation
70} 57}
71 58
72export class PluginManager implements ServerHook { 59export class PluginManager implements ServerHook {
73 60
74 private static instance: PluginManager 61 private static instance: PluginManager
75 62
76 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} 63 private registeredPlugins: { [name: string]: RegisteredPlugin } = {}
77 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
78 private hooks: { [ name: string ]: HookInformationValue[] } = {}
79 private translations: PluginLocalesTranslations = {}
80 64
81 private updatedVideoConstants: UpdatedVideoConstant = { 65 private hooks: { [name: string]: HookInformationValue[] } = {}
82 language: {}, 66 private translations: PluginLocalesTranslations = {}
83 licence: {},
84 category: {}
85 }
86 67
87 private constructor () { 68 private constructor () {
88 } 69 }
@@ -97,7 +78,7 @@ export class PluginManager implements ServerHook {
97 return this.registeredPlugins[npmName] 78 return this.registeredPlugins[npmName]
98 } 79 }
99 80
100 getRegisteredPlugin (name: string) { 81 getRegisteredPluginByShortName (name: string) {
101 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) 82 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN)
102 const registered = this.getRegisteredPluginOrTheme(npmName) 83 const registered = this.getRegisteredPluginOrTheme(npmName)
103 84
@@ -106,7 +87,7 @@ export class PluginManager implements ServerHook {
106 return registered 87 return registered
107 } 88 }
108 89
109 getRegisteredTheme (name: string) { 90 getRegisteredThemeByShortName (name: string) {
110 const npmName = PluginModel.buildNpmName(name, PluginType.THEME) 91 const npmName = PluginModel.buildNpmName(name, PluginType.THEME)
111 const registered = this.getRegisteredPluginOrTheme(npmName) 92 const registered = this.getRegisteredPluginOrTheme(npmName)
112 93
@@ -123,17 +104,102 @@ export class PluginManager implements ServerHook {
123 return this.getRegisteredPluginsOrThemes(PluginType.THEME) 104 return this.getRegisteredPluginsOrThemes(PluginType.THEME)
124 } 105 }
125 106
107 getIdAndPassAuths () {
108 return this.getRegisteredPlugins()
109 .map(p => ({
110 npmName: p.npmName,
111 name: p.name,
112 version: p.version,
113 idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths()
114 }))
115 .filter(v => v.idAndPassAuths.length !== 0)
116 }
117
118 getExternalAuths () {
119 return this.getRegisteredPlugins()
120 .map(p => ({
121 npmName: p.npmName,
122 name: p.name,
123 version: p.version,
124 externalAuths: p.registerHelpersStore.getExternalAuths()
125 }))
126 .filter(v => v.externalAuths.length !== 0)
127 }
128
126 getRegisteredSettings (npmName: string) { 129 getRegisteredSettings (npmName: string) {
127 return this.settings[npmName] || [] 130 const result = this.getRegisteredPluginOrTheme(npmName)
131 if (!result || result.type !== PluginType.PLUGIN) return []
132
133 return result.registerHelpersStore.getSettings()
134 }
135
136 getRouter (npmName: string) {
137 const result = this.getRegisteredPluginOrTheme(npmName)
138 if (!result || result.type !== PluginType.PLUGIN) return null
139
140 return result.registerHelpersStore.getRouter()
128 } 141 }
129 142
130 getTranslations (locale: string) { 143 getTranslations (locale: string) {
131 return this.translations[locale] || {} 144 return this.translations[locale] || {}
132 } 145 }
133 146
147 async isTokenValid (token: MOAuthTokenUser, type: 'access' | 'refresh') {
148 const auth = this.getAuth(token.User.pluginAuth, token.authName)
149 if (!auth) return true
150
151 if (auth.hookTokenValidity) {
152 try {
153 const { valid } = await auth.hookTokenValidity({ token, type })
154
155 if (valid === false) {
156 logger.info('Rejecting %s token validity from auth %s of plugin %s', type, token.authName, token.User.pluginAuth)
157 }
158
159 return valid
160 } catch (err) {
161 logger.warn('Cannot run check token validity from auth %s of plugin %s.', token.authName, token.User.pluginAuth, { err })
162 return true
163 }
164 }
165
166 return true
167 }
168
169 // ###################### External events ######################
170
171 onLogout (npmName: string, authName: string, user: MUser) {
172 const auth = this.getAuth(npmName, authName)
173
174 if (auth?.onLogout) {
175 logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName)
176
177 try {
178 auth.onLogout(user)
179 } catch (err) {
180 logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err })
181 }
182 }
183 }
184
185 onSettingsChanged (name: string, settings: any) {
186 const registered = this.getRegisteredPluginByShortName(name)
187 if (!registered) {
188 logger.error('Cannot find plugin %s to call on settings changed.', name)
189 }
190
191 for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) {
192 try {
193 cb(settings)
194 } catch (err) {
195 logger.error('Cannot run on settings changed callback for %s.', registered.npmName, { err })
196 }
197 }
198 }
199
134 // ###################### Hooks ###################### 200 // ###################### Hooks ######################
135 201
136 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 202 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
137 if (!this.hooks[hookName]) return Promise.resolve(result) 203 if (!this.hooks[hookName]) return Promise.resolve(result)
138 204
139 const hookType = getHookType(hookName) 205 const hookType = getHookType(hookName)
@@ -185,7 +251,6 @@ export class PluginManager implements ServerHook {
185 } 251 }
186 252
187 delete this.registeredPlugins[plugin.npmName] 253 delete this.registeredPlugins[plugin.npmName]
188 delete this.settings[plugin.npmName]
189 254
190 this.deleteTranslations(plugin.npmName) 255 this.deleteTranslations(plugin.npmName)
191 256
@@ -197,7 +262,8 @@ export class PluginManager implements ServerHook {
197 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) 262 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
198 } 263 }
199 264
200 this.reinitVideoConstants(plugin.npmName) 265 const store = plugin.registerHelpersStore
266 store.reinitVideoConstants(plugin.npmName)
201 267
202 logger.info('Regenerating registered plugin CSS to global file.') 268 logger.info('Regenerating registered plugin CSS to global file.')
203 await this.regeneratePluginGlobalCSS() 269 await this.regeneratePluginGlobalCSS()
@@ -303,8 +369,11 @@ export class PluginManager implements ServerHook {
303 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) 369 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
304 370
305 let library: PluginLibrary 371 let library: PluginLibrary
372 let registerHelpersStore: RegisterHelpersStore
306 if (plugin.type === PluginType.PLUGIN) { 373 if (plugin.type === PluginType.PLUGIN) {
307 library = await this.registerPlugin(plugin, pluginPath, packageJSON) 374 const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
375 library = result.library
376 registerHelpersStore = result.registerStore
308 } 377 }
309 378
310 const clientScripts: { [id: string]: ClientScript } = {} 379 const clientScripts: { [id: string]: ClientScript } = {}
@@ -312,7 +381,7 @@ export class PluginManager implements ServerHook {
312 clientScripts[c.script] = c 381 clientScripts[c.script] = c
313 } 382 }
314 383
315 this.registeredPlugins[ npmName ] = { 384 this.registeredPlugins[npmName] = {
316 npmName, 385 npmName,
317 name: plugin.name, 386 name: plugin.name,
318 type: plugin.type, 387 type: plugin.type,
@@ -323,6 +392,7 @@ export class PluginManager implements ServerHook {
323 staticDirs: packageJSON.staticDirs, 392 staticDirs: packageJSON.staticDirs,
324 clientScripts, 393 clientScripts,
325 css: packageJSON.css, 394 css: packageJSON.css,
395 registerHelpersStore: registerHelpersStore || undefined,
326 unregister: library ? library.unregister : undefined 396 unregister: library ? library.unregister : undefined
327 } 397 }
328 398
@@ -341,15 +411,15 @@ export class PluginManager implements ServerHook {
341 throw new Error('Library code is not valid (miss register or unregister function)') 411 throw new Error('Library code is not valid (miss register or unregister function)')
342 } 412 }
343 413
344 const registerHelpers = this.getRegisterHelpers(npmName, plugin) 414 const { registerOptions, registerStore } = this.getRegisterHelpers(npmName, plugin)
345 library.register(registerHelpers) 415 library.register(registerOptions)
346 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) 416 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err }))
347 417
348 logger.info('Add plugin %s CSS to global file.', npmName) 418 logger.info('Add plugin %s CSS to global file.', npmName)
349 419
350 await this.addCSSToGlobalFile(pluginPath, packageJSON.css) 420 await this.addCSSToGlobalFile(pluginPath, packageJSON.css)
351 421
352 return library 422 return { library, registerStore }
353 } 423 }
354 424
355 // ###################### Translations ###################### 425 // ###################### Translations ######################
@@ -432,13 +502,23 @@ export class PluginManager implements ServerHook {
432 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) 502 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName)
433 } 503 }
434 504
505 private getAuth (npmName: string, authName: string) {
506 const plugin = this.getRegisteredPluginOrTheme(npmName)
507 if (!plugin || plugin.type !== PluginType.PLUGIN) return null
508
509 let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
510 auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
511
512 return auths.find(a => a.authName === authName)
513 }
514
435 // ###################### Private getters ###################### 515 // ###################### Private getters ######################
436 516
437 private getRegisteredPluginsOrThemes (type: PluginType) { 517 private getRegisteredPluginsOrThemes (type: PluginType) {
438 const plugins: RegisteredPlugin[] = [] 518 const plugins: RegisteredPlugin[] = []
439 519
440 for (const npmName of Object.keys(this.registeredPlugins)) { 520 for (const npmName of Object.keys(this.registeredPlugins)) {
441 const plugin = this.registeredPlugins[ npmName ] 521 const plugin = this.registeredPlugins[npmName]
442 if (plugin.type !== type) continue 522 if (plugin.type !== type) continue
443 523
444 plugins.push(plugin) 524 plugins.push(plugin)
@@ -449,149 +529,26 @@ export class PluginManager implements ServerHook {
449 529
450 // ###################### Generate register helpers ###################### 530 // ###################### Generate register helpers ######################
451 531
452 private getRegisterHelpers (npmName: string, plugin: PluginModel): RegisterServerOptions { 532 private getRegisterHelpers (
453 const registerHook = (options: RegisterServerHookOptions) => { 533 npmName: string,
454 if (serverHookObject[options.target] !== true) { 534 plugin: PluginModel
455 logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, npmName) 535 ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } {
456 return 536 const onHookAdded = (options: RegisterServerHookOptions) => {
457 }
458
459 if (!this.hooks[options.target]) this.hooks[options.target] = [] 537 if (!this.hooks[options.target]) this.hooks[options.target] = []
460 538
461 this.hooks[options.target].push({ 539 this.hooks[options.target].push({
462 npmName, 540 npmName: npmName,
463 pluginName: plugin.name, 541 pluginName: plugin.name,
464 handler: options.handler, 542 handler: options.handler,
465 priority: options.priority || 0 543 priority: options.priority || 0
466 }) 544 })
467 } 545 }
468 546
469 const registerSetting = (options: RegisterServerSettingOptions) => { 547 const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this))
470 if (!this.settings[npmName]) this.settings[npmName] = []
471
472 this.settings[npmName].push(options)
473 }
474
475 const settingsManager: PluginSettingsManager = {
476 getSetting: (name: string) => PluginModel.getSetting(plugin.name, plugin.type, name),
477
478 setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, plugin.type, name, value)
479 }
480
481 const storageManager: PluginStorageManager = {
482 getData: (key: string) => PluginModel.getData(plugin.name, plugin.type, key),
483
484 storeData: (key: string, data: any) => PluginModel.storeData(plugin.name, plugin.type, key, data)
485 }
486
487 const videoLanguageManager: PluginVideoLanguageManager = {
488 addLanguage: (key: string, label: string) => this.addConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }),
489
490 deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
491 }
492
493 const videoCategoryManager: PluginVideoCategoryManager = {
494 addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }),
495
496 deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
497 }
498
499 const videoLicenceManager: PluginVideoLicenceManager = {
500 addLicence: (key: number, label: string) => this.addConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key, label }),
501
502 deleteLicence: (key: number) => this.deleteConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key })
503 }
504
505 const peertubeHelpers = {
506 logger
507 }
508 548
509 return { 549 return {
510 registerHook, 550 registerStore: registerHelpersStore,
511 registerSetting, 551 registerOptions: registerHelpersStore.buildRegisterHelpers()
512 settingsManager,
513 storageManager,
514 videoLanguageManager,
515 videoCategoryManager,
516 videoLicenceManager,
517 peertubeHelpers
518 }
519 }
520
521 private addConstant <T extends string | number> (parameters: {
522 npmName: string,
523 type: AlterableVideoConstant,
524 obj: VideoConstant,
525 key: T,
526 label: string
527 }) {
528 const { npmName, type, obj, key, label } = parameters
529
530 if (obj[key]) {
531 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
532 return false
533 }
534
535 if (!this.updatedVideoConstants[type][npmName]) {
536 this.updatedVideoConstants[type][npmName] = {
537 added: [],
538 deleted: []
539 }
540 }
541
542 this.updatedVideoConstants[type][npmName].added.push({ key, label })
543 obj[key] = label
544
545 return true
546 }
547
548 private deleteConstant <T extends string | number> (parameters: {
549 npmName: string,
550 type: AlterableVideoConstant,
551 obj: VideoConstant,
552 key: T
553 }) {
554 const { npmName, type, obj, key } = parameters
555
556 if (!obj[key]) {
557 logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
558 return false
559 }
560
561 if (!this.updatedVideoConstants[type][npmName]) {
562 this.updatedVideoConstants[type][npmName] = {
563 added: [],
564 deleted: []
565 }
566 }
567
568 this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
569 delete obj[key]
570
571 return true
572 }
573
574 private reinitVideoConstants (npmName: string) {
575 const hash = {
576 language: VIDEO_LANGUAGES,
577 licence: VIDEO_LICENCES,
578 category: VIDEO_CATEGORIES
579 }
580 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category' ]
581
582 for (const type of types) {
583 const updatedConstants = this.updatedVideoConstants[type][npmName]
584 if (!updatedConstants) continue
585
586 for (const added of updatedConstants.added) {
587 delete hash[type][added.key]
588 }
589
590 for (const deleted of updatedConstants.deleted) {
591 hash[type][deleted.key] = deleted.label
592 }
593
594 delete this.updatedVideoConstants[type][npmName]
595 } 552 }
596 } 553 }
597 554
@@ -604,7 +561,7 @@ export class PluginManager implements ServerHook {
604 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType) 561 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType)
605 if (!packageJSONValid) { 562 if (!packageJSONValid) {
606 const formattedFields = badFields.map(f => `"${f}"`) 563 const formattedFields = badFields.map(f => `"${f}"`)
607 .join(', ') 564 .join(', ')
608 565
609 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`) 566 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`)
610 } 567 }
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts
new file mode 100644
index 000000000..e337b1cb0
--- /dev/null
+++ b/server/lib/plugins/register-helpers-store.ts
@@ -0,0 +1,355 @@
1import * as express from 'express'
2import { logger } from '@server/helpers/logger'
3import {
4 VIDEO_CATEGORIES,
5 VIDEO_LANGUAGES,
6 VIDEO_LICENCES,
7 VIDEO_PLAYLIST_PRIVACIES,
8 VIDEO_PRIVACIES
9} from '@server/initializers/constants'
10import { onExternalUserAuthenticated } from '@server/lib/auth'
11import { PluginModel } from '@server/models/server/plugin'
12import { RegisterServerOptions } from '@server/typings/plugins'
13import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
14import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
15import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model'
16import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
17import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
18import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
19import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
20import {
21 RegisterServerAuthExternalOptions,
22 RegisterServerAuthExternalResult,
23 RegisterServerAuthPassOptions,
24 RegisterServerExternalAuthenticatedResult
25} from '@shared/models/plugins/register-server-auth.model'
26import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
27import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
28import { serverHookObject } from '@shared/models/plugins/server-hook.model'
29import { buildPluginHelpers } from './plugin-helpers'
30
31type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
32type VideoConstant = { [key in number | string]: string }
33
34type UpdatedVideoConstant = {
35 [name in AlterableVideoConstant]: {
36 added: { key: number | string, label: string }[]
37 deleted: { key: number | string, label: string }[]
38 }
39}
40
41export class RegisterHelpersStore {
42 private readonly updatedVideoConstants: UpdatedVideoConstant = {
43 playlistPrivacy: { added: [], deleted: [] },
44 privacy: { added: [], deleted: [] },
45 language: { added: [], deleted: [] },
46 licence: { added: [], deleted: [] },
47 category: { added: [], deleted: [] }
48 }
49
50 private readonly settings: RegisterServerSettingOptions[] = []
51
52 private idAndPassAuths: RegisterServerAuthPassOptions[] = []
53 private externalAuths: RegisterServerAuthExternalOptions[] = []
54
55 private readonly onSettingsChangeCallbacks: ((settings: any) => void)[] = []
56
57 private readonly router: express.Router
58
59 constructor (
60 private readonly npmName: string,
61 private readonly plugin: PluginModel,
62 private readonly onHookAdded: (options: RegisterServerHookOptions) => void
63 ) {
64 this.router = express.Router()
65 }
66
67 buildRegisterHelpers (): RegisterServerOptions {
68 const registerHook = this.buildRegisterHook()
69 const registerSetting = this.buildRegisterSetting()
70
71 const getRouter = this.buildGetRouter()
72
73 const settingsManager = this.buildSettingsManager()
74 const storageManager = this.buildStorageManager()
75
76 const videoLanguageManager = this.buildVideoLanguageManager()
77
78 const videoLicenceManager = this.buildVideoLicenceManager()
79 const videoCategoryManager = this.buildVideoCategoryManager()
80
81 const videoPrivacyManager = this.buildVideoPrivacyManager()
82 const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
83
84 const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
85 const registerExternalAuth = this.buildRegisterExternalAuth()
86 const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
87 const unregisterExternalAuth = this.buildUnregisterExternalAuth()
88
89 const peertubeHelpers = buildPluginHelpers(this.npmName)
90
91 return {
92 registerHook,
93 registerSetting,
94
95 getRouter,
96
97 settingsManager,
98 storageManager,
99
100 videoLanguageManager,
101 videoCategoryManager,
102 videoLicenceManager,
103
104 videoPrivacyManager,
105 playlistPrivacyManager,
106
107 registerIdAndPassAuth,
108 registerExternalAuth,
109 unregisterIdAndPassAuth,
110 unregisterExternalAuth,
111
112 peertubeHelpers
113 }
114 }
115
116 reinitVideoConstants (npmName: string) {
117 const hash = {
118 language: VIDEO_LANGUAGES,
119 licence: VIDEO_LICENCES,
120 category: VIDEO_CATEGORIES,
121 privacy: VIDEO_PRIVACIES,
122 playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
123 }
124 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
125
126 for (const type of types) {
127 const updatedConstants = this.updatedVideoConstants[type][npmName]
128 if (!updatedConstants) continue
129
130 for (const added of updatedConstants.added) {
131 delete hash[type][added.key]
132 }
133
134 for (const deleted of updatedConstants.deleted) {
135 hash[type][deleted.key] = deleted.label
136 }
137
138 delete this.updatedVideoConstants[type][npmName]
139 }
140 }
141
142 getSettings () {
143 return this.settings
144 }
145
146 getRouter () {
147 return this.router
148 }
149
150 getIdAndPassAuths () {
151 return this.idAndPassAuths
152 }
153
154 getExternalAuths () {
155 return this.externalAuths
156 }
157
158 getOnSettingsChangedCallbacks () {
159 return this.onSettingsChangeCallbacks
160 }
161
162 private buildGetRouter () {
163 return () => this.router
164 }
165
166 private buildRegisterSetting () {
167 return (options: RegisterServerSettingOptions) => {
168 this.settings.push(options)
169 }
170 }
171
172 private buildRegisterHook () {
173 return (options: RegisterServerHookOptions) => {
174 if (serverHookObject[options.target] !== true) {
175 logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, this.npmName)
176 return
177 }
178
179 return this.onHookAdded(options)
180 }
181 }
182
183 private buildRegisterIdAndPassAuth () {
184 return (options: RegisterServerAuthPassOptions) => {
185 if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') {
186 logger.error('Cannot register auth plugin %s: authName, getWeight or login are not valid.', this.npmName, { options })
187 return
188 }
189
190 this.idAndPassAuths.push(options)
191 }
192 }
193
194 private buildRegisterExternalAuth () {
195 const self = this
196
197 return (options: RegisterServerAuthExternalOptions) => {
198 if (!options.authName || typeof options.authDisplayName !== 'function' || typeof options.onAuthRequest !== 'function') {
199 logger.error('Cannot register auth plugin %s: authName, authDisplayName or onAuthRequest are not valid.', this.npmName, { options })
200 return
201 }
202
203 this.externalAuths.push(options)
204
205 return {
206 userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void {
207 onExternalUserAuthenticated({
208 npmName: self.npmName,
209 authName: options.authName,
210 authResult: result
211 }).catch(err => {
212 logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err })
213 })
214 }
215 } as RegisterServerAuthExternalResult
216 }
217 }
218
219 private buildUnregisterExternalAuth () {
220 return (authName: string) => {
221 this.externalAuths = this.externalAuths.filter(a => a.authName !== authName)
222 }
223 }
224
225 private buildUnregisterIdAndPassAuth () {
226 return (authName: string) => {
227 this.idAndPassAuths = this.idAndPassAuths.filter(a => a.authName !== authName)
228 }
229 }
230
231 private buildSettingsManager (): PluginSettingsManager {
232 return {
233 getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name, this.settings),
234
235 getSettings: (names: string[]) => PluginModel.getSettings(this.plugin.name, this.plugin.type, names, this.settings),
236
237 setSetting: (name: string, value: string) => PluginModel.setSetting(this.plugin.name, this.plugin.type, name, value),
238
239 onSettingsChange: (cb: (settings: any) => void) => this.onSettingsChangeCallbacks.push(cb)
240 }
241 }
242
243 private buildStorageManager (): PluginStorageManager {
244 return {
245 getData: (key: string) => PluginModel.getData(this.plugin.name, this.plugin.type, key),
246
247 storeData: (key: string, data: any) => PluginModel.storeData(this.plugin.name, this.plugin.type, key, data)
248 }
249 }
250
251 private buildVideoLanguageManager (): PluginVideoLanguageManager {
252 return {
253 addLanguage: (key: string, label: string) => {
254 return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label })
255 },
256
257 deleteLanguage: (key: string) => {
258 return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
259 }
260 }
261 }
262
263 private buildVideoCategoryManager (): PluginVideoCategoryManager {
264 return {
265 addCategory: (key: number, label: string) => {
266 return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
267 },
268
269 deleteCategory: (key: number) => {
270 return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
271 }
272 }
273 }
274
275 private buildVideoPrivacyManager (): PluginVideoPrivacyManager {
276 return {
277 deletePrivacy: (key: number) => {
278 return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key })
279 }
280 }
281 }
282
283 private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager {
284 return {
285 deletePlaylistPrivacy: (key: number) => {
286 return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key })
287 }
288 }
289 }
290
291 private buildVideoLicenceManager (): PluginVideoLicenceManager {
292 return {
293 addLicence: (key: number, label: string) => {
294 return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
295 },
296
297 deleteLicence: (key: number) => {
298 return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key })
299 }
300 }
301 }
302
303 private addConstant<T extends string | number> (parameters: {
304 npmName: string
305 type: AlterableVideoConstant
306 obj: VideoConstant
307 key: T
308 label: string
309 }) {
310 const { npmName, type, obj, key, label } = parameters
311
312 if (obj[key]) {
313 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
314 return false
315 }
316
317 if (!this.updatedVideoConstants[type][npmName]) {
318 this.updatedVideoConstants[type][npmName] = {
319 added: [],
320 deleted: []
321 }
322 }
323
324 this.updatedVideoConstants[type][npmName].added.push({ key, label })
325 obj[key] = label
326
327 return true
328 }
329
330 private deleteConstant<T extends string | number> (parameters: {
331 npmName: string
332 type: AlterableVideoConstant
333 obj: VideoConstant
334 key: T
335 }) {
336 const { npmName, type, obj, key } = parameters
337
338 if (!obj[key]) {
339 logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
340 return false
341 }
342
343 if (!this.updatedVideoConstants[type][npmName]) {
344 this.updatedVideoConstants[type][npmName] = {
345 added: [],
346 deleted: []
347 }
348 }
349
350 this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
351 delete obj[key]
352
353 return true
354 }
355}
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index f77d0b62c..b4cd6f8e7 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -6,13 +6,14 @@ import {
6 CONTACT_FORM_LIFETIME, 6 CONTACT_FORM_LIFETIME,
7 USER_EMAIL_VERIFY_LIFETIME, 7 USER_EMAIL_VERIFY_LIFETIME,
8 USER_PASSWORD_RESET_LIFETIME, 8 USER_PASSWORD_RESET_LIFETIME,
9 USER_PASSWORD_CREATE_LIFETIME,
9 VIDEO_VIEW_LIFETIME, 10 VIDEO_VIEW_LIFETIME,
10 WEBSERVER 11 WEBSERVER
11} from '../initializers/constants' 12} from '../initializers/constants'
12import { CONFIG } from '../initializers/config' 13import { CONFIG } from '../initializers/config'
13 14
14type CachedRoute = { 15type CachedRoute = {
15 body: string, 16 body: string
16 contentType?: string 17 contentType?: string
17 statusCode?: string 18 statusCode?: string
18} 19}
@@ -24,7 +25,8 @@ class Redis {
24 private client: RedisClient 25 private client: RedisClient
25 private prefix: string 26 private prefix: string
26 27
27 private constructor () {} 28 private constructor () {
29 }
28 30
29 init () { 31 init () {
30 // Already initialized 32 // Already initialized
@@ -49,9 +51,9 @@ class Redis {
49 return Object.assign({}, 51 return Object.assign({},
50 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {}, 52 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
51 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {}, 53 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
52 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT) ? 54 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT)
53 { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT } : 55 ? { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT }
54 { path: CONFIG.REDIS.SOCKET } 56 : { path: CONFIG.REDIS.SOCKET }
55 ) 57 )
56 } 58 }
57 59
@@ -63,7 +65,7 @@ class Redis {
63 return this.prefix 65 return this.prefix
64 } 66 }
65 67
66 /************* Forgot password *************/ 68 /* ************ Forgot password ************ */
67 69
68 async setResetPasswordVerificationString (userId: number) { 70 async setResetPasswordVerificationString (userId: number) {
69 const generatedString = await generateRandomString(32) 71 const generatedString = await generateRandomString(32)
@@ -73,11 +75,19 @@ class Redis {
73 return generatedString 75 return generatedString
74 } 76 }
75 77
78 async setCreatePasswordVerificationString (userId: number) {
79 const generatedString = await generateRandomString(32)
80
81 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME)
82
83 return generatedString
84 }
85
76 async getResetPasswordLink (userId: number) { 86 async getResetPasswordLink (userId: number) {
77 return this.getValue(this.generateResetPasswordKey(userId)) 87 return this.getValue(this.generateResetPasswordKey(userId))
78 } 88 }
79 89
80 /************* Email verification *************/ 90 /* ************ Email verification ************ */
81 91
82 async setVerifyEmailVerificationString (userId: number) { 92 async setVerifyEmailVerificationString (userId: number) {
83 const generatedString = await generateRandomString(32) 93 const generatedString = await generateRandomString(32)
@@ -91,7 +101,7 @@ class Redis {
91 return this.getValue(this.generateVerifyEmailKey(userId)) 101 return this.getValue(this.generateVerifyEmailKey(userId))
92 } 102 }
93 103
94 /************* Contact form per IP *************/ 104 /* ************ Contact form per IP ************ */
95 105
96 async setContactFormIp (ip: string) { 106 async setContactFormIp (ip: string) {
97 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME) 107 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
@@ -101,7 +111,7 @@ class Redis {
101 return this.exists(this.generateContactFormKey(ip)) 111 return this.exists(this.generateContactFormKey(ip))
102 } 112 }
103 113
104 /************* Views per IP *************/ 114 /* ************ Views per IP ************ */
105 115
106 setIPVideoView (ip: string, videoUUID: string) { 116 setIPVideoView (ip: string, videoUUID: string) {
107 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) 117 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
@@ -111,7 +121,7 @@ class Redis {
111 return this.exists(this.generateViewKey(ip, videoUUID)) 121 return this.exists(this.generateViewKey(ip, videoUUID))
112 } 122 }
113 123
114 /************* API cache *************/ 124 /* ************ API cache ************ */
115 125
116 async getCachedRoute (req: express.Request) { 126 async getCachedRoute (req: express.Request) {
117 const cached = await this.getObject(this.generateCachedRouteKey(req)) 127 const cached = await this.getObject(this.generateCachedRouteKey(req))
@@ -120,17 +130,17 @@ class Redis {
120 } 130 }
121 131
122 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) { 132 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
123 const cached: CachedRoute = Object.assign({}, { 133 const cached: CachedRoute = Object.assign(
124 body: body.toString() 134 {},
125 }, 135 { body: body.toString() },
126 (contentType) ? { contentType } : null, 136 (contentType) ? { contentType } : null,
127 (statusCode) ? { statusCode: statusCode.toString() } : null 137 (statusCode) ? { statusCode: statusCode.toString() } : null
128 ) 138 )
129 139
130 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime) 140 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
131 } 141 }
132 142
133 /************* Video views *************/ 143 /* ************ Video views ************ */
134 144
135 addVideoView (videoId: number) { 145 addVideoView (videoId: number) {
136 const keyIncr = this.generateVideoViewKey(videoId) 146 const keyIncr = this.generateVideoViewKey(videoId)
@@ -173,7 +183,7 @@ class Redis {
173 ]) 183 ])
174 } 184 }
175 185
176 /************* Keys generation *************/ 186 /* ************ Keys generation ************ */
177 187
178 generateCachedRouteKey (req: express.Request) { 188 generateCachedRouteKey (req: express.Request) {
179 return req.method + '-' + req.originalUrl 189 return req.method + '-' + req.originalUrl
@@ -207,7 +217,7 @@ class Redis {
207 return 'contact-form-' + ip 217 return 'contact-form-' + ip
208 } 218 }
209 219
210 /************* Redis helpers *************/ 220 /* ************ Redis helpers ************ */
211 221
212 private getValue (key: string) { 222 private getValue (key: string) {
213 return new Promise<string>((res, rej) => { 223 return new Promise<string>((res, rej) => {
@@ -265,7 +275,7 @@ class Redis {
265 }) 275 })
266 } 276 }
267 277
268 private setObject (key: string, obj: { [ id: string ]: string }, expirationMilliseconds: number) { 278 private setObject (key: string, obj: { [id: string]: string }, expirationMilliseconds: number) {
269 return new Promise<void>((res, rej) => { 279 return new Promise<void>((res, rej) => {
270 this.client.hmset(this.prefix + key, obj, (err, ok) => { 280 this.client.hmset(this.prefix + key, obj, (err, ok) => {
271 if (err) return rej(err) 281 if (err) return rej(err)
@@ -282,7 +292,7 @@ class Redis {
282 } 292 }
283 293
284 private getObject (key: string) { 294 private getObject (key: string) {
285 return new Promise<{ [ id: string ]: string }>((res, rej) => { 295 return new Promise<{ [id: string]: string }>((res, rej) => {
286 this.client.hgetall(this.prefix + key, (err, value) => { 296 this.client.hgetall(this.prefix + key, (err, value) => {
287 if (err) return rej(err) 297 if (err) return rej(err)
288 298
diff --git a/server/lib/redundancy.ts b/server/lib/redundancy.ts
index 1b4ecd7c0..361b401a5 100644
--- a/server/lib/redundancy.ts
+++ b/server/lib/redundancy.ts
@@ -1,8 +1,12 @@
1import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 1import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
2import { sendUndoCacheFile } from './activitypub/send' 2import { sendUndoCacheFile } from './activitypub/send'
3import { Transaction } from 'sequelize' 3import { Transaction } from 'sequelize'
4import { getServerActor } from '../helpers/utils' 4import { MActorSignature, MVideoRedundancyVideo } from '@server/typings/models'
5import { MVideoRedundancyVideo } from '@server/typings/models' 5import { CONFIG } from '@server/initializers/config'
6import { logger } from '@server/helpers/logger'
7import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
8import { Activity } from '@shared/models'
9import { getServerActor } from '@server/models/application/application'
6 10
7async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) { 11async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) {
8 const serverActor = await getServerActor() 12 const serverActor = await getServerActor()
@@ -13,17 +17,38 @@ async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?
13 await videoRedundancy.destroy({ transaction: t }) 17 await videoRedundancy.destroy({ transaction: t })
14} 18}
15 19
16async function removeRedundancyOf (serverId: number) { 20async function removeRedundanciesOfServer (serverId: number) {
17 const videosRedundancy = await VideoRedundancyModel.listLocalOfServer(serverId) 21 const redundancies = await VideoRedundancyModel.listLocalOfServer(serverId)
18 22
19 for (const redundancy of videosRedundancy) { 23 for (const redundancy of redundancies) {
20 await removeVideoRedundancy(redundancy) 24 await removeVideoRedundancy(redundancy)
21 } 25 }
22} 26}
23 27
28async function isRedundancyAccepted (activity: Activity, byActor: MActorSignature) {
29 const configAcceptFrom = CONFIG.REMOTE_REDUNDANCY.VIDEOS.ACCEPT_FROM
30 if (configAcceptFrom === 'nobody') {
31 logger.info('Do not accept remote redundancy %s due instance accept policy.', activity.id)
32 return false
33 }
34
35 if (configAcceptFrom === 'followings') {
36 const serverActor = await getServerActor()
37 const allowed = await ActorFollowModel.isFollowedBy(byActor.id, serverActor.id)
38
39 if (allowed !== true) {
40 logger.info('Do not accept remote redundancy %s because actor %s is not followed by our instance.', activity.id, byActor.url)
41 return false
42 }
43 }
44
45 return true
46}
47
24// --------------------------------------------------------------------------- 48// ---------------------------------------------------------------------------
25 49
26export { 50export {
27 removeRedundancyOf, 51 isRedundancyAccepted,
52 removeRedundanciesOfServer,
28 removeVideoRedundancy 53 removeVideoRedundancy
29} 54}
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts
index dd326bc1e..a57436a45 100644
--- a/server/lib/schedulers/auto-follow-index-instances.ts
+++ b/server/lib/schedulers/auto-follow-index-instances.ts
@@ -6,7 +6,7 @@ import { chunk } from 'lodash'
6import { doRequest } from '@server/helpers/requests' 6import { doRequest } from '@server/helpers/requests'
7import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 7import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
8import { JobQueue } from '@server/lib/job-queue' 8import { JobQueue } from '@server/lib/job-queue'
9import { getServerActor } from '@server/helpers/utils' 9import { getServerActor } from '@server/models/application/application'
10 10
11export class AutoFollowIndexInstances extends AbstractScheduler { 11export class AutoFollowIndexInstances extends AbstractScheduler {
12 12
@@ -41,7 +41,11 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
41 41
42 this.lastCheck = new Date() 42 this.lastCheck = new Date()
43 43
44 const { body } = await doRequest({ uri, qs, json: true }) 44 const { body } = await doRequest<any>({ uri, qs, json: true })
45 if (!body.data || Array.isArray(body.data) === false) {
46 logger.error('Cannot auto follow instances of index %s: bad URL format. Please check the auto follow URL.', indexUrl)
47 return
48 }
45 49
46 const hosts: string[] = body.data.map(o => o.host) 50 const hosts: string[] = body.data.map(o => o.host)
47 const chunks = chunk(hosts, 20) 51 const chunks = chunk(hosts, 20)
@@ -57,8 +61,7 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
57 isAutoFollow: true 61 isAutoFollow: true
58 } 62 }
59 63
60 await JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 64 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
61 .catch(err => logger.error('Cannot create follow job for %s.', unfollowedHost, err))
62 } 65 }
63 } 66 }
64 67
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts
index 7ff41e639..014993e94 100644
--- a/server/lib/schedulers/plugins-check-scheduler.ts
+++ b/server/lib/schedulers/plugins-check-scheduler.ts
@@ -43,7 +43,7 @@ export class PluginsCheckScheduler extends AbstractScheduler {
43 const results = await getLatestPluginsVersion(npmNames) 43 const results = await getLatestPluginsVersion(npmNames)
44 44
45 for (const result of results) { 45 for (const result of results) {
46 const plugin = pluginIndex[ result.npmName ] 46 const plugin = pluginIndex[result.npmName]
47 if (!result.latestVersion) continue 47 if (!result.latestVersion) continue
48 48
49 if ( 49 if (
diff --git a/server/lib/schedulers/remove-old-views-scheduler.ts b/server/lib/schedulers/remove-old-views-scheduler.ts
index 39fbb9163..5ae87fe50 100644
--- a/server/lib/schedulers/remove-old-views-scheduler.ts
+++ b/server/lib/schedulers/remove-old-views-scheduler.ts
@@ -1,9 +1,7 @@
1import { logger } from '../../helpers/logger' 1import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler' 2import { AbstractScheduler } from './abstract-scheduler'
3import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' 3import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
4import { UserVideoHistoryModel } from '../../models/account/user-video-history'
5import { CONFIG } from '../../initializers/config' 4import { CONFIG } from '../../initializers/config'
6import { isTestInstance } from '../../helpers/core-utils'
7import { VideoViewModel } from '../../models/video/video-views' 5import { VideoViewModel } from '../../models/video/video-views'
8 6
9export class RemoveOldViewsScheduler extends AbstractScheduler { 7export class RemoveOldViewsScheduler extends AbstractScheduler {
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index 350a335d3..d32c1c068 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -2,9 +2,8 @@ import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler' 2import { AbstractScheduler } from './abstract-scheduler'
3import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' 3import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update'
4import { retryTransactionWrapper } from '../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../helpers/database-utils'
5import { federateVideoIfNeeded } from '../activitypub' 5import { federateVideoIfNeeded } from '../activitypub/videos'
6import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' 6import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
7import { VideoPrivacy } from '../../../shared/models/videos'
8import { Notifier } from '../notifier' 7import { Notifier } from '../notifier'
9import { sequelizeTypescript } from '../../initializers/database' 8import { sequelizeTypescript } from '../../initializers/database'
10import { MVideoFullLight } from '@server/typings/models' 9import { MVideoFullLight } from '@server/typings/models'
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index c1c91b656..8da9d52b5 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -1,16 +1,15 @@
1import { AbstractScheduler } from './abstract-scheduler' 1import { AbstractScheduler } from './abstract-scheduler'
2import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants' 2import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { VideosRedundancy } from '../../../shared/models/redundancy' 4import { VideosRedundancyStrategy } from '../../../shared/models/redundancy'
5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
6import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' 6import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent'
7import { join } from 'path' 7import { join } from 'path'
8import { move } from 'fs-extra' 8import { move } from 'fs-extra'
9import { getServerActor } from '../../helpers/utils'
10import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' 9import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
11import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' 10import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
12import { removeVideoRedundancy } from '../redundancy' 11import { removeVideoRedundancy } from '../redundancy'
13import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' 12import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos'
14import { downloadPlaylistSegments } from '../hls' 13import { downloadPlaylistSegments } from '../hls'
15import { CONFIG } from '../../initializers/config' 14import { CONFIG } from '../../initializers/config'
16import { 15import {
@@ -25,11 +24,13 @@ import {
25 MVideoWithAllFiles 24 MVideoWithAllFiles
26} from '@server/typings/models' 25} from '@server/typings/models'
27import { getVideoFilename } from '../video-paths' 26import { getVideoFilename } from '../video-paths'
27import { VideoModel } from '@server/models/video/video'
28import { getServerActor } from '@server/models/application/application'
28 29
29type CandidateToDuplicate = { 30type CandidateToDuplicate = {
30 redundancy: VideosRedundancy, 31 redundancy: VideosRedundancyStrategy
31 video: MVideoWithAllFiles, 32 video: MVideoWithAllFiles
32 files: MVideoFile[], 33 files: MVideoFile[]
33 streamingPlaylists: MStreamingPlaylistFiles[] 34 streamingPlaylists: MStreamingPlaylistFiles[]
34} 35}
35 36
@@ -41,7 +42,7 @@ function isMVideoRedundancyFileVideo (
41 42
42export class VideosRedundancyScheduler extends AbstractScheduler { 43export class VideosRedundancyScheduler extends AbstractScheduler {
43 44
44 private static instance: AbstractScheduler 45 private static instance: VideosRedundancyScheduler
45 46
46 protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL 47 protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL
47 48
@@ -49,6 +50,22 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
49 super() 50 super()
50 } 51 }
51 52
53 async createManualRedundancy (videoId: number) {
54 const videoToDuplicate = await VideoModel.loadWithFiles(videoId)
55
56 if (!videoToDuplicate) {
57 logger.warn('Video to manually duplicate %d does not exist anymore.', videoId)
58 return
59 }
60
61 return this.createVideoRedundancies({
62 video: videoToDuplicate,
63 redundancy: null,
64 files: videoToDuplicate.VideoFiles,
65 streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists
66 })
67 }
68
52 protected async internalExecute () { 69 protected async internalExecute () {
53 for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { 70 for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) {
54 logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) 71 logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy)
@@ -94,7 +111,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
94 for (const redundancyModel of expired) { 111 for (const redundancyModel of expired) {
95 try { 112 try {
96 const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) 113 const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy)
97 const candidate = { 114 const candidate: CandidateToDuplicate = {
98 redundancy: redundancyConfig, 115 redundancy: redundancyConfig,
99 video: null, 116 video: null,
100 files: [], 117 files: [],
@@ -140,7 +157,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
140 } 157 }
141 } 158 }
142 159
143 private findVideoToDuplicate (cache: VideosRedundancy) { 160 private findVideoToDuplicate (cache: VideosRedundancyStrategy) {
144 if (cache.strategy === 'most-views') { 161 if (cache.strategy === 'most-views') {
145 return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR) 162 return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR)
146 } 163 }
@@ -187,13 +204,21 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
187 } 204 }
188 } 205 }
189 206
190 private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: MVideoAccountLight, fileArg: MVideoFile) { 207 private async createVideoFileRedundancy (redundancy: VideosRedundancyStrategy | null, video: MVideoAccountLight, fileArg: MVideoFile) {
208 let strategy = 'manual'
209 let expiresOn: Date = null
210
211 if (redundancy) {
212 strategy = redundancy.strategy
213 expiresOn = this.buildNewExpiration(redundancy.minLifetime)
214 }
215
191 const file = fileArg as MVideoFileVideo 216 const file = fileArg as MVideoFileVideo
192 file.Video = video 217 file.Video = video
193 218
194 const serverActor = await getServerActor() 219 const serverActor = await getServerActor()
195 220
196 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) 221 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy)
197 222
198 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 223 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
199 const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) 224 const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs)
@@ -204,10 +229,10 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
204 await move(tmpPath, destPath, { overwrite: true }) 229 await move(tmpPath, destPath, { overwrite: true })
205 230
206 const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ 231 const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
207 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 232 expiresOn,
208 url: getVideoCacheFileActivityPubUrl(file), 233 url: getVideoCacheFileActivityPubUrl(file),
209 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), 234 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL),
210 strategy: redundancy.strategy, 235 strategy,
211 videoFileId: file.id, 236 videoFileId: file.id,
212 actorId: serverActor.id 237 actorId: serverActor.id
213 }) 238 })
@@ -220,25 +245,33 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
220 } 245 }
221 246
222 private async createStreamingPlaylistRedundancy ( 247 private async createStreamingPlaylistRedundancy (
223 redundancy: VideosRedundancy, 248 redundancy: VideosRedundancyStrategy,
224 video: MVideoAccountLight, 249 video: MVideoAccountLight,
225 playlistArg: MStreamingPlaylist 250 playlistArg: MStreamingPlaylist
226 ) { 251 ) {
252 let strategy = 'manual'
253 let expiresOn: Date = null
254
255 if (redundancy) {
256 strategy = redundancy.strategy
257 expiresOn = this.buildNewExpiration(redundancy.minLifetime)
258 }
259
227 const playlist = playlistArg as MStreamingPlaylistVideo 260 const playlist = playlistArg as MStreamingPlaylistVideo
228 playlist.Video = video 261 playlist.Video = video
229 262
230 const serverActor = await getServerActor() 263 const serverActor = await getServerActor()
231 264
232 logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy) 265 logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy)
233 266
234 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) 267 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid)
235 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) 268 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
236 269
237 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ 270 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
238 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 271 expiresOn,
239 url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), 272 url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist),
240 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), 273 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL),
241 strategy: redundancy.strategy, 274 strategy,
242 videoStreamingPlaylistId: playlist.id, 275 videoStreamingPlaylistId: playlist.id,
243 actorId: serverActor.id 276 actorId: serverActor.id
244 }) 277 })
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index a99f71629..8dbd41771 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -69,7 +69,7 @@ function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile,
69function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) { 69function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) {
70 const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 70 const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
71 71
72 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() 72 const thumbnail = existingThumbnail || new ThumbnailModel()
73 73
74 thumbnail.filename = filename 74 thumbnail.filename = filename
75 thumbnail.height = height 75 thumbnail.height = height
@@ -142,18 +142,18 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
142} 142}
143 143
144async function createThumbnailFromFunction (parameters: { 144async function createThumbnailFromFunction (parameters: {
145 thumbnailCreator: () => Promise<any>, 145 thumbnailCreator: () => Promise<any>
146 filename: string, 146 filename: string
147 height: number, 147 height: number
148 width: number, 148 width: number
149 type: ThumbnailType, 149 type: ThumbnailType
150 automaticallyGenerated?: boolean, 150 automaticallyGenerated?: boolean
151 fileUrl?: string, 151 fileUrl?: string
152 existingThumbnail?: MThumbnail 152 existingThumbnail?: MThumbnail
153}) { 153}) {
154 const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters 154 const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters
155 155
156 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() 156 const thumbnail = existingThumbnail || new ThumbnailModel()
157 157
158 thumbnail.filename = filename 158 thumbnail.filename = filename
159 thumbnail.height = height 159 thumbnail.height = height
diff --git a/server/lib/user.ts b/server/lib/user.ts
index c45438d95..8b447583e 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -1,8 +1,8 @@
1import * as uuidv4 from 'uuid/v4' 1import { v4 as uuidv4 } from 'uuid'
2import { ActivityPubActorType } from '../../shared/models/activitypub' 2import { ActivityPubActorType } from '../../shared/models/activitypub'
3import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' 3import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
4import { AccountModel } from '../models/account/account' 4import { AccountModel } from '../models/account/account'
5import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' 5import { buildActorInstance, setAsyncActorKeys } from './activitypub/actor'
6import { createLocalVideoChannel } from './video-channel' 6import { createLocalVideoChannel } from './video-channel'
7import { ActorModel } from '../models/activitypub/actor' 7import { ActorModel } from '../models/activitypub/actor'
8import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 8import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
@@ -14,13 +14,14 @@ import { Redis } from './redis'
14import { Emailer } from './emailer' 14import { Emailer } from './emailer'
15import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models' 15import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models'
16import { MUser, MUserDefault, MUserId } from '../typings/models/user' 16import { MUser, MUserDefault, MUserId } from '../typings/models/user'
17import { getAccountActivityPubUrl } from './activitypub/url'
17 18
18type ChannelNames = { name: string, displayName: string } 19type ChannelNames = { name: string, displayName: string }
19 20
20async function createUserAccountAndChannelAndPlaylist (parameters: { 21async function createUserAccountAndChannelAndPlaylist (parameters: {
21 userToCreate: MUser, 22 userToCreate: MUser
22 userDisplayName?: string, 23 userDisplayName?: string
23 channelNames?: ChannelNames, 24 channelNames?: ChannelNames
24 validateUser?: boolean 25 validateUser?: boolean
25}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> { 26}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> {
26 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters 27 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
@@ -63,11 +64,11 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
63} 64}
64 65
65async function createLocalAccountWithoutKeys (parameters: { 66async function createLocalAccountWithoutKeys (parameters: {
66 name: string, 67 name: string
67 displayName?: string, 68 displayName?: string
68 userId: number | null, 69 userId: number | null
69 applicationId: number | null, 70 applicationId: number | null
70 t: Transaction | undefined, 71 t: Transaction | undefined
71 type?: ActivityPubActorType 72 type?: ActivityPubActorType
72}) { 73}) {
73 const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters 74 const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index 1dd45b76d..bd60c6201 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -1,23 +1,33 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { sequelizeTypescript } from '@server/initializers/database'
3import {
4 MUser,
5 MVideoAccountLight,
6 MVideoBlacklist,
7 MVideoBlacklistVideo,
8 MVideoFullLight,
9 MVideoWithBlacklistLight
10} from '@server/typings/models'
11import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models'
12import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
13import { logger } from '../helpers/logger'
2import { CONFIG } from '../initializers/config' 14import { CONFIG } from '../initializers/config'
3import { UserRight, VideoBlacklistType } from '../../shared/models'
4import { VideoBlacklistModel } from '../models/video/video-blacklist' 15import { VideoBlacklistModel } from '../models/video/video-blacklist'
5import { logger } from '../helpers/logger' 16import { sendDeleteVideo } from './activitypub/send'
6import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 17import { federateVideoIfNeeded } from './activitypub/videos'
7import { Hooks } from './plugins/hooks'
8import { Notifier } from './notifier' 18import { Notifier } from './notifier'
9import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models' 19import { Hooks } from './plugins/hooks'
10 20
11async function autoBlacklistVideoIfNeeded (parameters: { 21async function autoBlacklistVideoIfNeeded (parameters: {
12 video: MVideoWithBlacklistLight, 22 video: MVideoWithBlacklistLight
13 user?: MUser, 23 user?: MUser
14 isRemote: boolean, 24 isRemote: boolean
15 isNew: boolean, 25 isNew: boolean
16 notify?: boolean, 26 notify?: boolean
17 transaction?: Transaction 27 transaction?: Transaction
18}) { 28}) {
19 const { video, user, isRemote, isNew, notify = true, transaction } = parameters 29 const { video, user, isRemote, isNew, notify = true, transaction } = parameters
20 const doAutoBlacklist = await Hooks.wrapPromiseFun( 30 const doAutoBlacklist = await Hooks.wrapFun(
21 autoBlacklistNeeded, 31 autoBlacklistNeeded,
22 { video, user, isRemote, isNew }, 32 { video, user, isRemote, isNew },
23 'filter:video.auto-blacklist.result' 33 'filter:video.auto-blacklist.result'
@@ -49,10 +59,64 @@ async function autoBlacklistVideoIfNeeded (parameters: {
49 return true 59 return true
50} 60}
51 61
52async function autoBlacklistNeeded (parameters: { 62async function blacklistVideo (videoInstance: MVideoAccountLight, options: VideoBlacklistCreate) {
53 video: MVideoWithBlacklistLight, 63 const blacklist: MVideoBlacklistVideo = await VideoBlacklistModel.create({
54 isRemote: boolean, 64 videoId: videoInstance.id,
55 isNew: boolean, 65 unfederated: options.unfederate === true,
66 reason: options.reason,
67 type: VideoBlacklistType.MANUAL
68 }
69 )
70 blacklist.Video = videoInstance
71
72 if (options.unfederate === true) {
73 await sendDeleteVideo(videoInstance, undefined)
74 }
75
76 Notifier.Instance.notifyOnVideoBlacklist(blacklist)
77}
78
79async function unblacklistVideo (videoBlacklist: MVideoBlacklist, video: MVideoFullLight) {
80 const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
81 const unfederated = videoBlacklist.unfederated
82 const videoBlacklistType = videoBlacklist.type
83
84 await videoBlacklist.destroy({ transaction: t })
85 video.VideoBlacklist = undefined
86
87 // Re federate the video
88 if (unfederated === true) {
89 await federateVideoIfNeeded(video, true, t)
90 }
91
92 return videoBlacklistType
93 })
94
95 Notifier.Instance.notifyOnVideoUnblacklist(video)
96
97 if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) {
98 Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video)
99
100 // Delete on object so new video notifications will send
101 delete video.VideoBlacklist
102 Notifier.Instance.notifyOnNewVideoIfNeeded(video)
103 }
104}
105
106// ---------------------------------------------------------------------------
107
108export {
109 autoBlacklistVideoIfNeeded,
110 blacklistVideo,
111 unblacklistVideo
112}
113
114// ---------------------------------------------------------------------------
115
116function autoBlacklistNeeded (parameters: {
117 video: MVideoWithBlacklistLight
118 isRemote: boolean
119 isNew: boolean
56 user?: MUser 120 user?: MUser
57}) { 121}) {
58 const { user, video, isRemote, isNew } = parameters 122 const { user, video, isRemote, isNew } = parameters
@@ -66,9 +130,3 @@ async function autoBlacklistNeeded (parameters: {
66 130
67 return true 131 return true
68} 132}
69
70// ---------------------------------------------------------------------------
71
72export {
73 autoBlacklistVideoIfNeeded
74}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 41eab456b..102c1088d 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -1,13 +1,14 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4' 2import { v4 as uuidv4 } from 'uuid'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { VideoChannelModel } from '../models/video/video-channel' 4import { VideoChannelModel } from '../models/video/video-channel'
5import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub' 5import { buildActorInstance } from './activitypub/actor'
6import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
7import { MAccountId, MChannelDefault, MChannelId } from '../typings/models' 7import { MAccountId, MChannelDefault, MChannelId } from '../typings/models'
8import { getVideoChannelActivityPubUrl } from './activitypub/url'
9import { federateVideoIfNeeded } from './activitypub/videos'
8 10
9type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & 11type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T }
10 { Account?: T }
11 12
12async function createLocalVideoChannel <T extends MAccountId> ( 13async function createLocalVideoChannel <T extends MAccountId> (
13 videoChannelInfo: VideoChannelCreate, 14 videoChannelInfo: VideoChannelCreate,
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index b8074e6d2..516c912a9 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -2,14 +2,14 @@ import * as Sequelize from 'sequelize'
2import { ResultList } from '../../shared/models' 2import { ResultList } from '../../shared/models'
3import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' 3import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
4import { VideoCommentModel } from '../models/video/video-comment' 4import { VideoCommentModel } from '../models/video/video-comment'
5import { getVideoCommentActivityPubUrl } from './activitypub' 5import { getVideoCommentActivityPubUrl } from './activitypub/url'
6import { sendCreateVideoComment } from './activitypub/send' 6import { sendCreateVideoComment } from './activitypub/send'
7import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models' 7import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models'
8 8
9async function createVideoComment (obj: { 9async function createVideoComment (obj: {
10 text: string, 10 text: string
11 inReplyToComment: MComment | null, 11 inReplyToComment: MComment | null
12 video: MVideoFullLight, 12 video: MVideoFullLight
13 account: MAccountDefault 13 account: MAccountDefault
14}, t: Sequelize.Transaction) { 14}, t: Sequelize.Transaction) {
15 let originCommentId: number | null = null 15 let originCommentId: number | null = null
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts
index fe0a004e4..05aaca8af 100644
--- a/server/lib/video-paths.ts
+++ b/server/lib/video-paths.ts
@@ -1,8 +1,8 @@
1import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/typings/models' 1import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/typings/models'
2import { extractVideo } from './videos'
3import { join } from 'path' 2import { join } from 'path'
4import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
5import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants' 4import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants'
5import { extractVideo } from '@server/helpers/video'
6 6
7// ################## Video file name ################## 7// ################## Video file name ##################
8 8
diff --git a/server/lib/video-playlist.ts b/server/lib/video-playlist.ts
index 29b70cfda..75fbd6896 100644
--- a/server/lib/video-playlist.ts
+++ b/server/lib/video-playlist.ts
@@ -1,7 +1,7 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { VideoPlaylistModel } from '../models/video/video-playlist' 2import { VideoPlaylistModel } from '../models/video/video-playlist'
3import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' 3import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
4import { getVideoPlaylistActivityPubUrl } from './activitypub' 4import { getVideoPlaylistActivityPubUrl } from './activitypub/url'
5import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' 5import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
6import { MAccount } from '../typings/models' 6import { MAccount } from '../typings/models'
7import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist' 7import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist'
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 0d5b3ae39..dcda82e0a 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -3,6 +3,7 @@ import { basename, extname as extnameUtil, join } from 'path'
3import { 3import {
4 canDoQuickTranscode, 4 canDoQuickTranscode,
5 getDurationFromVideoFile, 5 getDurationFromVideoFile,
6 getMetadataFromFile,
6 getVideoFileFPS, 7 getVideoFileFPS,
7 transcode, 8 transcode,
8 TranscodeOptions, 9 TranscodeOptions,
@@ -202,10 +203,11 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso
202 203
203 newVideoFile.size = stats.size 204 newVideoFile.size = stats.size
204 newVideoFile.fps = await getVideoFileFPS(videoFilePath) 205 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
206 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
205 207
206 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) 208 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
207 209
208 await newVideoFile.save() 210 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
209 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') 211 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
210 212
211 video.setHLSPlaylist(videoStreamingPlaylist) 213 video.setHLSPlaylist(videoStreamingPlaylist)
@@ -230,11 +232,13 @@ export {
230async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) { 232async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
231 const stats = await stat(transcodingPath) 233 const stats = await stat(transcodingPath)
232 const fps = await getVideoFileFPS(transcodingPath) 234 const fps = await getVideoFileFPS(transcodingPath)
235 const metadata = await getMetadataFromFile(transcodingPath)
233 236
234 await move(transcodingPath, outputPath) 237 await move(transcodingPath, outputPath)
235 238
236 videoFile.size = stats.size 239 videoFile.size = stats.size
237 videoFile.fps = fps 240 videoFile.fps = fps
241 videoFile.metadata = metadata
238 242
239 await createTorrentAndSetInfoHash(video, videoFile) 243 await createTorrentAndSetInfoHash(video, videoFile)
240 244
diff --git a/server/lib/videos.ts b/server/lib/videos.ts
deleted file mode 100644
index 22e9afbf9..000000000
--- a/server/lib/videos.ts
+++ /dev/null
@@ -1,11 +0,0 @@
1import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
2
3function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
4 return isStreamingPlaylist(videoOrPlaylist)
5 ? videoOrPlaylist.Video
6 : videoOrPlaylist
7}
8
9export {
10 extractVideo
11}