aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/actor.ts51
-rw-r--r--server/lib/activitypub/audience.ts8
-rw-r--r--server/lib/activitypub/cache-file.ts4
-rw-r--r--server/lib/activitypub/crawl.ts6
-rw-r--r--server/lib/activitypub/follow.ts1
-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.ts13
-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.ts5
-rw-r--r--server/lib/activitypub/send/send-view.ts6
-rw-r--r--server/lib/activitypub/send/utils.ts40
-rw-r--r--server/lib/activitypub/video-comments.ts14
-rw-r--r--server/lib/activitypub/video-rates.ts2
-rw-r--r--server/lib/activitypub/videos.ts196
-rw-r--r--server/lib/client-html.ts24
-rw-r--r--server/lib/emailer.ts123
-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-http-broadcast.ts2
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts2
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts8
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts2
-rw-r--r--server/lib/job-queue/handlers/video-import.ts2
-rw-r--r--server/lib/job-queue/handlers/video-redundancy.ts20
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts17
-rw-r--r--server/lib/job-queue/handlers/video-views.ts4
-rw-r--r--server/lib/job-queue/job-queue.ts49
-rw-r--r--server/lib/moderation.ts22
-rw-r--r--server/lib/notifier.ts47
-rw-r--r--server/lib/plugins/plugin-index.ts4
-rw-r--r--server/lib/plugins/plugin-manager.ts44
-rw-r--r--server/lib/redis.ts50
-rw-r--r--server/lib/redundancy.ts8
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts3
-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.ts1
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts63
-rw-r--r--server/lib/thumbnail.ts18
-rw-r--r--server/lib/user.ts16
-rw-r--r--server/lib/video-blacklist.ts20
-rw-r--r--server/lib/video-channel.ts3
-rw-r--r--server/lib/video-comment.ts6
49 files changed, 547 insertions, 406 deletions
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 0b21de0ca..9eabef4b0 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,6 +1,6 @@
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 * as uuidv4 from 'uuid/v4'
5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
@@ -33,9 +33,9 @@ import {
33 MActorFull, 33 MActorFull,
34 MActorFullActor, 34 MActorFullActor,
35 MActorId, 35 MActorId,
36 MChannel, 36 MChannel
37 MChannelAccountDefault
38} from '../../typings/models' 37} from '../../typings/models'
38import { extname } from 'path'
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) {
@@ -121,13 +121,13 @@ async function getOrCreateActorAndServerAndModel (
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
@@ -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..9933ae2b5 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}
@@ -48,7 +48,7 @@ function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[
48 } 48 }
49} 49}
50 50
51async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) { 51async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t) 52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
53 53
54 const videoAll = video as VideoModel 54 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..a1c95504e 100644
--- a/server/lib/activitypub/follow.ts
+++ b/server/lib/activitypub/follow.ts
@@ -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/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..8bdcf6417 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -16,6 +16,7 @@ import {
16 MVideoRedundancyFileVideo, 16 MVideoRedundancyFileVideo,
17 MVideoRedundancyStreamingPlaylistVideo 17 MVideoRedundancyStreamingPlaylistVideo
18} from '../../../typings/models' 18} from '../../../typings/models'
19import { ContextType } from '@server/helpers/activitypub'
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
@@ -130,11 +132,12 @@ export {
130// --------------------------------------------------------------------------- 132// ---------------------------------------------------------------------------
131 133
132async function sendVideoRelatedCreateActivity (options: { 134async function sendVideoRelatedCreateActivity (options: {
133 byActor: MActorLight, 135 byActor: MActorLight
134 video: MVideoAccountLight, 136 video: MVideoAccountLight
135 url: string, 137 url: string
136 object: any, 138 object: any
137 transaction?: Transaction 139 transaction?: Transaction
140 contextType?: ContextType
138}) { 141}) {
139 const activityBuilder = (audience: ActivityAudience) => { 142 const activityBuilder = (audience: ActivityAudience) => {
140 return buildCreateActivity(options.url, options.byActor, options.object, audience) 143 return buildCreateActivity(options.url, options.byActor, options.object, audience)
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..2b01ca5e7 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -8,7 +8,6 @@ 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' 12import { getServerActor } from '../../../helpers/utils'
14import { 13import {
@@ -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..b57bae8fd 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/utils.ts
@@ -7,24 +7,28 @@ import { JobQueue } from '../../job-queue'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' 7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
8import { getServerActor } from '../../../helpers/utils' 8import { getServerActor } from '../../../helpers/utils'
9import { afterCommitIfTransaction } from '../../../helpers/database-utils' 9import { afterCommitIfTransaction } from '../../../helpers/database-utils'
10import { MActorWithInboxes, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models' 10import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models'
11import { ContextType } from '@server/helpers/activitypub'
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 })
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d5c078a29..8642d2432 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
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 6bd46bb58..79ccfbc7e 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -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..d182ca5a2 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -6,7 +6,8 @@ import {
6 ActivityHashTagObject, 6 ActivityHashTagObject,
7 ActivityMagnetUrlObject, 7 ActivityMagnetUrlObject,
8 ActivityPlaylistSegmentHashesObject, 8 ActivityPlaylistSegmentHashesObject,
9 ActivityPlaylistUrlObject, ActivityTagObject, 9 ActivityPlaylistUrlObject,
10 ActivityTagObject,
10 ActivityUrlObject, 11 ActivityUrlObject,
11 ActivityVideoUrlObject, 12 ActivityVideoUrlObject,
12 VideoState 13 VideoState
@@ -17,14 +18,14 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
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, THUMBNAILS_SIZE
28} from '../../initializers/constants' 29} from '../../initializers/constants'
29import { TagModel } from '../../models/video/tag' 30import { TagModel } from '../../models/video/tag'
30import { VideoModel } from '../../models/video/video' 31import { VideoModel } from '../../models/video/video'
@@ -40,7 +41,7 @@ import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub
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'
@@ -67,10 +68,11 @@ import {
67 MVideoAPWithoutCaption, 68 MVideoAPWithoutCaption,
68 MVideoFile, 69 MVideoFile,
69 MVideoFullLight, 70 MVideoFullLight,
70 MVideoId, 71 MVideoId, MVideoImmutable,
71 MVideoThumbnail 72 MVideoThumbnail
72} from '../../typings/models' 73} from '../../typings/models'
73import { MThumbnail } from '../../typings/models/video/thumbnail' 74import { MThumbnail } from '../../typings/models/video/thumbnail'
75import { maxBy, minBy } from 'lodash'
74 76
75async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { 77async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
76 const video = videoArg as MVideoAP 78 const video = videoArg as MVideoAP
@@ -131,19 +133,6 @@ async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
131 return body.description ? body.description : '' 133 return body.description ? body.description : ''
132} 134}
133 135
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) { 136function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
148 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 137 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
149 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 138 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
@@ -173,7 +162,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
173 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate) 162 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
174 163
175 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner) 164 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
176 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err })) 165 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.likes }))
177 } else { 166 } else {
178 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) 167 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
179 } 168 }
@@ -183,7 +172,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
183 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate) 172 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
184 173
185 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner) 174 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
186 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err })) 175 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.dislikes }))
187 } else { 176 } else {
188 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) 177 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
189 } 178 }
@@ -193,7 +182,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
193 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) 182 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
194 183
195 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner) 184 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
196 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err })) 185 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: fetchedVideo.shares }))
197 } else { 186 } else {
198 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) 187 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
199 } 188 }
@@ -203,32 +192,49 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
203 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) 192 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
204 193
205 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner) 194 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
206 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err })) 195 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: fetchedVideo.comments }))
207 } else { 196 } else {
208 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' }) 197 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
209 } 198 }
210 199
211 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })) 200 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }))
212} 201}
213 202
214function getOrCreateVideoAndAccountAndChannel (options: { 203type GetVideoResult <T> = Promise<{
215 videoObject: { id: string } | string, 204 video: T
216 syncParam?: SyncParam, 205 created: boolean
217 fetchType?: 'all', 206 autoBlacklisted?: boolean
207}>
208
209type GetVideoParamAll = {
210 videoObject: { id: string } | string
211 syncParam?: SyncParam
212 fetchType?: 'all'
218 allowRefresh?: boolean 213 allowRefresh?: boolean
219}): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }> 214}
220function getOrCreateVideoAndAccountAndChannel (options: { 215
221 videoObject: { id: string } | string, 216type GetVideoParamImmutable = {
222 syncParam?: SyncParam, 217 videoObject: { id: string } | string
223 fetchType?: VideoFetchByUrlType, 218 syncParam?: SyncParam
219 fetchType: 'only-immutable-attributes'
220 allowRefresh: false
221}
222
223type GetVideoParamOther = {
224 videoObject: { id: string } | string
225 syncParam?: SyncParam
226 fetchType?: 'all' | 'only-video'
224 allowRefresh?: boolean 227 allowRefresh?: boolean
225}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> 228}
226async function getOrCreateVideoAndAccountAndChannel (options: { 229
227 videoObject: { id: string } | string, 230function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
228 syncParam?: SyncParam, 231function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
229 fetchType?: VideoFetchByUrlType, 232function getOrCreateVideoAndAccountAndChannel (
230 allowRefresh?: boolean // true by default 233 options: GetVideoParamOther
231}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> { 234): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
235async function getOrCreateVideoAndAccountAndChannel (
236 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
237): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
232 // Default params 238 // Default params
233 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 239 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
234 const fetchType = options.fetchType || 'all' 240 const fetchType = options.fetchType || 'all'
@@ -236,18 +242,25 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
236 242
237 // Get video url 243 // Get video url
238 const videoUrl = getAPId(options.videoObject) 244 const videoUrl = getAPId(options.videoObject)
239
240 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 245 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
246
241 if (videoFromDatabase) { 247 if (videoFromDatabase) {
242 if (videoFromDatabase.isOutdated() && allowRefresh === true) { 248 // If allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type
249 if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) {
243 const refreshOptions = { 250 const refreshOptions = {
244 video: videoFromDatabase, 251 video: videoFromDatabase as MVideoThumbnail,
245 fetchedType: fetchType, 252 fetchedType: fetchType,
246 syncParam 253 syncParam
247 } 254 }
248 255
249 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) 256 if (syncParam.refreshVideo === true) {
250 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) 257 videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
258 } else {
259 await JobQueue.Instance.createJobWithPromise({
260 type: 'activitypub-refresher',
261 payload: { type: 'video', url: videoFromDatabase.url }
262 })
263 }
251 } 264 }
252 265
253 return { video: videoFromDatabase, created: false } 266 return { video: videoFromDatabase, created: false }
@@ -266,10 +279,10 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
266} 279}
267 280
268async function updateVideoFromAP (options: { 281async function updateVideoFromAP (options: {
269 video: MVideoAccountLightBlacklistAllFiles, 282 video: MVideoAccountLightBlacklistAllFiles
270 videoObject: VideoTorrentObject, 283 videoObject: VideoTorrentObject
271 account: MAccountIdActor, 284 account: MAccountIdActor
272 channel: MChannelDefault, 285 channel: MChannelDefault
273 overrideTo?: string[] 286 overrideTo?: string[]
274}) { 287}) {
275 const { video, videoObject, account, channel, overrideTo } = options 288 const { video, videoObject, account, channel, overrideTo } = options
@@ -284,7 +297,7 @@ async function updateVideoFromAP (options: {
284 let thumbnailModel: MThumbnail 297 let thumbnailModel: MThumbnail
285 298
286 try { 299 try {
287 thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 300 thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
288 } catch (err) { 301 } catch (err) {
289 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) 302 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
290 } 303 }
@@ -300,7 +313,7 @@ async function updateVideoFromAP (options: {
300 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url) 313 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
301 } 314 }
302 315
303 const to = overrideTo ? overrideTo : videoObject.to 316 const to = overrideTo || videoObject.to
304 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to) 317 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to)
305 video.name = videoData.name 318 video.name = videoData.name
306 video.uuid = videoData.uuid 319 video.uuid = videoData.uuid
@@ -327,10 +340,11 @@ async function updateVideoFromAP (options: {
327 340
328 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 341 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
329 342
330 // FIXME: use icon URL instead 343 if (videoUpdated.getPreview()) {
331 const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename)) 344 const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated)
332 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 345 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
333 await videoUpdated.addAndSaveThumbnail(previewModel, t) 346 await videoUpdated.addAndSaveThumbnail(previewModel, t)
347 }
334 348
335 { 349 {
336 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) 350 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url)
@@ -391,7 +405,7 @@ async function updateVideoFromAP (options: {
391 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) 405 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
392 406
393 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 407 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
394 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t) 408 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, c.url, t)
395 }) 409 })
396 await Promise.all(videoCaptionsPromises) 410 await Promise.all(videoCaptionsPromises)
397 } 411 }
@@ -424,8 +438,8 @@ async function updateVideoFromAP (options: {
424} 438}
425 439
426async function refreshVideoIfNeeded (options: { 440async function refreshVideoIfNeeded (options: {
427 video: MVideoThumbnail, 441 video: MVideoThumbnail
428 fetchedType: VideoFetchByUrlType, 442 fetchedType: VideoFetchByUrlType
429 syncParam: SyncParam 443 syncParam: SyncParam
430}): Promise<MVideoThumbnail> { 444}): Promise<MVideoThumbnail> {
431 if (!options.video.isOutdated()) return options.video 445 if (!options.video.isOutdated()) return options.video
@@ -483,7 +497,6 @@ export {
483 federateVideoIfNeeded, 497 federateVideoIfNeeded,
484 fetchRemoteVideo, 498 fetchRemoteVideo,
485 getOrCreateVideoAndAccountAndChannel, 499 getOrCreateVideoAndAccountAndChannel,
486 fetchRemoteVideoStaticFile,
487 fetchRemoteVideoDescription, 500 fetchRemoteVideoDescription,
488 getOrCreateVideoChannelFromVideoObject 501 getOrCreateVideoChannelFromVideoObject
489} 502}
@@ -519,7 +532,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
519 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) 532 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
520 const video = VideoModel.build(videoData) as MVideoThumbnail 533 const video = VideoModel.build(videoData) as MVideoThumbnail
521 534
522 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 535 const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
536 .catch(err => {
537 logger.error('Cannot create miniature from url.', { err })
538 return undefined
539 })
523 540
524 let thumbnailModel: MThumbnail 541 let thumbnailModel: MThumbnail
525 if (waitThumbnail === true) { 542 if (waitThumbnail === true) {
@@ -534,9 +551,12 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
534 551
535 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 552 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
536 553
537 // FIXME: use icon URL instead 554 const previewIcon = getPreviewFromIcons(videoObject)
538 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) 555 const previewUrl = previewIcon
539 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 556 ? previewIcon.url
557 : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
558 const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
559
540 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 560 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
541 561
542 // Process files 562 // Process files
@@ -567,7 +587,7 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
567 587
568 // Process captions 588 // Process captions
569 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 589 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
570 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t) 590 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, c.url, t)
571 }) 591 })
572 await Promise.all(videoCaptionsPromises) 592 await Promise.all(videoCaptionsPromises)
573 593
@@ -588,7 +608,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
588 }) 608 })
589 609
590 if (waitThumbnail === false) { 610 if (waitThumbnail === false) {
611 // Error is already caught above
612 // eslint-disable-next-line @typescript-eslint/no-floating-promises
591 promiseThumbnail.then(thumbnailModel => { 613 promiseThumbnail.then(thumbnailModel => {
614 if (!thumbnailModel) return
615
592 thumbnailModel = videoCreated.id 616 thumbnailModel = videoCreated.id
593 617
594 return thumbnailModel.save() 618 return thumbnailModel.save()
@@ -598,24 +622,19 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
598 return { autoBlacklisted, videoCreated } 622 return { autoBlacklisted, videoCreated }
599} 623}
600 624
601async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) { 625function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) {
602 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED 626 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
603 const duration = videoObject.duration.replace(/[^\d]+/, '') 627 const duration = videoObject.duration.replace(/[^\d]+/, '')
604 628
605 let language: string | undefined 629 const language = videoObject.language?.identifier
606 if (videoObject.language) {
607 language = videoObject.language.identifier
608 }
609 630
610 let category: number | undefined 631 const category = videoObject.category
611 if (videoObject.category) { 632 ? parseInt(videoObject.category.identifier, 10)
612 category = parseInt(videoObject.category.identifier, 10) 633 : undefined
613 }
614 634
615 let licence: number | undefined 635 const licence = videoObject.licence
616 if (videoObject.licence) { 636 ? parseInt(videoObject.licence.identifier, 10)
617 licence = parseInt(videoObject.licence.identifier, 10) 637 : undefined
618 }
619 638
620 const description = videoObject.content || null 639 const description = videoObject.content || null
621 const support = videoObject.support || null 640 const support = videoObject.support || null
@@ -638,8 +657,11 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide
638 duration: parseInt(duration, 10), 657 duration: parseInt(duration, 10),
639 createdAt: new Date(videoObject.published), 658 createdAt: new Date(videoObject.published),
640 publishedAt: new Date(videoObject.published), 659 publishedAt: new Date(videoObject.published),
641 originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null, 660
642 // FIXME: updatedAt does not seems to be considered by Sequelize 661 originallyPublishedAt: videoObject.originallyPublishedAt
662 ? new Date(videoObject.originallyPublishedAt)
663 : null,
664
643 updatedAt: new Date(videoObject.updated), 665 updatedAt: new Date(videoObject.updated),
644 views: videoObject.views, 666 views: videoObject.views,
645 likes: 0, 667 likes: 0,
@@ -672,7 +694,7 @@ function videoFileActivityUrlToDBAttributes (
672 694
673 const mediaType = fileUrl.mediaType 695 const mediaType = fileUrl.mediaType
674 const attribute = { 696 const attribute = {
675 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], 697 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[mediaType],
676 infoHash: parsed.infoHash, 698 infoHash: parsed.infoHash,
677 resolution: fileUrl.height, 699 resolution: fileUrl.height,
678 size: fileUrl.size, 700 size: fileUrl.size,
@@ -722,3 +744,19 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
722 744
723 return attributes 745 return attributes
724} 746}
747
748function getThumbnailFromIcons (videoObject: VideoTorrentObject) {
749 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
750 // Fallback if there are not valid icons
751 if (validIcons.length === 0) validIcons = videoObject.icon
752
753 return minBy(validIcons, 'width')
754}
755
756function getPreviewFromIcons (videoObject: VideoTorrentObject) {
757 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
758
759 // FIXME: don't put a fallback here for compatibility with PeerTube <2.2
760
761 return maxBy(validIcons, 'width')
762}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 1d8a08ed0..572bd03bd 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 }
@@ -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..d0874ab20 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,7 +1,7 @@
1import { createTransport, Transporter } from 'nodemailer' 1import { createTransport, Transporter } from 'nodemailer'
2import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance } 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' 6import { EmailPayload } from './job-queue/handlers/email'
7import { readFileSync } from 'fs-extra' 7import { readFileSync } from 'fs-extra'
@@ -32,14 +32,15 @@ class Emailer {
32 private initialized = false 32 private initialized = false
33 private transporter: Transporter 33 private transporter: Transporter
34 34
35 private constructor () {} 35 private constructor () {
36 }
36 37
37 init () { 38 init () {
38 // Already initialized 39 // Already initialized
39 if (this.initialized === true) return 40 if (this.initialized === true) return
40 this.initialized = true 41 this.initialized = true
41 42
42 if (Emailer.isEnabled()) { 43 if (isEmailEnabled) {
43 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) 44 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
44 45
45 let tls 46 let tls
@@ -97,12 +98,12 @@ class Emailer {
97 const channelName = video.VideoChannel.getDisplayName() 98 const channelName = video.VideoChannel.getDisplayName()
98 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 99 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
99 100
100 const text = `Hi dear user,\n\n` + 101 const text = 'Hi dear user,\n\n' +
101 `Your subscription ${channelName} just published a new video: ${video.name}` + 102 `Your subscription ${channelName} just published a new video: ${video.name}` +
102 `\n\n` + 103 '\n\n' +
103 `You can view it on ${videoUrl} ` + 104 `You can view it on ${videoUrl} ` +
104 `\n\n` + 105 '\n\n' +
105 `Cheers,\n` + 106 'Cheers,\n' +
106 `${CONFIG.EMAIL.BODY.SIGNATURE}` 107 `${CONFIG.EMAIL.BODY.SIGNATURE}`
107 108
108 const emailPayload: EmailPayload = { 109 const emailPayload: EmailPayload = {
@@ -118,10 +119,10 @@ class Emailer {
118 const followerName = actorFollow.ActorFollower.Account.getDisplayName() 119 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
119 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 120 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
120 121
121 const text = `Hi dear user,\n\n` + 122 const text = 'Hi dear user,\n\n' +
122 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` + 123 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
123 `\n\n` + 124 '\n\n' +
124 `Cheers,\n` + 125 'Cheers,\n' +
125 `${CONFIG.EMAIL.BODY.SIGNATURE}` 126 `${CONFIG.EMAIL.BODY.SIGNATURE}`
126 127
127 const emailPayload: EmailPayload = { 128 const emailPayload: EmailPayload = {
@@ -136,10 +137,10 @@ class Emailer {
136 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { 137 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
137 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' 138 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
138 139
139 const text = `Hi dear admin,\n\n` + 140 const text = 'Hi dear admin,\n\n' +
140 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` + 141 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
141 `\n\n` + 142 '\n\n' +
142 `Cheers,\n` + 143 'Cheers,\n' +
143 `${CONFIG.EMAIL.BODY.SIGNATURE}` 144 `${CONFIG.EMAIL.BODY.SIGNATURE}`
144 145
145 const emailPayload: EmailPayload = { 146 const emailPayload: EmailPayload = {
@@ -152,10 +153,10 @@ class Emailer {
152 } 153 }
153 154
154 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { 155 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
155 const text = `Hi dear admin,\n\n` + 156 const text = 'Hi dear admin,\n\n' +
156 `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` + 157 `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
157 `\n\n` + 158 '\n\n' +
158 `Cheers,\n` + 159 'Cheers,\n' +
159 `${CONFIG.EMAIL.BODY.SIGNATURE}` 160 `${CONFIG.EMAIL.BODY.SIGNATURE}`
160 161
161 const emailPayload: EmailPayload = { 162 const emailPayload: EmailPayload = {
@@ -170,12 +171,12 @@ class Emailer {
170 myVideoPublishedNotification (to: string[], video: MVideo) { 171 myVideoPublishedNotification (to: string[], video: MVideo) {
171 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 172 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
172 173
173 const text = `Hi dear user,\n\n` + 174 const text = 'Hi dear user,\n\n' +
174 `Your video ${video.name} has been published.` + 175 `Your video ${video.name} has been published.` +
175 `\n\n` + 176 '\n\n' +
176 `You can view it on ${videoUrl} ` + 177 `You can view it on ${videoUrl} ` +
177 `\n\n` + 178 '\n\n' +
178 `Cheers,\n` + 179 'Cheers,\n' +
179 `${CONFIG.EMAIL.BODY.SIGNATURE}` 180 `${CONFIG.EMAIL.BODY.SIGNATURE}`
180 181
181 const emailPayload: EmailPayload = { 182 const emailPayload: EmailPayload = {
@@ -190,12 +191,12 @@ class Emailer {
190 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { 191 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
191 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() 192 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
192 193
193 const text = `Hi dear user,\n\n` + 194 const text = 'Hi dear user,\n\n' +
194 `Your video import ${videoImport.getTargetIdentifier()} is finished.` + 195 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
195 `\n\n` + 196 '\n\n' +
196 `You can view the imported video on ${videoUrl} ` + 197 `You can view the imported video on ${videoUrl} ` +
197 `\n\n` + 198 '\n\n' +
198 `Cheers,\n` + 199 'Cheers,\n' +
199 `${CONFIG.EMAIL.BODY.SIGNATURE}` 200 `${CONFIG.EMAIL.BODY.SIGNATURE}`
200 201
201 const emailPayload: EmailPayload = { 202 const emailPayload: EmailPayload = {
@@ -210,12 +211,12 @@ class Emailer {
210 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { 211 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
211 const importUrl = WEBSERVER.URL + '/my-account/video-imports' 212 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
212 213
213 const text = `Hi dear user,\n\n` + 214 const text = 'Hi dear user,\n\n' +
214 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + 215 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` +
215 `\n\n` + 216 '\n\n' +
216 `See your videos import dashboard for more information: ${importUrl}` + 217 `See your videos import dashboard for more information: ${importUrl}` +
217 `\n\n` + 218 '\n\n' +
218 `Cheers,\n` + 219 'Cheers,\n' +
219 `${CONFIG.EMAIL.BODY.SIGNATURE}` 220 `${CONFIG.EMAIL.BODY.SIGNATURE}`
220 221
221 const emailPayload: EmailPayload = { 222 const emailPayload: EmailPayload = {
@@ -232,12 +233,12 @@ class Emailer {
232 const video = comment.Video 233 const video = comment.Video
233 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 234 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
234 235
235 const text = `Hi dear user,\n\n` + 236 const text = 'Hi dear user,\n\n' +
236 `A new comment has been posted by ${accountName} on your video ${video.name}` + 237 `A new comment has been posted by ${accountName} on your video ${video.name}` +
237 `\n\n` + 238 '\n\n' +
238 `You can view it on ${commentUrl} ` + 239 `You can view it on ${commentUrl} ` +
239 `\n\n` + 240 '\n\n' +
240 `Cheers,\n` + 241 'Cheers,\n' +
241 `${CONFIG.EMAIL.BODY.SIGNATURE}` 242 `${CONFIG.EMAIL.BODY.SIGNATURE}`
242 243
243 const emailPayload: EmailPayload = { 244 const emailPayload: EmailPayload = {
@@ -254,12 +255,12 @@ class Emailer {
254 const video = comment.Video 255 const video = comment.Video
255 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 256 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
256 257
257 const text = `Hi dear user,\n\n` + 258 const text = 'Hi dear user,\n\n' +
258 `${accountName} mentioned you on video ${video.name}` + 259 `${accountName} mentioned you on video ${video.name}` +
259 `\n\n` + 260 '\n\n' +
260 `You can view the comment on ${commentUrl} ` + 261 `You can view the comment on ${commentUrl} ` +
261 `\n\n` + 262 '\n\n' +
262 `Cheers,\n` + 263 'Cheers,\n' +
263 `${CONFIG.EMAIL.BODY.SIGNATURE}` 264 `${CONFIG.EMAIL.BODY.SIGNATURE}`
264 265
265 const emailPayload: EmailPayload = { 266 const emailPayload: EmailPayload = {
@@ -274,9 +275,9 @@ class Emailer {
274 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) { 275 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) {
275 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() 276 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
276 277
277 const text = `Hi,\n\n` + 278 const text = 'Hi,\n\n' +
278 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + 279 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` +
279 `Cheers,\n` + 280 'Cheers,\n' +
280 `${CONFIG.EMAIL.BODY.SIGNATURE}` 281 `${CONFIG.EMAIL.BODY.SIGNATURE}`
281 282
282 const emailPayload: EmailPayload = { 283 const emailPayload: EmailPayload = {
@@ -292,14 +293,14 @@ class Emailer {
292 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 293 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
293 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 294 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
294 295
295 const text = `Hi,\n\n` + 296 const text = 'Hi,\n\n' +
296 `A recently added video was auto-blacklisted and requires moderator review before publishing.` + 297 'A recently added video was auto-blacklisted and requires moderator review before publishing.' +
297 `\n\n` + 298 '\n\n' +
298 `You can view it and take appropriate action on ${videoUrl}` + 299 `You can view it and take appropriate action on ${videoUrl}` +
299 `\n\n` + 300 '\n\n' +
300 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` + 301 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
301 `\n\n` + 302 '\n\n' +
302 `Cheers,\n` + 303 'Cheers,\n' +
303 `${CONFIG.EMAIL.BODY.SIGNATURE}` 304 `${CONFIG.EMAIL.BODY.SIGNATURE}`
304 305
305 const emailPayload: EmailPayload = { 306 const emailPayload: EmailPayload = {
@@ -312,9 +313,9 @@ class Emailer {
312 } 313 }
313 314
314 addNewUserRegistrationNotification (to: string[], user: MUser) { 315 addNewUserRegistrationNotification (to: string[], user: MUser) {
315 const text = `Hi,\n\n` + 316 const text = 'Hi,\n\n' +
316 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` + 317 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
317 `Cheers,\n` + 318 'Cheers,\n' +
318 `${CONFIG.EMAIL.BODY.SIGNATURE}` 319 `${CONFIG.EMAIL.BODY.SIGNATURE}`
319 320
320 const emailPayload: EmailPayload = { 321 const emailPayload: EmailPayload = {
@@ -367,11 +368,11 @@ class Emailer {
367 } 368 }
368 369
369 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { 370 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
370 const text = `Hi dear user,\n\n` + 371 const text = 'Hi dear user,\n\n' +
371 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` + 372 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` +
372 `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` + 373 `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` +
373 `If you are not the person who initiated this request, please ignore this email.\n\n` + 374 'If you are not the person who initiated this request, please ignore this email.\n\n' +
374 `Cheers,\n` + 375 'Cheers,\n' +
375 `${CONFIG.EMAIL.BODY.SIGNATURE}` 376 `${CONFIG.EMAIL.BODY.SIGNATURE}`
376 377
377 const emailPayload: EmailPayload = { 378 const emailPayload: EmailPayload = {
@@ -383,12 +384,28 @@ class Emailer {
383 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 384 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
384 } 385 }
385 386
387 addPasswordCreateEmailJob (username: string, to: string, resetPasswordUrl: string) {
388 const text = 'Hi,\n\n' +
389 `Welcome to your ${WEBSERVER.HOST} PeerTube instance. Your username is: ${username}.\n\n` +
390 `Please set your password by following this link: ${resetPasswordUrl} (this link will expire within seven days).\n\n` +
391 'Cheers,\n' +
392 `${CONFIG.EMAIL.BODY.SIGNATURE}`
393
394 const emailPayload: EmailPayload = {
395 to: [ to ],
396 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New PeerTube account password',
397 text
398 }
399
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
401 }
402
386 addVerifyEmailJob (to: string, verifyEmailUrl: string) { 403 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
387 const text = `Welcome to PeerTube,\n\n` + 404 const text = 'Welcome to PeerTube,\n\n' +
388 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` + 405 `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` + 406 `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` + 407 'If you are not the person who initiated this request, please ignore this email.\n\n' +
391 `Cheers,\n` + 408 'Cheers,\n' +
392 `${CONFIG.EMAIL.BODY.SIGNATURE}` 409 `${CONFIG.EMAIL.BODY.SIGNATURE}`
393 410
394 const emailPayload: EmailPayload = { 411 const emailPayload: EmailPayload = {
@@ -442,7 +459,7 @@ class Emailer {
442 } 459 }
443 460
444 async sendMail (options: EmailPayload) { 461 async sendMail (options: EmailPayload) {
445 if (!Emailer.isEnabled()) { 462 if (!isEmailEnabled()) {
446 throw new Error('Cannot send mail because SMTP is not configured.') 463 throw new Error('Cannot send mail because SMTP is not configured.')
447 } 464 }
448 465
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-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
index 0ff7b44a0..7d9dd61e9 100644
--- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
@@ -5,11 +5,13 @@ 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'
8import { ContextType } from '@server/helpers/activitypub'
8 9
9export type ActivitypubHttpBroadcastPayload = { 10export type ActivitypubHttpBroadcastPayload = {
10 uris: string[] 11 uris: string[]
11 signatureActorId?: number 12 signatureActorId?: number
12 body: any 13 body: any
14 contextType?: ContextType
13} 15}
14 16
15async function processActivityPubHttpBroadcast (job: Bull.Job) { 17async function processActivityPubHttpBroadcast (job: Bull.Job) {
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
index c70ce3be9..6b71e2891 100644
--- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
@@ -4,11 +4,13 @@ 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'
7import { ContextType } from '@server/helpers/activitypub'
7 8
8export type ActivitypubHttpUnicastPayload = { 9export type ActivitypubHttpUnicastPayload = {
9 uri: string 10 uri: string
10 signatureActorId?: number 11 signatureActorId?: number
11 body: any 12 body: any
13 contextType?: ContextType
12} 14}
13 15
14async function processActivityPubHttpUnicast (job: Bull.Job) { 16async function processActivityPubHttpUnicast (job: Bull.Job) {
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..54b35840d 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,11 @@
1import { buildSignedActivity } from '../../../../helpers/activitypub' 1import { buildSignedActivity, ContextType } from '../../../../helpers/activitypub'
2import { getServerActor } from '../../../../helpers/utils' 2import { getServerActor } from '../../../../helpers/utils'
3import { ActorModel } from '../../../../models/activitypub/actor' 3import { ActorModel } from '../../../../models/activitypub/actor'
4import { sha256 } from '../../../../helpers/core-utils' 4import { sha256 } from '../../../../helpers/core-utils'
5import { HTTP_SIGNATURE } from '../../../../initializers/constants' 5import { HTTP_SIGNATURE } from '../../../../initializers/constants'
6import { MActor } from '../../../../typings/models' 6import { MActor } from '../../../../typings/models'
7 7
8type Payload = { body: any, signatureActorId?: number } 8type Payload = { body: any, contextType?: ContextType, signatureActorId?: number }
9 9
10async function computeBody (payload: Payload) { 10async function computeBody (payload: Payload) {
11 let body = payload.body 11 let body = payload.body
@@ -13,7 +13,7 @@ async function computeBody (payload: Payload) {
13 if (payload.signatureActorId) { 13 if (payload.signatureActorId) {
14 const actorSignature = await ActorModel.load(payload.signatureActorId) 14 const actorSignature = await ActorModel.load(payload.signatureActorId)
15 if (!actorSignature) throw new Error('Unknown signature actor id.') 15 if (!actorSignature) throw new Error('Unknown signature actor id.')
16 body = await buildSignedActivity(actorSignature, payload.body) 16 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType)
17 } 17 }
18 18
19 return body 19 return body
@@ -42,7 +42,7 @@ async function buildSignedRequestOptions (payload: Payload) {
42 42
43function buildGlobalHeaders (body: any) { 43function buildGlobalHeaders (body: any) {
44 return { 44 return {
45 'Digest': buildDigest(body) 45 Digest: buildDigest(body)
46 } 46 }
47} 47}
48 48
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 99c991e72..be9e7d181 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -11,7 +11,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
11import { getVideoFilePath } from '@server/lib/video-paths' 11import { getVideoFilePath } from '@server/lib/video-paths'
12 12
13export type VideoFileImportPayload = { 13export type VideoFileImportPayload = {
14 videoUUID: string, 14 videoUUID: string
15 filePath: string 15 filePath: string
16} 16}
17 17
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 1fca17584..09f225cec 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -221,7 +221,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
221 isNewVideo: true 221 isNewVideo: true
222 } 222 }
223 223
224 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) 224 await JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: dataInput })
225 } 225 }
226 226
227 } catch (err) { 227 } 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..319d7090e
--- /dev/null
+++ b/server/lib/job-queue/handlers/video-redundancy.ts
@@ -0,0 +1,20 @@
1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { VideosRedundancyScheduler } from '@server/lib/schedulers/videos-redundancy-scheduler'
4
5export type VideoRedundancyPayload = {
6 videoId: number
7}
8
9async function processVideoRedundancy (job: Bull.Job) {
10 const payload = job.data as VideoRedundancyPayload
11 logger.info('Processing video redundancy in job %d.', job.id)
12
13 return VideosRedundancyScheduler.Instance.createManualRedundancy(payload.videoId)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 processVideoRedundancy
20}
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 39b9fac98..c020057c9 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -6,7 +6,6 @@ import { JobQueue } from '../job-queue'
6import { federateVideoIfNeeded } from '../../activitypub' 6import { federateVideoIfNeeded } from '../../activitypub'
7import { retryTransactionWrapper } from '../../../helpers/database-utils' 7import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 9import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' 10import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
12import { Notifier } from '../../notifier' 11import { Notifier } from '../../notifier'
@@ -40,8 +39,11 @@ interface OptimizeTranscodingPayload extends BaseTranscodingPayload {
40 type: 'optimize' 39 type: 'optimize'
41} 40}
42 41
43export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload 42export type VideoTranscodingPayload =
44 | OptimizeTranscodingPayload | MergeAudioTranscodingPayload 43 HLSTranscodingPayload
44 | NewResolutionTranscodingPayload
45 | OptimizeTranscodingPayload
46 | MergeAudioTranscodingPayload
45 47
46async function processVideoTranscoding (job: Bull.Job) { 48async function processVideoTranscoding (job: Bull.Job) {
47 const payload = job.data as VideoTranscodingPayload 49 const payload = job.data as VideoTranscodingPayload
@@ -105,7 +107,7 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
105 107
106 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { 108 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
107 // Maybe the video changed in database, refresh it 109 // Maybe the video changed in database, refresh it
108 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) 110 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t)
109 // Video does not exist anymore 111 // Video does not exist anymore
110 if (!videoDatabase) return undefined 112 if (!videoDatabase) return undefined
111 113
@@ -122,8 +124,6 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
122 await createHlsJobIfEnabled(hlsPayload) 124 await createHlsJobIfEnabled(hlsPayload)
123 125
124 if (resolutionsEnabled.length !== 0) { 126 if (resolutionsEnabled.length !== 0) {
125 const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = []
126
127 for (const resolution of resolutionsEnabled) { 127 for (const resolution of resolutionsEnabled) {
128 let dataInput: VideoTranscodingPayload 128 let dataInput: VideoTranscodingPayload
129 129
@@ -143,12 +143,9 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
143 } 143 }
144 } 144 }
145 145
146 const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) 146 JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
147 tasks.push(p)
148 } 147 }
149 148
150 await Promise.all(tasks)
151
152 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) 149 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
153 } else { 150 } else {
154 // No transcoding to do, it's now published 151 // 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..2258cd029 100644
--- a/server/lib/job-queue/handlers/video-views.ts
+++ b/server/lib/job-queue/handlers/video-views.ts
@@ -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..14acace7d 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -13,6 +13,7 @@ import { processVideoImport, VideoImportPayload } from './handlers/video-import'
13import { processVideosViews } from './handlers/video-views' 13import { processVideosViews } from './handlers/video-views'
14import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' 14import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher'
15import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import' 15import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import'
16import { processVideoRedundancy, VideoRedundancyPayload } from '@server/lib/job-queue/handlers/video-redundancy'
16 17
17type CreateJobArgument = 18type CreateJobArgument =
18 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 19 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -24,20 +25,21 @@ type CreateJobArgument =
24 { type: 'email', payload: EmailPayload } | 25 { type: 'email', payload: EmailPayload } |
25 { type: 'video-import', payload: VideoImportPayload } | 26 { type: 'video-import', payload: VideoImportPayload } |
26 { type: 'activitypub-refresher', payload: RefreshPayload } | 27 { type: 'activitypub-refresher', payload: RefreshPayload } |
27 { type: 'videos-views', payload: {} } 28 { type: 'videos-views', payload: {} } |
29 { type: 'video-redundancy', payload: VideoRedundancyPayload }
28 30
29const handlers: { [ id in (JobType | 'video-file') ]: (job: Bull.Job) => Promise<any>} = { 31const handlers: { [id in JobType]: (job: Bull.Job) => Promise<any> } = {
30 'activitypub-http-broadcast': processActivityPubHttpBroadcast, 32 'activitypub-http-broadcast': processActivityPubHttpBroadcast,
31 'activitypub-http-unicast': processActivityPubHttpUnicast, 33 'activitypub-http-unicast': processActivityPubHttpUnicast,
32 'activitypub-http-fetcher': processActivityPubHttpFetcher, 34 'activitypub-http-fetcher': processActivityPubHttpFetcher,
33 'activitypub-follow': processActivityPubFollow, 35 'activitypub-follow': processActivityPubFollow,
34 'video-file-import': processVideoFileImport, 36 'video-file-import': processVideoFileImport,
35 'video-transcoding': processVideoTranscoding, 37 'video-transcoding': processVideoTranscoding,
36 'video-file': processVideoTranscoding, // TODO: remove it (changed in 1.3)
37 'email': processEmail, 38 'email': processEmail,
38 'video-import': processVideoImport, 39 'video-import': processVideoImport,
39 'videos-views': processVideosViews, 40 'videos-views': processVideosViews,
40 'activitypub-refresher': refreshAPObject 41 'activitypub-refresher': refreshAPObject,
42 'video-redundancy': processVideoRedundancy
41} 43}
42 44
43const jobTypes: JobType[] = [ 45const jobTypes: JobType[] = [
@@ -50,20 +52,22 @@ const jobTypes: JobType[] = [
50 'video-file-import', 52 'video-file-import',
51 'video-import', 53 'video-import',
52 'videos-views', 54 'videos-views',
53 'activitypub-refresher' 55 'activitypub-refresher',
56 'video-redundancy'
54] 57]
55 58
56class JobQueue { 59class JobQueue {
57 60
58 private static instance: JobQueue 61 private static instance: JobQueue
59 62
60 private queues: { [ id in JobType ]?: Bull.Queue } = {} 63 private queues: { [id in JobType]?: Bull.Queue } = {}
61 private initialized = false 64 private initialized = false
62 private jobRedisPrefix: string 65 private jobRedisPrefix: string
63 66
64 private constructor () {} 67 private constructor () {
68 }
65 69
66 async init () { 70 init () {
67 // Already initialized 71 // Already initialized
68 if (this.initialized === true) return 72 if (this.initialized === true) return
69 this.initialized = true 73 this.initialized = true
@@ -105,11 +109,16 @@ class JobQueue {
105 } 109 }
106 } 110 }
107 111
108 createJob (obj: CreateJobArgument) { 112 createJob (obj: CreateJobArgument): void {
113 this.createJobWithPromise(obj)
114 .catch(err => logger.error('Cannot create job.', { err, obj }))
115 }
116
117 createJobWithPromise (obj: CreateJobArgument) {
109 const queue = this.queues[obj.type] 118 const queue = this.queues[obj.type]
110 if (queue === undefined) { 119 if (queue === undefined) {
111 logger.error('Unknown queue %s: cannot create job.', obj.type) 120 logger.error('Unknown queue %s: cannot create job.', obj.type)
112 throw Error('Unknown queue, cannot create job') 121 return
113 } 122 }
114 123
115 const jobArgs: Bull.JobOptions = { 124 const jobArgs: Bull.JobOptions = {
@@ -122,10 +131,10 @@ class JobQueue {
122 } 131 }
123 132
124 async listForApi (options: { 133 async listForApi (options: {
125 state: JobState, 134 state: JobState
126 start: number, 135 start: number
127 count: number, 136 count: number
128 asc?: boolean, 137 asc?: boolean
129 jobType: JobType 138 jobType: JobType
130 }): Promise<Bull.Job[]> { 139 }): Promise<Bull.Job[]> {
131 const { state, start, count, asc, jobType } = options 140 const { state, start, count, asc, jobType } = options
@@ -133,16 +142,14 @@ class JobQueue {
133 142
134 const filteredJobTypes = this.filterJobTypes(jobType) 143 const filteredJobTypes = this.filterJobTypes(jobType)
135 144
136 // TODO: optimize
137 for (const jobType of filteredJobTypes) { 145 for (const jobType of filteredJobTypes) {
138 const queue = this.queues[ jobType ] 146 const queue = this.queues[jobType]
139 if (queue === undefined) { 147 if (queue === undefined) {
140 logger.error('Unknown queue %s to list jobs.', jobType) 148 logger.error('Unknown queue %s to list jobs.', jobType)
141 continue 149 continue
142 } 150 }
143 151
144 // FIXME: Bull queue typings does not have getJobs method 152 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) 153 results = results.concat(jobs)
147 } 154 }
148 155
@@ -164,7 +171,7 @@ class JobQueue {
164 const filteredJobTypes = this.filterJobTypes(jobType) 171 const filteredJobTypes = this.filterJobTypes(jobType)
165 172
166 for (const type of filteredJobTypes) { 173 for (const type of filteredJobTypes) {
167 const queue = this.queues[ type ] 174 const queue = this.queues[type]
168 if (queue === undefined) { 175 if (queue === undefined) {
169 logger.error('Unknown queue %s to count jobs.', type) 176 logger.error('Unknown queue %s to count jobs.', type)
170 continue 177 continue
@@ -172,7 +179,7 @@ class JobQueue {
172 179
173 const counts = await queue.getJobCounts() 180 const counts = await queue.getJobCounts()
174 181
175 total += counts[ state ] 182 total += counts[state]
176 } 183 }
177 184
178 return total 185 return total
@@ -188,7 +195,7 @@ class JobQueue {
188 private addRepeatableJobs () { 195 private addRepeatableJobs () {
189 this.queues['videos-views'].add({}, { 196 this.queues['videos-views'].add({}, {
190 repeat: REPEAT_JOBS['videos-views'] 197 repeat: REPEAT_JOBS['videos-views']
191 }) 198 }).catch(err => logger.error('Cannot add repeatable job.', { err }))
192 } 199 }
193 200
194 private filterJobTypes (jobType?: JobType) { 201 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..63197eee1 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -6,7 +6,6 @@ import { 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 } 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,
@@ -32,14 +32,15 @@ class 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 {
@@ -76,17 +79,17 @@ class Notifier {
76 79
77 notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void { 80 notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void {
78 this.notifyModeratorsOfNewVideoAbuse(videoAbuse) 81 this.notifyModeratorsOfNewVideoAbuse(videoAbuse)
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.', videoAbuse.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 {
@@ -548,10 +551,10 @@ class Notifier {
548 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 551 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
549 } 552 }
550 553
551 private async notify <T extends MUserWithNotificationSetting> (options: { 554 private async notify<T extends MUserWithNotificationSetting> (options: {
552 users: T[], 555 users: T[]
553 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>, 556 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
554 emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, 557 emailSender: (emails: string[]) => void
555 settingGetter: (user: T) => UserNotificationSettingValue 558 settingGetter: (user: T) => UserNotificationSettingValue
556 }) { 559 }) {
557 const emails: string[] = [] 560 const emails: string[] = []
@@ -569,7 +572,7 @@ class Notifier {
569 } 572 }
570 573
571 if (emails.length !== 0) { 574 if (emails.length !== 0) {
572 await options.emailSender(emails) 575 options.emailSender(emails)
573 } 576 }
574 } 577 }
575 578
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts
index 25b4f3c61..dcdfba28c 100644
--- a/server/lib/plugins/plugin-index.ts
+++ b/server/lib/plugins/plugin-index.ts
@@ -31,7 +31,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
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)
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 7ebdabd34..73f7a71ce 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -55,30 +55,30 @@ export interface HookInformationValue {
55} 55}
56 56
57type AlterableVideoConstant = 'language' | 'licence' | 'category' 57type AlterableVideoConstant = 'language' | 'licence' | 'category'
58type VideoConstant = { [ key in number | string ]: string } 58type VideoConstant = { [key in number | string]: string }
59type UpdatedVideoConstant = { 59type UpdatedVideoConstant = {
60 [ name in AlterableVideoConstant ]: { 60 [name in AlterableVideoConstant]: {
61 [ npmName: string ]: { 61 [npmName: string]: {
62 added: { key: number | string, label: string }[], 62 added: { key: number | string, label: string }[]
63 deleted: { key: number | string, label: string }[] 63 deleted: { key: number | string, label: string }[]
64 } 64 }
65 } 65 }
66} 66}
67 67
68type PluginLocalesTranslations = { 68type PluginLocalesTranslations = {
69 [ locale: string ]: PluginTranslation 69 [locale: string]: PluginTranslation
70} 70}
71 71
72export class PluginManager implements ServerHook { 72export class PluginManager implements ServerHook {
73 73
74 private static instance: PluginManager 74 private static instance: PluginManager
75 75
76 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} 76 private registeredPlugins: { [name: string]: RegisteredPlugin } = {}
77 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {} 77 private settings: { [name: string]: RegisterServerSettingOptions[] } = {}
78 private hooks: { [ name: string ]: HookInformationValue[] } = {} 78 private hooks: { [name: string]: HookInformationValue[] } = {}
79 private translations: PluginLocalesTranslations = {} 79 private translations: PluginLocalesTranslations = {}
80 80
81 private updatedVideoConstants: UpdatedVideoConstant = { 81 private readonly updatedVideoConstants: UpdatedVideoConstant = {
82 language: {}, 82 language: {},
83 licence: {}, 83 licence: {},
84 category: {} 84 category: {}
@@ -133,7 +133,7 @@ export class PluginManager implements ServerHook {
133 133
134 // ###################### Hooks ###################### 134 // ###################### Hooks ######################
135 135
136 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 136 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
137 if (!this.hooks[hookName]) return Promise.resolve(result) 137 if (!this.hooks[hookName]) return Promise.resolve(result)
138 138
139 const hookType = getHookType(hookName) 139 const hookType = getHookType(hookName)
@@ -312,7 +312,7 @@ export class PluginManager implements ServerHook {
312 clientScripts[c.script] = c 312 clientScripts[c.script] = c
313 } 313 }
314 314
315 this.registeredPlugins[ npmName ] = { 315 this.registeredPlugins[npmName] = {
316 npmName, 316 npmName,
317 name: plugin.name, 317 name: plugin.name,
318 type: plugin.type, 318 type: plugin.type,
@@ -438,7 +438,7 @@ export class PluginManager implements ServerHook {
438 const plugins: RegisteredPlugin[] = [] 438 const plugins: RegisteredPlugin[] = []
439 439
440 for (const npmName of Object.keys(this.registeredPlugins)) { 440 for (const npmName of Object.keys(this.registeredPlugins)) {
441 const plugin = this.registeredPlugins[ npmName ] 441 const plugin = this.registeredPlugins[npmName]
442 if (plugin.type !== type) continue 442 if (plugin.type !== type) continue
443 443
444 plugins.push(plugin) 444 plugins.push(plugin)
@@ -518,11 +518,11 @@ export class PluginManager implements ServerHook {
518 } 518 }
519 } 519 }
520 520
521 private addConstant <T extends string | number> (parameters: { 521 private addConstant<T extends string | number> (parameters: {
522 npmName: string, 522 npmName: string
523 type: AlterableVideoConstant, 523 type: AlterableVideoConstant
524 obj: VideoConstant, 524 obj: VideoConstant
525 key: T, 525 key: T
526 label: string 526 label: string
527 }) { 527 }) {
528 const { npmName, type, obj, key, label } = parameters 528 const { npmName, type, obj, key, label } = parameters
@@ -545,10 +545,10 @@ export class PluginManager implements ServerHook {
545 return true 545 return true
546 } 546 }
547 547
548 private deleteConstant <T extends string | number> (parameters: { 548 private deleteConstant<T extends string | number> (parameters: {
549 npmName: string, 549 npmName: string
550 type: AlterableVideoConstant, 550 type: AlterableVideoConstant
551 obj: VideoConstant, 551 obj: VideoConstant
552 key: T 552 key: T
553 }) { 553 }) {
554 const { npmName, type, obj, key } = parameters 554 const { npmName, type, obj, key } = parameters
@@ -604,7 +604,7 @@ export class PluginManager implements ServerHook {
604 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType) 604 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType)
605 if (!packageJSONValid) { 605 if (!packageJSONValid) {
606 const formattedFields = badFields.map(f => `"${f}"`) 606 const formattedFields = badFields.map(f => `"${f}"`)
607 .join(', ') 607 .join(', ')
608 608
609 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`) 609 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`)
610 } 610 }
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..78d84e02e 100644
--- a/server/lib/redundancy.ts
+++ b/server/lib/redundancy.ts
@@ -13,10 +13,10 @@ async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?
13 await videoRedundancy.destroy({ transaction: t }) 13 await videoRedundancy.destroy({ transaction: t })
14} 14}
15 15
16async function removeRedundancyOf (serverId: number) { 16async function removeRedundanciesOfServer (serverId: number) {
17 const videosRedundancy = await VideoRedundancyModel.listLocalOfServer(serverId) 17 const redundancies = await VideoRedundancyModel.listLocalOfServer(serverId)
18 18
19 for (const redundancy of videosRedundancy) { 19 for (const redundancy of redundancies) {
20 await removeVideoRedundancy(redundancy) 20 await removeVideoRedundancy(redundancy)
21 } 21 }
22} 22}
@@ -24,6 +24,6 @@ async function removeRedundancyOf (serverId: number) {
24// --------------------------------------------------------------------------- 24// ---------------------------------------------------------------------------
25 25
26export { 26export {
27 removeRedundancyOf, 27 removeRedundanciesOfServer,
28 removeVideoRedundancy 28 removeVideoRedundancy
29} 29}
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts
index dd326bc1e..d700a99f0 100644
--- a/server/lib/schedulers/auto-follow-index-instances.ts
+++ b/server/lib/schedulers/auto-follow-index-instances.ts
@@ -57,8 +57,7 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
57 isAutoFollow: true 57 isAutoFollow: true
58 } 58 }
59 59
60 await JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 60 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
61 .catch(err => logger.error('Cannot create follow job for %s.', unfollowedHost, err))
62 } 61 }
63 } 62 }
64 63
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..956780a77 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -4,7 +4,6 @@ import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-upda
4import { retryTransactionWrapper } from '../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../helpers/database-utils'
5import { federateVideoIfNeeded } from '../activitypub' 5import { federateVideoIfNeeded } from '../activitypub'
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..e33a4133a 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -1,7 +1,7 @@
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'
@@ -25,11 +25,12 @@ import {
25 MVideoWithAllFiles 25 MVideoWithAllFiles
26} from '@server/typings/models' 26} from '@server/typings/models'
27import { getVideoFilename } from '../video-paths' 27import { getVideoFilename } from '../video-paths'
28import { VideoModel } from '@server/models/video/video'
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..88e60a7df 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -18,9 +18,9 @@ import { MUser, MUserDefault, MUserId } from '../typings/models/user'
18type ChannelNames = { name: string, displayName: string } 18type ChannelNames = { name: string, displayName: string }
19 19
20async function createUserAccountAndChannelAndPlaylist (parameters: { 20async function createUserAccountAndChannelAndPlaylist (parameters: {
21 userToCreate: MUser, 21 userToCreate: MUser
22 userDisplayName?: string, 22 userDisplayName?: string
23 channelNames?: ChannelNames, 23 channelNames?: ChannelNames
24 validateUser?: boolean 24 validateUser?: boolean
25}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> { 25}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> {
26 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters 26 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
@@ -63,11 +63,11 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
63} 63}
64 64
65async function createLocalAccountWithoutKeys (parameters: { 65async function createLocalAccountWithoutKeys (parameters: {
66 name: string, 66 name: string
67 displayName?: string, 67 displayName?: string
68 userId: number | null, 68 userId: number | null
69 applicationId: number | null, 69 applicationId: number | null
70 t: Transaction | undefined, 70 t: Transaction | undefined
71 type?: ActivityPubActorType 71 type?: ActivityPubActorType
72}) { 72}) {
73 const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters 73 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..3b90b1b94 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -9,15 +9,15 @@ import { Notifier } from './notifier'
9import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models' 9import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models'
10 10
11async function autoBlacklistVideoIfNeeded (parameters: { 11async function autoBlacklistVideoIfNeeded (parameters: {
12 video: MVideoWithBlacklistLight, 12 video: MVideoWithBlacklistLight
13 user?: MUser, 13 user?: MUser
14 isRemote: boolean, 14 isRemote: boolean
15 isNew: boolean, 15 isNew: boolean
16 notify?: boolean, 16 notify?: boolean
17 transaction?: Transaction 17 transaction?: Transaction
18}) { 18}) {
19 const { video, user, isRemote, isNew, notify = true, transaction } = parameters 19 const { video, user, isRemote, isNew, notify = true, transaction } = parameters
20 const doAutoBlacklist = await Hooks.wrapPromiseFun( 20 const doAutoBlacklist = await Hooks.wrapFun(
21 autoBlacklistNeeded, 21 autoBlacklistNeeded,
22 { video, user, isRemote, isNew }, 22 { video, user, isRemote, isNew },
23 'filter:video.auto-blacklist.result' 23 'filter:video.auto-blacklist.result'
@@ -49,10 +49,10 @@ async function autoBlacklistVideoIfNeeded (parameters: {
49 return true 49 return true
50} 50}
51 51
52async function autoBlacklistNeeded (parameters: { 52function autoBlacklistNeeded (parameters: {
53 video: MVideoWithBlacklistLight, 53 video: MVideoWithBlacklistLight
54 isRemote: boolean, 54 isRemote: boolean
55 isNew: boolean, 55 isNew: boolean
56 user?: MUser 56 user?: MUser
57}) { 57}) {
58 const { user, video, isRemote, isNew } = parameters 58 const { user, video, isRemote, isNew } = parameters
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 41eab456b..14829c9d6 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -6,8 +6,7 @@ import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUr
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'
8 8
9type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & 9type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T }
10 { Account?: T }
11 10
12async function createLocalVideoChannel <T extends MAccountId> ( 11async function createLocalVideoChannel <T extends MAccountId> (
13 videoChannelInfo: VideoChannelCreate, 12 videoChannelInfo: VideoChannelCreate,
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index b8074e6d2..fe83d23e7 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -7,9 +7,9 @@ import { 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