aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
commit88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch)
treeb242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /server/helpers
parent53a94c7cfa8368da4cd248d65df8346905938f0c (diff)
parent9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff)
downloadPeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip
Merge branch 'develop' into pr/1217
Diffstat (limited to 'server/helpers')
-rw-r--r--server/helpers/activitypub.ts72
-rw-r--r--server/helpers/captions-utils.ts4
-rw-r--r--server/helpers/core-utils.ts80
-rw-r--r--server/helpers/custom-jsonld-signature.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts99
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts29
-rw-r--r--server/helpers/custom-validators/activitypub/announce.ts13
-rw-r--r--server/helpers/custom-validators/activitypub/cache-file.ts28
-rw-r--r--server/helpers/custom-validators/activitypub/flag.ts14
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts24
-rw-r--r--server/helpers/custom-validators/activitypub/rate.ts15
-rw-r--r--server/helpers/custom-validators/activitypub/undo.ts20
-rw-r--r--server/helpers/custom-validators/activitypub/video-comments.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts34
-rw-r--r--server/helpers/custom-validators/activitypub/view.ts10
-rw-r--r--server/helpers/custom-validators/misc.ts10
-rw-r--r--server/helpers/custom-validators/servers.ts11
-rw-r--r--server/helpers/custom-validators/user-notifications.ts23
-rw-r--r--server/helpers/custom-validators/users.ts10
-rw-r--r--server/helpers/custom-validators/video-captions.ts4
-rw-r--r--server/helpers/custom-validators/video-imports.ts4
-rw-r--r--server/helpers/custom-validators/videos.ts22
-rw-r--r--server/helpers/express-utils.ts7
-rw-r--r--server/helpers/ffmpeg-utils.ts209
-rw-r--r--server/helpers/image-utils.ts17
-rw-r--r--server/helpers/peertube-crypto.ts128
-rw-r--r--server/helpers/regexp.ts23
-rw-r--r--server/helpers/requests.ts21
-rw-r--r--server/helpers/utils.ts37
-rw-r--r--server/helpers/video.ts4
-rw-r--r--server/helpers/webtorrent.ts6
-rw-r--r--server/helpers/youtube-dl.ts8
32 files changed, 658 insertions, 343 deletions
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 2469b37b1..62d78373e 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -1,11 +1,12 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { ResultList } from '../../shared/models' 3import { ResultList } from '../../shared/models'
4import { Activity, ActivityPubActor } from '../../shared/models/activitypub' 4import { Activity } from '../../shared/models/activitypub'
5import { ACTIVITY_PUB } from '../initializers' 5import { ACTIVITY_PUB } from '../initializers'
6import { ActorModel } from '../models/activitypub/actor' 6import { ActorModel } from '../models/activitypub/actor'
7import { signObject } from './peertube-crypto' 7import { signJsonLDObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
9import { parse } from 'url'
9 10
10function activityPubContextify <T> (data: T) { 11function activityPubContextify <T> (data: T) {
11 return Object.assign(data, { 12 return Object.assign(data, {
@@ -14,25 +15,26 @@ function activityPubContextify <T> (data: T) {
14 'https://w3id.org/security/v1', 15 'https://w3id.org/security/v1',
15 { 16 {
16 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', 17 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
17 pt: 'https://joinpeertube.org/ns', 18 pt: 'https://joinpeertube.org/ns#',
18 schema: 'http://schema.org#', 19 sc: 'http://schema.org#',
19 Hashtag: 'as:Hashtag', 20 Hashtag: 'as:Hashtag',
20 uuid: 'schema:identifier', 21 uuid: 'sc:identifier',
21 category: 'schema:category', 22 category: 'sc:category',
22 licence: 'schema:license', 23 licence: 'sc:license',
23 subtitleLanguage: 'schema:subtitleLanguage', 24 subtitleLanguage: 'sc:subtitleLanguage',
24 sensitive: 'as:sensitive', 25 sensitive: 'as:sensitive',
25 language: 'schema:inLanguage', 26 language: 'sc:inLanguage',
26 views: 'schema:Number', 27 views: 'sc:Number',
27 stats: 'schema:Number', 28 state: 'sc:Number',
28 size: 'schema:Number', 29 size: 'sc:Number',
29 fps: 'schema:Number', 30 fps: 'sc:Number',
30 commentsEnabled: 'schema:Boolean', 31 commentsEnabled: 'sc:Boolean',
31 downloadEnabled: 'schema:Boolean', 32 downloadEnabled: 'sc:Boolean',
32 waitTranscoding: 'schema:Boolean', 33 waitTranscoding: 'sc:Boolean',
33 expires: 'schema:expires', 34 expires: 'sc:expires',
34 support: 'schema:Text', 35 support: 'sc:Text',
35 CacheFile: 'pt:CacheFile' 36 CacheFile: 'pt:CacheFile',
37 Infohash: 'pt:Infohash'
36 }, 38 },
37 { 39 {
38 likes: { 40 likes: {
@@ -57,16 +59,16 @@ function activityPubContextify <T> (data: T) {
57} 59}
58 60
59type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> 61type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
60async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { 62async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
61 if (!page || !validator.isInt(page)) { 63 if (!page || !validator.isInt(page)) {
62 // We just display the first page URL, we only need the total items 64 // We just display the first page URL, we only need the total items
63 const result = await handler(0, 1) 65 const result = await handler(0, 1)
64 66
65 return { 67 return {
66 id: url, 68 id: baseUrl,
67 type: 'OrderedCollection', 69 type: 'OrderedCollection',
68 totalItems: result.total, 70 totalItems: result.total,
69 first: url + '?page=1' 71 first: baseUrl + '?page=1'
70 } 72 }
71 } 73 }
72 74
@@ -81,19 +83,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
81 83
82 // There are more results 84 // There are more results
83 if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { 85 if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
84 next = url + '?page=' + (page + 1) 86 next = baseUrl + '?page=' + (page + 1)
85 } 87 }
86 88
87 if (page > 1) { 89 if (page > 1) {
88 prev = url + '?page=' + (page - 1) 90 prev = baseUrl + '?page=' + (page - 1)
89 } 91 }
90 92
91 return { 93 return {
92 id: url + '?page=' + page, 94 id: baseUrl + '?page=' + page,
93 type: 'OrderedCollectionPage', 95 type: 'OrderedCollectionPage',
94 prev, 96 prev,
95 next, 97 next,
96 partOf: url, 98 partOf: baseUrl,
97 orderedItems: result.data, 99 orderedItems: result.data,
98 totalItems: result.total 100 totalItems: result.total
99 } 101 }
@@ -103,19 +105,27 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
103function buildSignedActivity (byActor: ActorModel, data: Object) { 105function buildSignedActivity (byActor: ActorModel, data: Object) {
104 const activity = activityPubContextify(data) 106 const activity = activityPubContextify(data)
105 107
106 return signObject(byActor, activity) as Promise<Activity> 108 return signJsonLDObject(byActor, activity) as Promise<Activity>
107} 109}
108 110
109function getActorUrl (activityActor: string | ActivityPubActor) { 111function getAPId (activity: string | { id: string }) {
110 if (typeof activityActor === 'string') return activityActor 112 if (typeof activity === 'string') return activity
111 113
112 return activityActor.id 114 return activity.id
115}
116
117function checkUrlsSameHost (url1: string, url2: string) {
118 const idHost = parse(url1).host
119 const actorHost = parse(url2).host
120
121 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
113} 122}
114 123
115// --------------------------------------------------------------------------- 124// ---------------------------------------------------------------------------
116 125
117export { 126export {
118 getActorUrl, 127 checkUrlsSameHost,
128 getAPId,
119 activityPubContextify, 129 activityPubContextify,
120 activityPubCollectionPagination, 130 activityPubCollectionPagination,
121 buildSignedActivity 131 buildSignedActivity
diff --git a/server/helpers/captions-utils.ts b/server/helpers/captions-utils.ts
index 660dce65c..0fb11a125 100644
--- a/server/helpers/captions-utils.ts
+++ b/server/helpers/captions-utils.ts
@@ -2,7 +2,7 @@ import { join } from 'path'
2import { CONFIG } from '../initializers' 2import { CONFIG } from '../initializers'
3import { VideoCaptionModel } from '../models/video/video-caption' 3import { VideoCaptionModel } from '../models/video/video-caption'
4import * as srt2vtt from 'srt-to-vtt' 4import * as srt2vtt from 'srt-to-vtt'
5import { createReadStream, createWriteStream, remove, rename } from 'fs-extra' 5import { createReadStream, createWriteStream, remove, move } from 'fs-extra'
6 6
7async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { 7async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) {
8 const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR 8 const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
@@ -13,7 +13,7 @@ async function moveAndProcessCaptionFile (physicalFile: { filename: string, path
13 await convertSrtToVtt(physicalFile.path, destination) 13 await convertSrtToVtt(physicalFile.path, destination)
14 await remove(physicalFile.path) 14 await remove(physicalFile.path)
15 } else { // Just move the vtt file 15 } else { // Just move the vtt file
16 await rename(physicalFile.path, destination) 16 await move(physicalFile.path, destination, { overwrite: true })
17 } 17 }
18 18
19 // This is important in case if there is another attempt in the retry process 19 // This is important in case if there is another attempt in the retry process
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 00bc0bdda..f38b82d97 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -5,12 +5,31 @@
5 5
6import * as bcrypt from 'bcrypt' 6import * as bcrypt from 'bcrypt'
7import * as createTorrent from 'create-torrent' 7import * as createTorrent from 'create-torrent'
8import { createHash, pseudoRandomBytes } from 'crypto' 8import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto'
9import { isAbsolute, join } from 'path' 9import { isAbsolute, join } from 'path'
10import * as pem from 'pem' 10import * as pem from 'pem'
11import { URL } from 'url' 11import { URL } from 'url'
12import { truncate } from 'lodash' 12import { truncate } from 'lodash'
13import { exec } from 'child_process' 13import { exec } from 'child_process'
14import { isArray } from './custom-validators/misc'
15
16const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
17 if (!oldObject || typeof oldObject !== 'object') {
18 return valueConverter(oldObject)
19 }
20
21 if (isArray(oldObject)) {
22 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
23 }
24
25 const newObject = {}
26 Object.keys(oldObject).forEach(oldKey => {
27 const newKey = keyConverter(oldKey)
28 newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter)
29 })
30
31 return newObject
32}
14 33
15const timeTable = { 34const timeTable = {
16 ms: 1, 35 ms: 1,
@@ -21,6 +40,7 @@ const timeTable = {
21 week: 3600000 * 24 * 7, 40 week: 3600000 * 24 * 7,
22 month: 3600000 * 24 * 30 41 month: 3600000 * 24 * 30
23} 42}
43
24export function parseDuration (duration: number | string): number { 44export function parseDuration (duration: number | string): number {
25 if (typeof duration === 'number') return duration 45 if (typeof duration === 'number') return duration
26 46
@@ -41,6 +61,53 @@ export function parseDuration (duration: number | string): number {
41 throw new Error('Duration could not be properly parsed') 61 throw new Error('Duration could not be properly parsed')
42} 62}
43 63
64export function parseBytes (value: string | number): number {
65 if (typeof value === 'number') return value
66
67 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
68 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
69 const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
70 const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
71 const t = /^(\d+)\s*TB$/
72 const g = /^(\d+)\s*GB$/
73 const m = /^(\d+)\s*MB$/
74 const b = /^(\d+)\s*B$/
75 let match
76
77 if (value.match(tgm)) {
78 match = value.match(tgm)
79 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
80 + parseInt(match[2], 10) * 1024 * 1024 * 1024
81 + parseInt(match[3], 10) * 1024 * 1024
82 } else if (value.match(tg)) {
83 match = value.match(tg)
84 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
85 + parseInt(match[2], 10) * 1024 * 1024 * 1024
86 } else if (value.match(tm)) {
87 match = value.match(tm)
88 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
89 + parseInt(match[2], 10) * 1024 * 1024
90 } else if (value.match(gm)) {
91 match = value.match(gm)
92 return parseInt(match[1], 10) * 1024 * 1024 * 1024
93 + parseInt(match[2], 10) * 1024 * 1024
94 } else if (value.match(t)) {
95 match = value.match(t)
96 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
97 } else if (value.match(g)) {
98 match = value.match(g)
99 return parseInt(match[1], 10) * 1024 * 1024 * 1024
100 } else if (value.match(m)) {
101 match = value.match(m)
102 return parseInt(match[1], 10) * 1024 * 1024
103 } else if (value.match(b)) {
104 match = value.match(b)
105 return parseInt(match[1], 10) * 1024
106 } else {
107 return parseInt(value, 10)
108 }
109}
110
44function sanitizeUrl (url: string) { 111function sanitizeUrl (url: string) {
45 const urlObject = new URL(url) 112 const urlObject = new URL(url)
46 113
@@ -126,8 +193,12 @@ function peertubeTruncate (str: string, maxLength: number) {
126 return truncate(str, options) 193 return truncate(str, options)
127} 194}
128 195
129function sha256 (str: string) { 196function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
130 return createHash('sha256').update(str).digest('hex') 197 return createHash('sha256').update(str).digest(encoding)
198}
199
200function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
201 return createHash('sha1').update(str).digest(encoding)
131} 202}
132 203
133function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { 204function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
@@ -187,6 +258,7 @@ export {
187 isTestInstance, 258 isTestInstance,
188 isProdInstance, 259 isProdInstance,
189 260
261 objectConverter,
190 root, 262 root,
191 escapeHTML, 263 escapeHTML,
192 pageToStartAndCount, 264 pageToStartAndCount,
@@ -194,7 +266,9 @@ export {
194 sanitizeHost, 266 sanitizeHost,
195 buildPath, 267 buildPath,
196 peertubeTruncate, 268 peertubeTruncate,
269
197 sha256, 270 sha256,
271 sha1,
198 272
199 promisify0, 273 promisify0,
200 promisify1, 274 promisify1,
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts
index e4f28018e..27a187db1 100644
--- a/server/helpers/custom-jsonld-signature.ts
+++ b/server/helpers/custom-jsonld-signature.ts
@@ -1,5 +1,5 @@
1import * as AsyncLRU from 'async-lru' 1import * as AsyncLRU from 'async-lru'
2import * as jsonld from 'jsonld/' 2import * as jsonld from 'jsonld'
3import * as jsig from 'jsonld-signatures' 3import * as jsig from 'jsonld-signatures'
4 4
5const nodeDocumentLoader = jsonld.documentLoaders.node() 5const nodeDocumentLoader = jsonld.documentLoaders.node()
@@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => {
17 17
18jsig.use('jsonld', jsonld) 18jsig.use('jsonld', jsonld)
19 19
20export { jsig } 20export { jsig, jsonld }
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index 2562ead9b..b24590d9d 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -1,26 +1,14 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { 3import { sanitizeAndCheckActorObject } from './actor'
4 isActorAcceptActivityValid, 4import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc'
5 isActorDeleteActivityValid, 5import { isDislikeActivityValid } from './rate'
6 isActorFollowActivityValid, 6import { sanitizeAndCheckVideoCommentObject } from './video-comments'
7 isActorRejectActivityValid, 7import { sanitizeAndCheckVideoTorrentObject } from './videos'
8 isActorUpdateActivityValid
9} from './actor'
10import { isAnnounceActivityValid } from './announce'
11import { isActivityPubUrlValid } from './misc'
12import { isDislikeActivityValid, isLikeActivityValid } from './rate'
13import { isUndoActivityValid } from './undo'
14import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments'
15import {
16 isVideoFlagValid,
17 isVideoTorrentDeleteActivityValid,
18 sanitizeAndCheckVideoTorrentCreateActivity,
19 sanitizeAndCheckVideoTorrentUpdateActivity
20} from './videos'
21import { isViewActivityValid } from './view' 8import { isViewActivityValid } from './view'
22import { exists } from '../misc' 9import { exists } from '../misc'
23import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file' 10import { isCacheFileObjectValid } from './cache-file'
11import { isFlagActivityValid } from './flag'
24 12
25function isRootActivityValid (activity: any) { 13function isRootActivityValid (activity: any) {
26 return Array.isArray(activity['@context']) && ( 14 return Array.isArray(activity['@context']) && (
@@ -46,7 +34,10 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean
46 Reject: checkRejectActivity, 34 Reject: checkRejectActivity,
47 Announce: checkAnnounceActivity, 35 Announce: checkAnnounceActivity,
48 Undo: checkUndoActivity, 36 Undo: checkUndoActivity,
49 Like: checkLikeActivity 37 Like: checkLikeActivity,
38 View: checkViewActivity,
39 Flag: checkFlagActivity,
40 Dislike: checkDislikeActivity
50} 41}
51 42
52function isActivityValid (activity: any) { 43function isActivityValid (activity: any) {
@@ -66,47 +57,79 @@ export {
66 57
67// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
68 59
60function checkViewActivity (activity: any) {
61 return isBaseActivityValid(activity, 'View') &&
62 isViewActivityValid(activity)
63}
64
65function checkFlagActivity (activity: any) {
66 return isBaseActivityValid(activity, 'Flag') &&
67 isFlagActivityValid(activity)
68}
69
70function checkDislikeActivity (activity: any) {
71 return isBaseActivityValid(activity, 'Dislike') &&
72 isDislikeActivityValid(activity)
73}
74
69function checkCreateActivity (activity: any) { 75function checkCreateActivity (activity: any) {
70 return isViewActivityValid(activity) || 76 return isBaseActivityValid(activity, 'Create') &&
71 isDislikeActivityValid(activity) || 77 (
72 sanitizeAndCheckVideoTorrentCreateActivity(activity) || 78 isViewActivityValid(activity.object) ||
73 isVideoFlagValid(activity) || 79 isDislikeActivityValid(activity.object) ||
74 isVideoCommentCreateActivityValid(activity) || 80 isFlagActivityValid(activity.object) ||
75 isCacheFileCreateActivityValid(activity) 81
82 isCacheFileObjectValid(activity.object) ||
83 sanitizeAndCheckVideoCommentObject(activity.object) ||
84 sanitizeAndCheckVideoTorrentObject(activity.object)
85 )
76} 86}
77 87
78function checkUpdateActivity (activity: any) { 88function checkUpdateActivity (activity: any) {
79 return isCacheFileUpdateActivityValid(activity) || 89 return isBaseActivityValid(activity, 'Update') &&
80 sanitizeAndCheckVideoTorrentUpdateActivity(activity) || 90 (
81 isActorUpdateActivityValid(activity) 91 isCacheFileObjectValid(activity.object) ||
92 sanitizeAndCheckVideoTorrentObject(activity.object) ||
93 sanitizeAndCheckActorObject(activity.object)
94 )
82} 95}
83 96
84function checkDeleteActivity (activity: any) { 97function checkDeleteActivity (activity: any) {
85 return isVideoTorrentDeleteActivityValid(activity) || 98 // We don't really check objects
86 isActorDeleteActivityValid(activity) || 99 return isBaseActivityValid(activity, 'Delete') &&
87 isVideoCommentDeleteActivityValid(activity) 100 isObjectValid(activity.object)
88} 101}
89 102
90function checkFollowActivity (activity: any) { 103function checkFollowActivity (activity: any) {
91 return isActorFollowActivityValid(activity) 104 return isBaseActivityValid(activity, 'Follow') &&
105 isObjectValid(activity.object)
92} 106}
93 107
94function checkAcceptActivity (activity: any) { 108function checkAcceptActivity (activity: any) {
95 return isActorAcceptActivityValid(activity) 109 return isBaseActivityValid(activity, 'Accept')
96} 110}
97 111
98function checkRejectActivity (activity: any) { 112function checkRejectActivity (activity: any) {
99 return isActorRejectActivityValid(activity) 113 return isBaseActivityValid(activity, 'Reject')
100} 114}
101 115
102function checkAnnounceActivity (activity: any) { 116function checkAnnounceActivity (activity: any) {
103 return isAnnounceActivityValid(activity) 117 return isBaseActivityValid(activity, 'Announce') &&
118 isObjectValid(activity.object)
104} 119}
105 120
106function checkUndoActivity (activity: any) { 121function checkUndoActivity (activity: any) {
107 return isUndoActivityValid(activity) 122 return isBaseActivityValid(activity, 'Undo') &&
123 (
124 checkFollowActivity(activity.object) ||
125 checkLikeActivity(activity.object) ||
126 checkDislikeActivity(activity.object) ||
127 checkAnnounceActivity(activity.object) ||
128 checkCreateActivity(activity.object)
129 )
108} 130}
109 131
110function checkLikeActivity (activity: any) { 132function checkLikeActivity (activity: any) {
111 return isLikeActivityValid(activity) 133 return isBaseActivityValid(activity, 'Like') &&
134 isObjectValid(activity.object)
112} 135}
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts
index 77c003cdf..c05f60f14 100644
--- a/server/helpers/custom-validators/activitypub/actor.ts
+++ b/server/helpers/custom-validators/activitypub/actor.ts
@@ -27,7 +27,8 @@ function isActorPublicKeyValid (publicKey: string) {
27 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY) 27 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
28} 28}
29 29
30const actorNameRegExp = new RegExp('^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_\.]+$') 30const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]'
31const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
31function isActorPreferredUsernameValid (preferredUsername: string) { 32function isActorPreferredUsernameValid (preferredUsername: string) {
32 return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp) 33 return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
33} 34}
@@ -72,24 +73,10 @@ function isActorDeleteActivityValid (activity: any) {
72 return isBaseActivityValid(activity, 'Delete') 73 return isBaseActivityValid(activity, 'Delete')
73} 74}
74 75
75function isActorFollowActivityValid (activity: any) { 76function sanitizeAndCheckActorObject (object: any) {
76 return isBaseActivityValid(activity, 'Follow') && 77 normalizeActor(object)
77 isActivityPubUrlValid(activity.object)
78}
79
80function isActorAcceptActivityValid (activity: any) {
81 return isBaseActivityValid(activity, 'Accept')
82}
83
84function isActorRejectActivityValid (activity: any) {
85 return isBaseActivityValid(activity, 'Reject')
86}
87
88function isActorUpdateActivityValid (activity: any) {
89 normalizeActor(activity.object)
90 78
91 return isBaseActivityValid(activity, 'Update') && 79 return isActorObjectValid(object)
92 isActorObjectValid(activity.object)
93} 80}
94 81
95function normalizeActor (actor: any) { 82function normalizeActor (actor: any) {
@@ -127,6 +114,7 @@ function areValidActorHandles (handles: string[]) {
127 114
128export { 115export {
129 normalizeActor, 116 normalizeActor,
117 actorNameAlphabet,
130 areValidActorHandles, 118 areValidActorHandles,
131 isActorEndpointsObjectValid, 119 isActorEndpointsObjectValid,
132 isActorPublicKeyObjectValid, 120 isActorPublicKeyObjectValid,
@@ -137,10 +125,7 @@ export {
137 isActorObjectValid, 125 isActorObjectValid,
138 isActorFollowingCountValid, 126 isActorFollowingCountValid,
139 isActorFollowersCountValid, 127 isActorFollowersCountValid,
140 isActorFollowActivityValid,
141 isActorAcceptActivityValid,
142 isActorRejectActivityValid,
143 isActorDeleteActivityValid, 128 isActorDeleteActivityValid,
144 isActorUpdateActivityValid, 129 sanitizeAndCheckActorObject,
145 isValidActorHandle 130 isValidActorHandle
146} 131}
diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts
deleted file mode 100644
index 0519c6026..000000000
--- a/server/helpers/custom-validators/activitypub/announce.ts
+++ /dev/null
@@ -1,13 +0,0 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
2
3function isAnnounceActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Announce') &&
5 (
6 isActivityPubUrlValid(activity.object) ||
7 (activity.object && isActivityPubUrlValid(activity.object.id))
8 )
9}
10
11export {
12 isAnnounceActivityValid
13}
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts
index bd70934c8..21d5c53ca 100644
--- a/server/helpers/custom-validators/activitypub/cache-file.ts
+++ b/server/helpers/custom-validators/activitypub/cache-file.ts
@@ -1,28 +1,26 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 1import { isActivityPubUrlValid } from './misc'
2import { isRemoteVideoUrlValid } from './videos' 2import { isRemoteVideoUrlValid } from './videos'
3import { isDateValid, exists } from '../misc' 3import { exists, isDateValid } from '../misc'
4import { CacheFileObject } from '../../../../shared/models/activitypub/objects' 4import { CacheFileObject } from '../../../../shared/models/activitypub/objects'
5 5
6function isCacheFileCreateActivityValid (activity: any) {
7 return isBaseActivityValid(activity, 'Create') &&
8 isCacheFileObjectValid(activity.object)
9}
10
11function isCacheFileUpdateActivityValid (activity: any) {
12 return isBaseActivityValid(activity, 'Update') &&
13 isCacheFileObjectValid(activity.object)
14}
15
16function isCacheFileObjectValid (object: CacheFileObject) { 6function isCacheFileObjectValid (object: CacheFileObject) {
17 return exists(object) && 7 return exists(object) &&
18 object.type === 'CacheFile' && 8 object.type === 'CacheFile' &&
19 isDateValid(object.expires) && 9 isDateValid(object.expires) &&
20 isActivityPubUrlValid(object.object) && 10 isActivityPubUrlValid(object.object) &&
21 isRemoteVideoUrlValid(object.url) 11 (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
22} 12}
23 13
14// ---------------------------------------------------------------------------
15
24export { 16export {
25 isCacheFileUpdateActivityValid,
26 isCacheFileCreateActivityValid,
27 isCacheFileObjectValid 17 isCacheFileObjectValid
28} 18}
19
20// ---------------------------------------------------------------------------
21
22function isPlaylistRedundancyUrlValid (url: any) {
23 return url.type === 'Link' &&
24 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
25 isActivityPubUrlValid(url.href)
26}
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts
new file mode 100644
index 000000000..6452e297c
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/flag.ts
@@ -0,0 +1,14 @@
1import { isActivityPubUrlValid } from './misc'
2import { isVideoAbuseReasonValid } from '../video-abuses'
3
4function isFlagActivityValid (activity: any) {
5 return activity.type === 'Flag' &&
6 isVideoAbuseReasonValid(activity.content) &&
7 isActivityPubUrlValid(activity.object)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 isFlagActivityValid
14}
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index 4e2c57f04..f1762d11c 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -28,15 +28,20 @@ function isBaseActivityValid (activity: any, type: string) {
28 return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && 28 return (activity['@context'] === undefined || Array.isArray(activity['@context'])) &&
29 activity.type === type && 29 activity.type === type &&
30 isActivityPubUrlValid(activity.id) && 30 isActivityPubUrlValid(activity.id) &&
31 exists(activity.actor) && 31 isObjectValid(activity.actor) &&
32 (isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) && 32 isUrlCollectionValid(activity.to) &&
33 ( 33 isUrlCollectionValid(activity.cc)
34 activity.to === undefined || 34}
35 (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t))) 35
36 ) && 36function isUrlCollectionValid (collection: any) {
37 return collection === undefined ||
38 (Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
39}
40
41function isObjectValid (object: any) {
42 return exists(object) &&
37 ( 43 (
38 activity.cc === undefined || 44 isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id)
39 (Array.isArray(activity.cc) && activity.cc.every(t => isActivityPubUrlValid(t)))
40 ) 45 )
41} 46}
42 47
@@ -57,5 +62,6 @@ export {
57 isUrlValid, 62 isUrlValid,
58 isActivityPubUrlValid, 63 isActivityPubUrlValid,
59 isBaseActivityValid, 64 isBaseActivityValid,
60 setValidAttributedTo 65 setValidAttributedTo,
66 isObjectValid
61} 67}
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts
index e70bd94b8..ba68e8074 100644
--- a/server/helpers/custom-validators/activitypub/rate.ts
+++ b/server/helpers/custom-validators/activitypub/rate.ts
@@ -1,20 +1,13 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 1import { isActivityPubUrlValid, isObjectValid } from './misc'
2
3function isLikeActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Like') &&
5 isActivityPubUrlValid(activity.object)
6}
7 2
8function isDislikeActivityValid (activity: any) { 3function isDislikeActivityValid (activity: any) {
9 return isBaseActivityValid(activity, 'Create') && 4 return activity.type === 'Dislike' &&
10 activity.object.type === 'Dislike' && 5 isActivityPubUrlValid(activity.actor) &&
11 isActivityPubUrlValid(activity.object.actor) && 6 isObjectValid(activity.object)
12 isActivityPubUrlValid(activity.object.object)
13} 7}
14 8
15// --------------------------------------------------------------------------- 9// ---------------------------------------------------------------------------
16 10
17export { 11export {
18 isLikeActivityValid,
19 isDislikeActivityValid 12 isDislikeActivityValid
20} 13}
diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts
deleted file mode 100644
index 578035893..000000000
--- a/server/helpers/custom-validators/activitypub/undo.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import { isActorFollowActivityValid } from './actor'
2import { isBaseActivityValid } from './misc'
3import { isDislikeActivityValid, isLikeActivityValid } from './rate'
4import { isAnnounceActivityValid } from './announce'
5import { isCacheFileCreateActivityValid } from './cache-file'
6
7function isUndoActivityValid (activity: any) {
8 return isBaseActivityValid(activity, 'Undo') &&
9 (
10 isActorFollowActivityValid(activity.object) ||
11 isLikeActivityValid(activity.object) ||
12 isDislikeActivityValid(activity.object) ||
13 isAnnounceActivityValid(activity.object) ||
14 isCacheFileCreateActivityValid(activity.object)
15 )
16}
17
18export {
19 isUndoActivityValid
20}
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts
index 051c4565a..0415db21c 100644
--- a/server/helpers/custom-validators/activitypub/video-comments.ts
+++ b/server/helpers/custom-validators/activitypub/video-comments.ts
@@ -3,11 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers'
3import { exists, isArray, isDateValid } from '../misc' 3import { exists, isArray, isDateValid } from '../misc'
4import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 4import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
5 5
6function isVideoCommentCreateActivityValid (activity: any) {
7 return isBaseActivityValid(activity, 'Create') &&
8 sanitizeAndCheckVideoCommentObject(activity.object)
9}
10
11function sanitizeAndCheckVideoCommentObject (comment: any) { 6function sanitizeAndCheckVideoCommentObject (comment: any) {
12 if (!comment || comment.type !== 'Note') return false 7 if (!comment || comment.type !== 'Note') return false
13 8
@@ -25,15 +20,9 @@ function sanitizeAndCheckVideoCommentObject (comment: any) {
25 ) // Only accept public comments 20 ) // Only accept public comments
26} 21}
27 22
28function isVideoCommentDeleteActivityValid (activity: any) {
29 return isBaseActivityValid(activity, 'Delete')
30}
31
32// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
33 24
34export { 25export {
35 isVideoCommentCreateActivityValid,
36 isVideoCommentDeleteActivityValid,
37 sanitizeAndCheckVideoCommentObject 26 sanitizeAndCheckVideoCommentObject
38} 27}
39 28
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 5015c59dd..53ad0588d 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -1,7 +1,7 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' 2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers'
3import { peertubeTruncate } from '../../core-utils' 3import { peertubeTruncate } from '../../core-utils'
4import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 4import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
5import { 5import {
6 isVideoDurationValid, 6 isVideoDurationValid,
7 isVideoNameValid, 7 isVideoNameValid,
@@ -12,29 +12,12 @@ import {
12} from '../videos' 12} from '../videos'
13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
14import { VideoState } from '../../../../shared/models/videos' 14import { VideoState } from '../../../../shared/models/videos'
15import { isVideoAbuseReasonValid } from '../video-abuses'
16
17function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
18 return isBaseActivityValid(activity, 'Create') &&
19 sanitizeAndCheckVideoTorrentObject(activity.object)
20}
21 15
22function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 16function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
23 return isBaseActivityValid(activity, 'Update') && 17 return isBaseActivityValid(activity, 'Update') &&
24 sanitizeAndCheckVideoTorrentObject(activity.object) 18 sanitizeAndCheckVideoTorrentObject(activity.object)
25} 19}
26 20
27function isVideoTorrentDeleteActivityValid (activity: any) {
28 return isBaseActivityValid(activity, 'Delete')
29}
30
31function isVideoFlagValid (activity: any) {
32 return isBaseActivityValid(activity, 'Create') &&
33 activity.object.type === 'Flag' &&
34 isVideoAbuseReasonValid(activity.object.content) &&
35 isActivityPubUrlValid(activity.object.object)
36}
37
38function isActivityPubVideoDurationValid (value: string) { 21function isActivityPubVideoDurationValid (value: string) {
39 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration 22 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
40 return exists(value) && 23 return exists(value) &&
@@ -83,32 +66,35 @@ function isRemoteVideoUrlValid (url: any) {
83 66
84 return url.type === 'Link' && 67 return url.type === 'Link' &&
85 ( 68 (
86 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 && 69 // TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
70 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 &&
87 isActivityPubUrlValid(url.href) && 71 isActivityPubUrlValid(url.href) &&
88 validator.isInt(url.height + '', { min: 0 }) && 72 validator.isInt(url.height + '', { min: 0 }) &&
89 validator.isInt(url.size + '', { min: 0 }) && 73 validator.isInt(url.size + '', { min: 0 }) &&
90 (!url.fps || validator.isInt(url.fps + '', { min: -1 })) 74 (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
91 ) || 75 ) ||
92 ( 76 (
93 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 && 77 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 &&
94 isActivityPubUrlValid(url.href) && 78 isActivityPubUrlValid(url.href) &&
95 validator.isInt(url.height + '', { min: 0 }) 79 validator.isInt(url.height + '', { min: 0 })
96 ) || 80 ) ||
97 ( 81 (
98 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 && 82 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 &&
99 validator.isLength(url.href, { min: 5 }) && 83 validator.isLength(url.href, { min: 5 }) &&
100 validator.isInt(url.height + '', { min: 0 }) 84 validator.isInt(url.height + '', { min: 0 })
85 ) ||
86 (
87 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
88 isActivityPubUrlValid(url.href) &&
89 isArray(url.tag)
101 ) 90 )
102} 91}
103 92
104// --------------------------------------------------------------------------- 93// ---------------------------------------------------------------------------
105 94
106export { 95export {
107 sanitizeAndCheckVideoTorrentCreateActivity,
108 sanitizeAndCheckVideoTorrentUpdateActivity, 96 sanitizeAndCheckVideoTorrentUpdateActivity,
109 isVideoTorrentDeleteActivityValid,
110 isRemoteStringIdentifierValid, 97 isRemoteStringIdentifierValid,
111 isVideoFlagValid,
112 sanitizeAndCheckVideoTorrentObject, 98 sanitizeAndCheckVideoTorrentObject,
113 isRemoteVideoUrlValid 99 isRemoteVideoUrlValid
114} 100}
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts
index 7a3aca6f5..41d16469f 100644
--- a/server/helpers/custom-validators/activitypub/view.ts
+++ b/server/helpers/custom-validators/activitypub/view.ts
@@ -1,11 +1,11 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 1import { isActivityPubUrlValid } from './misc'
2 2
3function isViewActivityValid (activity: any) { 3function isViewActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Create') && 4 return activity.type === 'View' &&
5 activity.object.type === 'View' && 5 isActivityPubUrlValid(activity.actor) &&
6 isActivityPubUrlValid(activity.object.actor) && 6 isActivityPubUrlValid(activity.object)
7 isActivityPubUrlValid(activity.object.object)
8} 7}
8
9// --------------------------------------------------------------------------- 9// ---------------------------------------------------------------------------
10 10
11export { 11export {
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts
index 6d10a65a8..76647fea2 100644
--- a/server/helpers/custom-validators/misc.ts
+++ b/server/helpers/custom-validators/misc.ts
@@ -9,6 +9,14 @@ function isArray (value: any) {
9 return Array.isArray(value) 9 return Array.isArray(value)
10} 10}
11 11
12function isNotEmptyIntArray (value: any) {
13 return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0
14}
15
16function isArrayOf (value: any, validator: (value: any) => boolean) {
17 return isArray(value) && value.every(v => validator(v))
18}
19
12function isDateValid (value: string) { 20function isDateValid (value: string) {
13 return exists(value) && validator.isISO8601(value) 21 return exists(value) && validator.isISO8601(value)
14} 22}
@@ -78,6 +86,8 @@ function isFileValid (
78 86
79export { 87export {
80 exists, 88 exists,
89 isArrayOf,
90 isNotEmptyIntArray,
81 isArray, 91 isArray,
82 isIdValid, 92 isIdValid,
83 isUUIDValid, 93 isUUIDValid,
diff --git a/server/helpers/custom-validators/servers.ts b/server/helpers/custom-validators/servers.ts
index d5021bf38..18c80ec8f 100644
--- a/server/helpers/custom-validators/servers.ts
+++ b/server/helpers/custom-validators/servers.ts
@@ -3,6 +3,7 @@ import 'express-validator'
3 3
4import { isArray, exists } from './misc' 4import { isArray, exists } from './misc'
5import { isTestInstance } from '../core-utils' 5import { isTestInstance } from '../core-utils'
6import { CONSTRAINTS_FIELDS } from '../../initializers'
6 7
7function isHostValid (host: string) { 8function isHostValid (host: string) {
8 const isURLOptions = { 9 const isURLOptions = {
@@ -26,9 +27,19 @@ function isEachUniqueHostValid (hosts: string[]) {
26 }) 27 })
27} 28}
28 29
30function isValidContactBody (value: any) {
31 return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY)
32}
33
34function isValidContactFromName (value: any) {
35 return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME)
36}
37
29// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
30 39
31export { 40export {
41 isValidContactBody,
42 isValidContactFromName,
32 isEachUniqueHostValid, 43 isEachUniqueHostValid,
33 isHostValid 44 isHostValid
34} 45}
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts
new file mode 100644
index 000000000..02ea3bbc2
--- /dev/null
+++ b/server/helpers/custom-validators/user-notifications.ts
@@ -0,0 +1,23 @@
1import { exists } from './misc'
2import * as validator from 'validator'
3import { UserNotificationType } from '../../../shared/models/users'
4import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
5
6function isUserNotificationTypeValid (value: any) {
7 return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined
8}
9
10function isUserNotificationSettingValid (value: any) {
11 return exists(value) &&
12 validator.isInt('' + value) && (
13 value === UserNotificationSettingValue.NONE ||
14 value === UserNotificationSettingValue.WEB ||
15 value === UserNotificationSettingValue.EMAIL ||
16 value === (UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL)
17 )
18}
19
20export {
21 isUserNotificationSettingValid,
22 isUserNotificationTypeValid
23}
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 90fc74a48..80652b479 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -42,6 +42,14 @@ function isUserNSFWPolicyValid (value: any) {
42 return exists(value) && nsfwPolicies.indexOf(value) !== -1 42 return exists(value) && nsfwPolicies.indexOf(value) !== -1
43} 43}
44 44
45function isUserWebTorrentEnabledValid (value: any) {
46 return isBooleanValid(value)
47}
48
49function isUserVideosHistoryEnabledValid (value: any) {
50 return isBooleanValid(value)
51}
52
45function isUserAutoPlayVideoValid (value: any) { 53function isUserAutoPlayVideoValid (value: any) {
46 return isBooleanValid(value) 54 return isBooleanValid(value)
47} 55}
@@ -69,6 +77,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
69// --------------------------------------------------------------------------- 77// ---------------------------------------------------------------------------
70 78
71export { 79export {
80 isUserVideosHistoryEnabledValid,
72 isUserBlockedValid, 81 isUserBlockedValid,
73 isUserPasswordValid, 82 isUserPasswordValid,
74 isUserBlockedReasonValid, 83 isUserBlockedReasonValid,
@@ -78,6 +87,7 @@ export {
78 isUserUsernameValid, 87 isUserUsernameValid,
79 isUserEmailVerifiedValid, 88 isUserEmailVerifiedValid,
80 isUserNSFWPolicyValid, 89 isUserNSFWPolicyValid,
90 isUserWebTorrentEnabledValid,
81 isUserAutoPlayVideoValid, 91 isUserAutoPlayVideoValid,
82 isUserDisplayNameValid, 92 isUserDisplayNameValid,
83 isUserDescriptionValid, 93 isUserDescriptionValid,
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts
index 177e9e86e..b33d90e18 100644
--- a/server/helpers/custom-validators/video-captions.ts
+++ b/server/helpers/custom-validators/video-captions.ts
@@ -1,4 +1,4 @@
1import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES } from '../../initializers' 1import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers'
2import { exists, isFileValid } from './misc' 2import { exists, isFileValid } from './misc'
3import { Response } from 'express' 3import { Response } from 'express'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
@@ -8,7 +8,7 @@ function isVideoCaptionLanguageValid (value: any) {
8 return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined 8 return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined
9} 9}
10 10
11const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT) 11const videoCaptionTypes = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
12 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream >< 12 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream ><
13 .map(m => `(${m})`) 13 .map(m => `(${m})`)
14const videoCaptionTypesRegex = videoCaptionTypes.join('|') 14const videoCaptionTypesRegex = videoCaptionTypes.join('|')
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts
index 4d6ab1fa4..ce9e9193c 100644
--- a/server/helpers/custom-validators/video-imports.ts
+++ b/server/helpers/custom-validators/video-imports.ts
@@ -1,7 +1,7 @@
1import 'express-validator' 1import 'express-validator'
2import 'multer' 2import 'multer'
3import * as validator from 'validator' 3import * as validator from 'validator'
4import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers' 4import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers'
5import { exists, isFileValid } from './misc' 5import { exists, isFileValid } from './misc'
6import * as express from 'express' 6import * as express from 'express'
7import { VideoImportModel } from '../../models/video/video-import' 7import { VideoImportModel } from '../../models/video/video-import'
@@ -24,7 +24,7 @@ function isVideoImportStateValid (value: any) {
24 return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined 24 return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined
25} 25}
26 26
27const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`) 27const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`)
28const videoTorrentImportRegex = videoTorrentImportTypes.join('|') 28const videoTorrentImportRegex = videoTorrentImportTypes.join('|')
29function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 29function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
30 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) 30 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true)
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 714f7ac95..95e256b8f 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -3,12 +3,11 @@ import 'express-validator'
3import { values } from 'lodash' 3import { values } from 'lodash'
4import 'multer' 4import 'multer'
5import * as validator from 'validator' 5import * as validator from 'validator'
6import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' 6import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
7import { 7import {
8 CONSTRAINTS_FIELDS, 8 CONSTRAINTS_FIELDS, MIMETYPES,
9 VIDEO_CATEGORIES, 9 VIDEO_CATEGORIES,
10 VIDEO_LICENCES, 10 VIDEO_LICENCES,
11 VIDEO_MIMETYPE_EXT,
12 VIDEO_PRIVACIES, 11 VIDEO_PRIVACIES,
13 VIDEO_RATE_TYPES, 12 VIDEO_RATE_TYPES,
14 VIDEO_STATES 13 VIDEO_STATES
@@ -22,6 +21,10 @@ import { fetchVideo, VideoFetchType } from '../video'
22 21
23const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS 22const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
24 23
24function isVideoFilterValid (filter: VideoFilter) {
25 return filter === 'local' || filter === 'all-local'
26}
27
25function isVideoCategoryValid (value: any) { 28function isVideoCategoryValid (value: any) {
26 return value === null || VIDEO_CATEGORIES[ value ] !== undefined 29 return value === null || VIDEO_CATEGORIES[ value ] !== undefined
27} 30}
@@ -79,10 +82,15 @@ function isVideoRatingTypeValid (value: string) {
79 return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 82 return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1
80} 83}
81 84
82const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) 85function isVideoFileExtnameValid (value: string) {
83const videoFileTypesRegex = videoFileTypes.join('|') 86 return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined
87}
84 88
85function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 89function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
90 const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
91 .map(m => `(${m})`)
92 .join('|')
93
86 return isFileValid(files, videoFileTypesRegex, 'videofile', null) 94 return isFileValid(files, videoFileTypesRegex, 'videofile', null)
87} 95}
88 96
@@ -217,6 +225,7 @@ export {
217 isVideoStateValid, 225 isVideoStateValid,
218 isVideoViewsValid, 226 isVideoViewsValid,
219 isVideoRatingTypeValid, 227 isVideoRatingTypeValid,
228 isVideoFileExtnameValid,
220 isVideoDurationValid, 229 isVideoDurationValid,
221 isVideoTagValid, 230 isVideoTagValid,
222 isVideoPrivacyValid, 231 isVideoPrivacyValid,
@@ -225,5 +234,6 @@ export {
225 isVideoExist, 234 isVideoExist,
226 isVideoImage, 235 isVideoImage,
227 isVideoChannelOfAccountExist, 236 isVideoChannelOfAccountExist,
228 isVideoSupportValid 237 isVideoSupportValid,
238 isVideoFilterValid
229} 239}
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 8a9cee8c5..9a72ee96d 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -2,18 +2,17 @@ import * as express from 'express'
2import * as multer from 'multer' 2import * as multer from 'multer'
3import { CONFIG, REMOTE_SCHEME } from '../initializers' 3import { CONFIG, REMOTE_SCHEME } from '../initializers'
4import { logger } from './logger' 4import { logger } from './logger'
5import { User } from '../../shared/models/users'
6import { deleteFileAsync, generateRandomString } from './utils' 5import { deleteFileAsync, generateRandomString } from './utils'
7import { extname } from 'path' 6import { extname } from 'path'
8import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
9import { UserModel } from '../models/account/user' 8import { UserModel } from '../models/account/user'
10 9
11function buildNSFWFilter (res: express.Response, paramNSFW?: string) { 10function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
12 if (paramNSFW === 'true') return true 11 if (paramNSFW === 'true') return true
13 if (paramNSFW === 'false') return false 12 if (paramNSFW === 'false') return false
14 if (paramNSFW === 'both') return undefined 13 if (paramNSFW === 'both') return undefined
15 14
16 if (res.locals.oauth) { 15 if (res && res.locals.oauth) {
17 const user: UserModel = res.locals.oauth.token.User 16 const user: UserModel = res.locals.oauth.token.User
18 17
19 // User does not want NSFW videos 18 // User does not want NSFW videos
@@ -101,7 +100,7 @@ function createReqFiles (
101} 100}
102 101
103function isUserAbleToSearchRemoteURI (res: express.Response) { 102function isUserAbleToSearchRemoteURI (res: express.Response) {
104 const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined 103 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
105 104
106 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || 105 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
107 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) 106 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 22bc25476..133b1b03b 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,7 +1,7 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { dirname, join } from 'path'
3import { VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' 4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
7import { checkFFmpegEncoders } from '../initializers/checker-before-init' 7import { checkFFmpegEncoders } from '../initializers/checker-before-init'
@@ -29,19 +29,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
29 return resolutionsEnabled 29 return resolutionsEnabled
30} 30}
31 31
32async function getVideoFileResolution (path: string) { 32async function getVideoFileSize (path: string) {
33 const videoStream = await getVideoFileStream(path) 33 const videoStream = await getVideoFileStream(path)
34 34
35 return { 35 return {
36 videoFileResolution: Math.min(videoStream.height, videoStream.width), 36 width: videoStream.width,
37 isPortraitMode: videoStream.height > videoStream.width 37 height: videoStream.height
38 }
39}
40
41async function getVideoFileResolution (path: string) {
42 const size = await getVideoFileSize(path)
43
44 return {
45 videoFileResolution: Math.min(size.height, size.width),
46 isPortraitMode: size.height > size.width
38 } 47 }
39} 48}
40 49
41async function getVideoFileFPS (path: string) { 50async function getVideoFileFPS (path: string) {
42 const videoStream = await getVideoFileStream(path) 51 const videoStream = await getVideoFileStream(path)
43 52
44 for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { 53 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
45 const valuesText: string = videoStream[key] 54 const valuesText: string = videoStream[key]
46 if (!valuesText) continue 55 if (!valuesText) continue
47 56
@@ -55,6 +64,16 @@ async function getVideoFileFPS (path: string) {
55 return 0 64 return 0
56} 65}
57 66
67async function getVideoFileBitrate (path: string) {
68 return new Promise<number>((res, rej) => {
69 ffmpeg.ffprobe(path, (err, metadata) => {
70 if (err) return rej(err)
71
72 return res(metadata.format.bit_rate)
73 })
74 })
75}
76
58function getDurationFromVideoFile (path: string) { 77function getDurationFromVideoFile (path: string) {
59 return new Promise<number>((res, rej) => { 78 return new Promise<number>((res, rej) => {
60 ffmpeg.ffprobe(path, (err, metadata) => { 79 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -100,64 +119,87 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
100type TranscodeOptions = { 119type TranscodeOptions = {
101 inputPath: string 120 inputPath: string
102 outputPath: string 121 outputPath: string
103 resolution?: VideoResolution 122 resolution: VideoResolution
104 isPortraitMode?: boolean 123 isPortraitMode?: boolean
124
125 hlsPlaylist?: {
126 videoFilename: string
127 }
105} 128}
106 129
107function transcode (options: TranscodeOptions) { 130function transcode (options: TranscodeOptions) {
108 return new Promise<void>(async (res, rej) => { 131 return new Promise<void>(async (res, rej) => {
109 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 132 try {
110 .output(options.outputPath) 133 let fps = await getVideoFileFPS(options.inputPath)
111 .preset(standard)
112
113 if (CONFIG.TRANSCODING.THREADS > 0) {
114 // if we don't set any threads ffmpeg will chose automatically
115 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
116 }
117
118 let fps = await getVideoFileFPS(options.inputPath)
119 if (options.resolution !== undefined) {
120 // '?x720' or '720x?' for example
121 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
122 command = command.size(size)
123
124 // On small/medium resolutions, limit FPS 134 // On small/medium resolutions, limit FPS
125 if ( 135 if (
136 options.resolution !== undefined &&
126 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && 137 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
127 fps > VIDEO_TRANSCODING_FPS.AVERAGE 138 fps > VIDEO_TRANSCODING_FPS.AVERAGE
128 ) { 139 ) {
129 fps = VIDEO_TRANSCODING_FPS.AVERAGE 140 fps = VIDEO_TRANSCODING_FPS.AVERAGE
130 } 141 }
131 }
132 142
133 if (fps) { 143 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
134 // Hard FPS limits 144 .output(options.outputPath)
135 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX 145 command = await presetH264(command, options.resolution, fps)
136 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
137 146
138 command = command.withFPS(fps) 147 if (CONFIG.TRANSCODING.THREADS > 0) {
139 } 148 // if we don't set any threads ffmpeg will chose automatically
149 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
150 }
151
152 if (options.resolution !== undefined) {
153 // '?x720' or '720x?' for example
154 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
155 command = command.size(size)
156 }
157
158 if (fps) {
159 // Hard FPS limits
160 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
161 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
162
163 command = command.withFPS(fps)
164 }
165
166 if (options.hlsPlaylist) {
167 const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
168
169 command = command.outputOption('-hls_time 4')
170 .outputOption('-hls_list_size 0')
171 .outputOption('-hls_playlist_type vod')
172 .outputOption('-hls_segment_filename ' + videoPath)
173 .outputOption('-hls_segment_type fmp4')
174 .outputOption('-f hls')
175 .outputOption('-hls_flags single_file')
176 }
140 177
141 command 178 command
142 .on('error', (err, stdout, stderr) => { 179 .on('error', (err, stdout, stderr) => {
143 logger.error('Error in transcoding job.', { stdout, stderr }) 180 logger.error('Error in transcoding job.', { stdout, stderr })
144 return rej(err) 181 return rej(err)
145 }) 182 })
146 .on('end', res) 183 .on('end', res)
147 .run() 184 .run()
185 } catch (err) {
186 return rej(err)
187 }
148 }) 188 })
149} 189}
150 190
151// --------------------------------------------------------------------------- 191// ---------------------------------------------------------------------------
152 192
153export { 193export {
194 getVideoFileSize,
154 getVideoFileResolution, 195 getVideoFileResolution,
155 getDurationFromVideoFile, 196 getDurationFromVideoFile,
156 generateImageFromVideoFile, 197 generateImageFromVideoFile,
157 transcode, 198 transcode,
158 getVideoFileFPS, 199 getVideoFileFPS,
159 computeResolutionsToTranscode, 200 computeResolutionsToTranscode,
160 audio 201 audio,
202 getVideoFileBitrate
161} 203}
162 204
163// --------------------------------------------------------------------------- 205// ---------------------------------------------------------------------------
@@ -168,7 +210,7 @@ function getVideoFileStream (path: string) {
168 if (err) return rej(err) 210 if (err) return rej(err)
169 211
170 const videoStream = metadata.streams.find(s => s.codec_type === 'video') 212 const videoStream = metadata.streams.find(s => s.codec_type === 'video')
171 if (!videoStream) throw new Error('Cannot find video stream of ' + path) 213 if (!videoStream) return rej(new Error('Cannot find video stream of ' + path))
172 214
173 return res(videoStream) 215 return res(videoStream)
174 }) 216 })
@@ -182,11 +224,10 @@ function getVideoFileStream (path: string) {
182 * and quality. Superfast and ultrafast will give you better 224 * and quality. Superfast and ultrafast will give you better
183 * performance, but then quality is noticeably worse. 225 * performance, but then quality is noticeably worse.
184 */ 226 */
185function veryfast (_ffmpeg) { 227async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
186 _ffmpeg 228 let localCommand = await presetH264(command, resolution, fps)
187 .preset(standard) 229 localCommand = localCommand.outputOption('-preset:v veryfast')
188 .outputOption('-preset:v veryfast') 230 .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ])
189 .outputOption(['--aq-mode=2', '--aq-strength=1.3'])
190 /* 231 /*
191 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html 232 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
192 Our target situation is closer to a livestream than a stream, 233 Our target situation is closer to a livestream than a stream,
@@ -198,31 +239,39 @@ function veryfast (_ffmpeg) {
198 Make up for most of the loss of grain and macroblocking 239 Make up for most of the loss of grain and macroblocking
199 with less computing power. 240 with less computing power.
200 */ 241 */
242
243 return localCommand
201} 244}
202 245
203/** 246/**
204 * A preset optimised for a stillimage audio video 247 * A preset optimised for a stillimage audio video
205 */ 248 */
206function audio (_ffmpeg) { 249async function presetStillImageWithAudio (
207 _ffmpeg 250 command: ffmpeg.FfmpegCommand,
208 .preset(veryfast) 251 resolution: VideoResolution,
209 .outputOption('-tune stillimage') 252 fps: number
253): Promise<ffmpeg.FfmpegCommand> {
254 let localCommand = await presetH264VeryFast(command, resolution, fps)
255 localCommand = localCommand.outputOption('-tune stillimage')
256
257 return localCommand
210} 258}
211 259
212/** 260/**
213 * A toolbox to play with audio 261 * A toolbox to play with audio
214 */ 262 */
215namespace audio { 263namespace audio {
216 export const get = (_ffmpeg, pos: number | string = 0) => { 264 export const get = (option: ffmpeg.FfmpegCommand | string) => {
217 // without position, ffprobe considers the last input only 265 // without position, ffprobe considers the last input only
218 // we make it consider the first input only 266 // we make it consider the first input only
219 // if you pass a file path to pos, then ffprobe acts on that file directly 267 // if you pass a file path to pos, then ffprobe acts on that file directly
220 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { 268 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
221 _ffmpeg.ffprobe(pos, (err,data) => { 269
270 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
222 if (err) return rej(err) 271 if (err) return rej(err)
223 272
224 if ('streams' in data) { 273 if ('streams' in data) {
225 const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') 274 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
226 if (audioStream) { 275 if (audioStream) {
227 return res({ 276 return res({
228 absolutePath: data.format.filename, 277 absolutePath: data.format.filename,
@@ -230,8 +279,15 @@ namespace audio {
230 }) 279 })
231 } 280 }
232 } 281 }
282
233 return res({ absolutePath: data.format.filename }) 283 return res({ absolutePath: data.format.filename })
234 }) 284 }
285
286 if (typeof option === 'string') {
287 return ffmpeg.ffprobe(option, parseFfprobe)
288 }
289
290 return option.ffprobe(parseFfprobe)
235 }) 291 })
236 } 292 }
237 293
@@ -273,39 +329,48 @@ namespace audio {
273 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 329 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
274 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr 330 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
275 */ 331 */
276async function standard (_ffmpeg) { 332async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
277 let localFfmpeg = _ffmpeg 333 let localCommand = command
278 .format('mp4') 334 .format('mp4')
279 .videoCodec('libx264') 335 .videoCodec('libx264')
280 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution 336 .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
281 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it 337 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it
282 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 338 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
339 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
283 .outputOption('-map_metadata -1') // strip all metadata 340 .outputOption('-map_metadata -1') // strip all metadata
284 .outputOption('-movflags faststart') 341 .outputOption('-movflags faststart')
285 const _audio = await audio.get(localFfmpeg)
286 342
287 if (!_audio.audioStream) { 343 const parsedAudio = await audio.get(localCommand)
288 return localFfmpeg.noAudio()
289 }
290 344
291 // we favor VBR, if a good AAC encoder is available 345 if (!parsedAudio.audioStream) {
292 if ((await checkFFmpegEncoders()).get('libfdk_aac')) { 346 localCommand = localCommand.noAudio()
293 return localFfmpeg 347 } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available
348 localCommand = localCommand
294 .audioCodec('libfdk_aac') 349 .audioCodec('libfdk_aac')
295 .audioQuality(5) 350 .audioQuality(5)
351 } else {
352 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates
353 // of course this is far from perfect, but it might save some space in the end
354 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
355 let bitrate: number
356 if (audio.bitrate[ audioCodecName ]) {
357 localCommand = localCommand.audioCodec('aac')
358
359 bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
360 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
361 }
296 } 362 }
297 363
298 // we try to reduce the ceiling bitrate by making rough correspondances of bitrates 364 // Constrained Encoding (VBV)
299 // of course this is far from perfect, but it might save some space in the end 365 // https://slhck.info/video/2017/03/01/rate-control.html
300 const audioCodecName = _audio.audioStream['codec_name'] 366 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
301 let bitrate: number 367 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
302 if (audio.bitrate[audioCodecName]) { 368 localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
303 bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate'])
304
305 if (bitrate === -1) return localFfmpeg.audioCodec('copy')
306 }
307 369
308 if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) 370 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
371 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
372 // https://superuser.com/a/908325
373 localCommand = localCommand.outputOption(`-g ${ fps * 2 }`)
309 374
310 return localFfmpeg 375 return localCommand
311} 376}
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index 3eaa674ed..e43ea3f1d 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -1,13 +1,26 @@
1import 'multer' 1import 'multer'
2import * as sharp from 'sharp' 2import * as sharp from 'sharp'
3import { remove } from 'fs-extra' 3import { readFile, remove } from 'fs-extra'
4import { logger } from './logger'
4 5
5async function processImage ( 6async function processImage (
6 physicalFile: { path: string }, 7 physicalFile: { path: string },
7 destination: string, 8 destination: string,
8 newSize: { width: number, height: number } 9 newSize: { width: number, height: number }
9) { 10) {
10 await sharp(physicalFile.path) 11 if (physicalFile.path === destination) {
12 throw new Error('Sharp needs an input path different that the output path.')
13 }
14
15 logger.debug('Processing image %s to %s.', physicalFile.path, destination)
16
17 // Avoid sharp cache
18 const buf = await readFile(physicalFile.path)
19 const sharpInstance = sharp(buf)
20
21 await remove(destination)
22
23 await sharpInstance
11 .resize(newSize.width, newSize.height) 24 .resize(newSize.width, newSize.height)
12 .toFile(destination) 25 .toFile(destination)
13 26
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 5c182961d..ab9ec077e 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -1,8 +1,14 @@
1import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers' 1import { Request } from 'express'
2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
2import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
3import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' 4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
4import { jsig } from './custom-jsonld-signature' 5import { jsig, jsonld } from './custom-jsonld-signature'
5import { logger } from './logger' 6import { logger } from './logger'
7import { cloneDeep } from 'lodash'
8import { createVerify } from 'crypto'
9import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
10
11const httpSignature = require('http-signature')
6 12
7async function createPrivateAndPublicKeys () { 13async function createPrivateAndPublicKeys () {
8 logger.info('Generating a RSA key...') 14 logger.info('Generating a RSA key...')
@@ -13,18 +19,57 @@ async function createPrivateAndPublicKeys () {
13 return { privateKey: key, publicKey } 19 return { privateKey: key, publicKey }
14} 20}
15 21
16function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { 22// User password checks
23
24function comparePassword (plainPassword: string, hashPassword: string) {
25 return bcryptComparePromise(plainPassword, hashPassword)
26}
27
28async function cryptPassword (password: string) {
29 const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE)
30
31 return bcryptHashPromise(password, salt)
32}
33
34// HTTP Signature
35
36function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
37 if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
38 return buildDigest(rawBody.toString()) === req.headers['digest']
39 }
40
41 return true
42}
43
44function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean {
45 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
46}
47
48function parseHTTPSignature (req: Request, clockSkew?: number) {
49 return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew })
50}
51
52// JSONLD
53
54async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> {
55 if (signedDocument.signature.type === 'RsaSignature2017') {
56 // Mastodon algorithm
57 const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
58 // Success? If no, try with our library
59 if (res === true) return true
60 }
61
17 const publicKeyObject = { 62 const publicKeyObject = {
18 '@context': jsig.SECURITY_CONTEXT_URL, 63 '@context': jsig.SECURITY_CONTEXT_URL,
19 '@id': fromActor.url, 64 id: fromActor.url,
20 '@type': 'CryptographicKey', 65 type: 'CryptographicKey',
21 owner: fromActor.url, 66 owner: fromActor.url,
22 publicKeyPem: fromActor.publicKey 67 publicKeyPem: fromActor.publicKey
23 } 68 }
24 69
25 const publicKeyOwnerObject = { 70 const publicKeyOwnerObject = {
26 '@context': jsig.SECURITY_CONTEXT_URL, 71 '@context': jsig.SECURITY_CONTEXT_URL,
27 '@id': fromActor.url, 72 id: fromActor.url,
28 publicKey: [ publicKeyObject ] 73 publicKey: [ publicKeyObject ]
29 } 74 }
30 75
@@ -33,14 +78,54 @@ function isSignatureVerified (fromActor: ActorModel, signedDocument: object) {
33 publicKeyOwner: publicKeyOwnerObject 78 publicKeyOwner: publicKeyOwnerObject
34 } 79 }
35 80
36 return jsig.promises.verify(signedDocument, options) 81 return jsig.promises
37 .catch(err => { 82 .verify(signedDocument, options)
38 logger.error('Cannot check signature.', { err }) 83 .then((result: { verified: boolean }) => result.verified)
39 return false 84 .catch(err => {
40 }) 85 logger.error('Cannot check signature.', { err })
86 return false
87 })
88}
89
90// Backward compatibility with "other" implementations
91async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) {
92 function hash (obj: any): Promise<any> {
93 return jsonld.promises
94 .normalize(obj, {
95 algorithm: 'URDNA2015',
96 format: 'application/n-quads'
97 })
98 .then(res => sha256(res))
99 }
100
101 const signatureCopy = cloneDeep(signedDocument.signature)
102 Object.assign(signatureCopy, {
103 '@context': [
104 'https://w3id.org/security/v1',
105 { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
106 ]
107 })
108 delete signatureCopy.type
109 delete signatureCopy.id
110 delete signatureCopy.signatureValue
111
112 const docWithoutSignature = cloneDeep(signedDocument)
113 delete docWithoutSignature.signature
114
115 const [ documentHash, optionsHash ] = await Promise.all([
116 hash(docWithoutSignature),
117 hash(signatureCopy)
118 ])
119
120 const toVerify = optionsHash + documentHash
121
122 const verify = createVerify('RSA-SHA256')
123 verify.update(toVerify, 'utf8')
124
125 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
41} 126}
42 127
43function signObject (byActor: ActorModel, data: any) { 128function signJsonLDObject (byActor: ActorModel, data: any) {
44 const options = { 129 const options = {
45 privateKeyPem: byActor.privateKey, 130 privateKeyPem: byActor.privateKey,
46 creator: byActor.url, 131 creator: byActor.url,
@@ -50,22 +135,15 @@ function signObject (byActor: ActorModel, data: any) {
50 return jsig.promises.sign(data, options) 135 return jsig.promises.sign(data, options)
51} 136}
52 137
53function comparePassword (plainPassword: string, hashPassword: string) {
54 return bcryptComparePromise(plainPassword, hashPassword)
55}
56
57async function cryptPassword (password: string) {
58 const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE)
59
60 return bcryptHashPromise(password, salt)
61}
62
63// --------------------------------------------------------------------------- 138// ---------------------------------------------------------------------------
64 139
65export { 140export {
66 isSignatureVerified, 141 isHTTPSignatureDigestValid,
142 parseHTTPSignature,
143 isHTTPSignatureVerified,
144 isJsonLDSignatureVerified,
67 comparePassword, 145 comparePassword,
68 createPrivateAndPublicKeys, 146 createPrivateAndPublicKeys,
69 cryptPassword, 147 cryptPassword,
70 signObject 148 signJsonLDObject
71} 149}
diff --git a/server/helpers/regexp.ts b/server/helpers/regexp.ts
new file mode 100644
index 000000000..2336654b0
--- /dev/null
+++ b/server/helpers/regexp.ts
@@ -0,0 +1,23 @@
1// Thanks to https://regex101.com
2function regexpCapture (str: string, regex: RegExp, maxIterations = 100) {
3 let m: RegExpExecArray
4 let i = 0
5 let result: RegExpExecArray[] = []
6
7 // tslint:disable:no-conditional-assignment
8 while ((m = regex.exec(str)) !== null && i < maxIterations) {
9 // This is necessary to avoid infinite loops with zero-width matches
10 if (m.index === regex.lastIndex) {
11 regex.lastIndex++
12 }
13
14 result.push(m)
15 i++
16 }
17
18 return result
19}
20
21export {
22 regexpCapture
23}
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index ee9e80404..5c6dc5e19 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,17 +1,19 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { createWriteStream } from 'fs-extra' 2import { createWriteStream } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB } from '../initializers' 4import { ACTIVITY_PUB, CONFIG } from '../initializers'
5import { processImage } from './image-utils'
6import { join } from 'path'
5 7
6function doRequest ( 8function doRequest <T> (
7 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 9 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
8): Bluebird<{ response: request.RequestResponse, body: any }> { 10): Bluebird<{ response: request.RequestResponse, body: T }> {
9 if (requestOptions.activityPub === true) { 11 if (requestOptions.activityPub === true) {
10 if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} 12 if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
11 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 13 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
12 } 14 }
13 15
14 return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => { 16 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
15 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 17 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
16 }) 18 })
17} 19}
@@ -27,9 +29,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
27 }) 29 })
28} 30}
29 31
32async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
33 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
35
36 const destPath = join(destDir, destName)
37 await processImage({ path: tmpPath }, destPath, size)
38}
39
30// --------------------------------------------------------------------------- 40// ---------------------------------------------------------------------------
31 41
32export { 42export {
33 doRequest, 43 doRequest,
34 doRequestAndSaveToFile 44 doRequestAndSaveToFile,
45 downloadImage
35} 46}
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 6228fec04..cb0e823c5 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -19,10 +19,7 @@ async function generateRandomString (size: number) {
19 return raw.toString('hex') 19 return raw.toString('hex')
20} 20}
21 21
22interface FormattableToJSON { 22interface FormattableToJSON { toFormattedJSON (args?: any) }
23 toFormattedJSON (args?: any)
24}
25
26function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { 23function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
27 const formattedObjects: U[] = [] 24 const formattedObjects: U[] = []
28 25
@@ -40,21 +37,24 @@ const getServerActor = memoizee(async function () {
40 const application = await ApplicationModel.load() 37 const application = await ApplicationModel.load()
41 if (!application) throw Error('Could not load Application from database.') 38 if (!application) throw Error('Could not load Application from database.')
42 39
43 return application.Account.Actor 40 const actor = application.Account.Actor
41 actor.Account = application.Account
42
43 return actor
44}) 44})
45 45
46function generateVideoTmpPath (target: string | ParseTorrent) { 46function generateVideoImportTmpPath (target: string | ParseTorrent) {
47 const id = typeof target === 'string' ? target : target.infoHash 47 const id = typeof target === 'string' ? target : target.infoHash
48 48
49 const hash = sha256(id) 49 const hash = sha256(id)
50 return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') 50 return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4')
51} 51}
52 52
53function getSecureTorrentName (originalName: string) { 53function getSecureTorrentName (originalName: string) {
54 return sha256(originalName) + '.torrent' 54 return sha256(originalName) + '.torrent'
55} 55}
56 56
57async function getVersion () { 57async function getServerCommit () {
58 try { 58 try {
59 const tag = await execPromise2( 59 const tag = await execPromise2(
60 '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true', 60 '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true',
@@ -74,7 +74,21 @@ async function getVersion () {
74 logger.debug('Cannot get version from git HEAD.', { err }) 74 logger.debug('Cannot get version from git HEAD.', { err })
75 } 75 }
76 76
77 return require('../../../package.json').version 77 return ''
78}
79
80/**
81 * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns
82 * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does
83 * not contain a UUID, returns null.
84 */
85function getUUIDFromFilename (filename: string) {
86 const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
87 const result = filename.match(regex)
88
89 if (!result || Array.isArray(result) === false) return null
90
91 return result[0]
78} 92}
79 93
80// --------------------------------------------------------------------------- 94// ---------------------------------------------------------------------------
@@ -85,6 +99,7 @@ export {
85 getFormattedObjects, 99 getFormattedObjects,
86 getSecureTorrentName, 100 getSecureTorrentName,
87 getServerActor, 101 getServerActor,
88 getVersion, 102 getServerCommit,
89 generateVideoTmpPath 103 generateVideoImportTmpPath,
104 getUUIDFromFilename
90} 105}
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index 1bd21467d..c90fe06c7 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -1,10 +1,12 @@
1import { VideoModel } from '../models/video/video' 1import { VideoModel } from '../models/video/video'
2 2
3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' 3type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
4 4
5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { 5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) 6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
7 7
8 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
9
8 if (fetchType === 'only-video') return VideoModel.load(id) 10 if (fetchType === 'only-video') return VideoModel.load(id)
9 11
10 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) 12 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index ce35b87da..3c9a0b96a 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -1,5 +1,5 @@
1import { logger } from './logger' 1import { logger } from './logger'
2import { generateVideoTmpPath } from './utils' 2import { generateVideoImportTmpPath } from './utils'
3import * as WebTorrent from 'webtorrent' 3import * as WebTorrent from 'webtorrent'
4import { createWriteStream, ensureDir, remove } from 'fs-extra' 4import { createWriteStream, ensureDir, remove } from 'fs-extra'
5import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers'
@@ -9,10 +9,10 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
9 const id = target.magnetUri || target.torrentName 9 const id = target.magnetUri || target.torrentName
10 let timer 10 let timer
11 11
12 const path = generateVideoTmpPath(id) 12 const path = generateVideoImportTmpPath(id)
13 logger.info('Importing torrent video %s', id) 13 logger.info('Importing torrent video %s', id)
14 14
15 const directoryPath = join(CONFIG.STORAGE.VIDEOS_DIR, 'import') 15 const directoryPath = join(CONFIG.STORAGE.TMP_DIR, 'webtorrent')
16 await ensureDir(directoryPath) 16 await ensureDir(directoryPath)
17 17
18 return new Promise<string>((res, rej) => { 18 return new Promise<string>((res, rej) => {
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index 70b4e1b78..b74351b42 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,7 +1,7 @@
1import { truncate } from 'lodash' 1import { truncate } from 'lodash'
2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' 2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers'
3import { logger } from './logger' 3import { logger } from './logger'
4import { generateVideoTmpPath } from './utils' 4import { generateVideoImportTmpPath } from './utils'
5import { join } from 'path' 5import { join } from 'path'
6import { root } from './core-utils' 6import { root } from './core-utils'
7import { ensureDir, writeFile, remove } from 'fs-extra' 7import { ensureDir, writeFile, remove } from 'fs-extra'
@@ -24,10 +24,10 @@ const processOptions = {
24 24
25function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { 25function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> {
26 return new Promise<YoutubeDLInfo>(async (res, rej) => { 26 return new Promise<YoutubeDLInfo>(async (res, rej) => {
27 const options = opts || [ '-j', '--flat-playlist' ] 27 const args = opts || [ '-j', '--flat-playlist' ]
28 28
29 const youtubeDL = await safeGetYoutubeDL() 29 const youtubeDL = await safeGetYoutubeDL()
30 youtubeDL.getInfo(url, options, (err, info) => { 30 youtubeDL.getInfo(url, args, processOptions, (err, info) => {
31 if (err) return rej(err) 31 if (err) return rej(err)
32 if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) 32 if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
33 33
@@ -40,7 +40,7 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo>
40} 40}
41 41
42function downloadYoutubeDLVideo (url: string, timeout: number) { 42function downloadYoutubeDLVideo (url: string, timeout: number) {
43 const path = generateVideoTmpPath(url) 43 const path = generateVideoImportTmpPath(url)
44 let timer 44 let timer
45 45
46 logger.info('Importing youtubeDL video %s', url) 46 logger.info('Importing youtubeDL video %s', url)