aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts28
-rw-r--r--server/controllers/api/accounts.ts4
-rw-r--r--server/controllers/api/config.ts12
-rw-r--r--server/controllers/api/users/index.ts1
-rw-r--r--server/controllers/api/users/me.ts6
-rw-r--r--server/controllers/api/video-channel.ts6
-rw-r--r--server/controllers/api/videos/import.ts6
-rw-r--r--server/controllers/api/videos/index.ts24
-rw-r--r--server/controllers/bots.ts101
-rw-r--r--server/controllers/index.ts1
-rw-r--r--server/controllers/static.ts15
-rw-r--r--server/helpers/express-utils.ts4
-rw-r--r--server/helpers/image-utils.ts14
-rw-r--r--server/helpers/requests.ts9
-rw-r--r--server/helpers/utils.ts12
-rw-r--r--server/helpers/webtorrent.ts6
-rw-r--r--server/helpers/youtube-dl.ts8
-rw-r--r--server/initializers/checker-before-init.ts1
-rw-r--r--server/initializers/constants.ts27
-rw-r--r--server/lib/activitypub/actor.ts4
-rw-r--r--server/lib/activitypub/process/process-create.ts17
-rw-r--r--server/lib/activitypub/process/process-like.ts5
-rw-r--r--server/lib/activitypub/process/process-update.ts3
-rw-r--r--server/lib/activitypub/videos.ts125
-rw-r--r--server/lib/emailer.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts41
-rw-r--r--server/lib/job-queue/handlers/video-file.ts4
-rw-r--r--server/lib/job-queue/handlers/video-import.ts9
-rw-r--r--server/lib/job-queue/handlers/video-views.ts15
-rw-r--r--server/lib/job-queue/job-queue.ts8
-rw-r--r--server/lib/redis.ts9
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts4
-rw-r--r--server/middlewares/validators/users.ts1
-rw-r--r--server/models/account/account.ts21
-rw-r--r--server/models/redundancy/video-redundancy.ts4
-rw-r--r--server/models/video/video-channel.ts21
-rw-r--r--server/models/video/video.ts36
-rw-r--r--server/tests/api/activitypub/client.ts40
-rw-r--r--server/tests/api/activitypub/fetch.ts7
-rw-r--r--server/tests/api/activitypub/helpers.ts10
-rw-r--r--server/tests/api/activitypub/index.ts1
-rw-r--r--server/tests/api/activitypub/refresher.ts93
-rw-r--r--server/tests/api/activitypub/security.ts7
-rw-r--r--server/tests/api/check-params/users.ts17
-rw-r--r--server/tests/api/redundancy/redundancy.ts27
-rw-r--r--server/tests/api/users/users.ts2
-rw-r--r--server/tests/cli/index.ts1
-rw-r--r--server/tests/misc-endpoints.ts82
-rw-r--r--server/tests/utils/miscs/sql.ts38
-rw-r--r--server/tests/utils/miscs/stubs.ts14
-rw-r--r--server/tests/utils/requests/activitypub.ts43
-rw-r--r--server/tools/peertube-repl.ts7
52 files changed, 688 insertions, 320 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index d9d385460..1a4e28dc8 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -162,10 +162,10 @@ function getAccountVideoRate (rateType: VideoRateType) {
162 } 162 }
163} 163}
164 164
165async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 165async function videoController (req: express.Request, res: express.Response) {
166 const video: VideoModel = res.locals.video 166 const video: VideoModel = res.locals.video
167 167
168 if (video.isOwned() === false) return res.redirect(video.url) 168 if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url)
169 169
170 // We need captions to render AP object 170 // We need captions to render AP object
171 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) 171 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
@@ -181,17 +181,17 @@ async function videoController (req: express.Request, res: express.Response, nex
181 return activityPubResponse(activityPubContextify(videoObject), res) 181 return activityPubResponse(activityPubContextify(videoObject), res)
182} 182}
183 183
184async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { 184async function videoAnnounceController (req: express.Request, res: express.Response) {
185 const share = res.locals.videoShare as VideoShareModel 185 const share = res.locals.videoShare as VideoShareModel
186 186
187 if (share.Actor.isOwned() === false) return res.redirect(share.url) 187 if (share.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(share.url)
188 188
189 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) 189 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
190 190
191 return activityPubResponse(activityPubContextify(activity), res) 191 return activityPubResponse(activityPubContextify(activity), res)
192} 192}
193 193
194async function videoAnnouncesController (req: express.Request, res: express.Response, next: express.NextFunction) { 194async function videoAnnouncesController (req: express.Request, res: express.Response) {
195 const video: VideoModel = res.locals.video 195 const video: VideoModel = res.locals.video
196 196
197 const handler = async (start: number, count: number) => { 197 const handler = async (start: number, count: number) => {
@@ -206,21 +206,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
206 return activityPubResponse(activityPubContextify(json), res) 206 return activityPubResponse(activityPubContextify(json), res)
207} 207}
208 208
209async function videoLikesController (req: express.Request, res: express.Response, next: express.NextFunction) { 209async function videoLikesController (req: express.Request, res: express.Response) {
210 const video: VideoModel = res.locals.video 210 const video: VideoModel = res.locals.video
211 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) 211 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
212 212
213 return activityPubResponse(activityPubContextify(json), res) 213 return activityPubResponse(activityPubContextify(json), res)
214} 214}
215 215
216async function videoDislikesController (req: express.Request, res: express.Response, next: express.NextFunction) { 216async function videoDislikesController (req: express.Request, res: express.Response) {
217 const video: VideoModel = res.locals.video 217 const video: VideoModel = res.locals.video
218 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) 218 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
219 219
220 return activityPubResponse(activityPubContextify(json), res) 220 return activityPubResponse(activityPubContextify(json), res)
221} 221}
222 222
223async function videoCommentsController (req: express.Request, res: express.Response, next: express.NextFunction) { 223async function videoCommentsController (req: express.Request, res: express.Response) {
224 const video: VideoModel = res.locals.video 224 const video: VideoModel = res.locals.video
225 225
226 const handler = async (start: number, count: number) => { 226 const handler = async (start: number, count: number) => {
@@ -235,30 +235,30 @@ async function videoCommentsController (req: express.Request, res: express.Respo
235 return activityPubResponse(activityPubContextify(json), res) 235 return activityPubResponse(activityPubContextify(json), res)
236} 236}
237 237
238async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) { 238async function videoChannelController (req: express.Request, res: express.Response) {
239 const videoChannel: VideoChannelModel = res.locals.videoChannel 239 const videoChannel: VideoChannelModel = res.locals.videoChannel
240 240
241 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res) 241 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res)
242} 242}
243 243
244async function videoChannelFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) { 244async function videoChannelFollowersController (req: express.Request, res: express.Response) {
245 const videoChannel: VideoChannelModel = res.locals.videoChannel 245 const videoChannel: VideoChannelModel = res.locals.videoChannel
246 const activityPubResult = await actorFollowers(req, videoChannel.Actor) 246 const activityPubResult = await actorFollowers(req, videoChannel.Actor)
247 247
248 return activityPubResponse(activityPubContextify(activityPubResult), res) 248 return activityPubResponse(activityPubContextify(activityPubResult), res)
249} 249}
250 250
251async function videoChannelFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) { 251async function videoChannelFollowingController (req: express.Request, res: express.Response) {
252 const videoChannel: VideoChannelModel = res.locals.videoChannel 252 const videoChannel: VideoChannelModel = res.locals.videoChannel
253 const activityPubResult = await actorFollowing(req, videoChannel.Actor) 253 const activityPubResult = await actorFollowing(req, videoChannel.Actor)
254 254
255 return activityPubResponse(activityPubContextify(activityPubResult), res) 255 return activityPubResponse(activityPubContextify(activityPubResult), res)
256} 256}
257 257
258async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { 258async function videoCommentController (req: express.Request, res: express.Response) {
259 const videoComment: VideoCommentModel = res.locals.videoComment 259 const videoComment: VideoCommentModel = res.locals.videoComment
260 260
261 if (videoComment.isOwned() === false) return res.redirect(videoComment.url) 261 if (videoComment.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoComment.url)
262 262
263 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 263 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
264 const isPublic = true // Comments are always public 264 const isPublic = true // Comments are always public
@@ -276,7 +276,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
276 276
277async function videoRedundancyController (req: express.Request, res: express.Response) { 277async function videoRedundancyController (req: express.Request, res: express.Response) {
278 const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy 278 const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy
279 if (videoRedundancy.isOwned() === false) return res.redirect(videoRedundancy.url) 279 if (videoRedundancy.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoRedundancy.url)
280 280
281 const serverActor = await getServerActor() 281 const serverActor = await getServerActor()
282 282
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 86ef2aed1..a69a83acf 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -74,10 +74,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp
74 74
75async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 75async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
76 const account: AccountModel = res.locals.account 76 const account: AccountModel = res.locals.account
77 const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 77 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
78 78
79 const resultList = await VideoModel.listForApi({ 79 const resultList = await VideoModel.listForApi({
80 actorId, 80 followerActorId,
81 start: req.query.start, 81 start: req.query.start,
82 count: req.query.count, 82 count: req.query.count,
83 sort: req.query.sort, 83 sort: req.query.sort,
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 03c1cec7b..d65e321e9 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -10,7 +10,8 @@ import { customConfigUpdateValidator } from '../../middlewares/validators/config
10import { ClientHtml } from '../../lib/client-html' 10import { ClientHtml } from '../../lib/client-html'
11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' 11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
12import { remove, writeJSON } from 'fs-extra' 12import { remove, writeJSON } from 'fs-extra'
13import { getVersion } from '../../helpers/utils' 13import { getServerCommit } from '../../helpers/utils'
14import { Emailer } from '../../lib/emailer'
14 15
15const packageJSON = require('../../../../package.json') 16const packageJSON = require('../../../../package.json')
16const configRouter = express.Router() 17const configRouter = express.Router()
@@ -40,11 +41,11 @@ configRouter.delete('/custom',
40) 41)
41 42
42let serverCommit: string 43let serverCommit: string
43async function getConfig (req: express.Request, res: express.Response, next: express.NextFunction) { 44async function getConfig (req: express.Request, res: express.Response) {
44 const allowed = await isSignupAllowed() 45 const allowed = await isSignupAllowed()
45 const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip) 46 const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip)
46 serverCommit = (serverCommit) ? serverCommit : await getVersion() 47
47 if (serverCommit === packageJSON.version) serverCommit = '' 48 if (serverCommit === undefined) serverCommit = await getServerCommit()
48 49
49 const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) 50 const enabledResolutions = Object.keys(CONFIG.TRANSCODING.RESOLUTIONS)
50 .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true) 51 .filter(key => CONFIG.TRANSCODING.ENABLED === CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
@@ -61,6 +62,9 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
61 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 62 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
62 } 63 }
63 }, 64 },
65 email: {
66 enabled: Emailer.Instance.isEnabled()
67 },
64 serverVersion: packageJSON.version, 68 serverVersion: packageJSON.version,
65 serverCommit, 69 serverCommit,
66 signup: { 70 signup: {
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 9fcb8077f..87fab4a40 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -262,6 +262,7 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
262 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role 262 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
263 263
264 if (body.email !== undefined) userToUpdate.email = body.email 264 if (body.email !== undefined) userToUpdate.email = body.email
265 if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
265 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota 266 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
266 if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily 267 if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
267 if (body.role !== undefined) userToUpdate.role = body.role 268 if (body.role !== undefined) userToUpdate.role = body.role
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 82299747d..d2456346b 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -42,7 +42,7 @@ import { AccountModel } from '../../../models/account/account'
42 42
43const auditLogger = auditLoggerFactory('users-me') 43const auditLogger = auditLoggerFactory('users-me')
44 44
45const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 45const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
46 46
47const meRouter = express.Router() 47const meRouter = express.Router()
48 48
@@ -238,7 +238,7 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
238 nsfw: buildNSFWFilter(res, req.query.nsfw), 238 nsfw: buildNSFWFilter(res, req.query.nsfw),
239 filter: req.query.filter as VideoFilter, 239 filter: req.query.filter as VideoFilter,
240 withFiles: false, 240 withFiles: false,
241 actorId: user.Account.Actor.id, 241 followerActorId: user.Account.Actor.id,
242 user 242 user
243 }) 243 })
244 244
@@ -348,7 +348,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
348 return res.sendStatus(204) 348 return res.sendStatus(204)
349} 349}
350 350
351async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 351async function updateMyAvatar (req: express.Request, res: express.Response) {
352 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 352 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
353 const user: UserModel = res.locals.oauth.token.user 353 const user: UserModel = res.locals.oauth.token.user
354 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 354 const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 9bf3c5fd8..fd143a139 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -32,7 +32,7 @@ import { resetSequelizeInstance } from '../../helpers/database-utils'
32import { UserModel } from '../../models/account/user' 32import { UserModel } from '../../models/account/user'
33 33
34const auditLogger = auditLoggerFactory('channels') 34const auditLogger = auditLoggerFactory('channels')
35const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 35const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
36 36
37const videoChannelRouter = express.Router() 37const videoChannelRouter = express.Router()
38 38
@@ -202,10 +202,10 @@ async function getVideoChannel (req: express.Request, res: express.Response, nex
202 202
203async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 203async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
204 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel 204 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
205 const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 205 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
206 206
207 const resultList = await VideoModel.listForApi({ 207 const resultList = await VideoModel.listForApi({
208 actorId, 208 followerActorId,
209 start: req.query.start, 209 start: req.query.start,
210 count: req.query.count, 210 count: req.query.count,
211 sort: req.query.sort, 211 sort: req.query.sort,
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 398fd5a7f..f27d648c7 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -37,9 +37,9 @@ const reqVideoFileImport = createReqFiles(
37 [ 'thumbnailfile', 'previewfile', 'torrentfile' ], 37 [ 'thumbnailfile', 'previewfile', 'torrentfile' ],
38 Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 38 Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
39 { 39 {
40 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 40 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
41 previewfile: CONFIG.STORAGE.PREVIEWS_DIR, 41 previewfile: CONFIG.STORAGE.TMP_DIR,
42 torrentfile: CONFIG.STORAGE.TORRENTS_DIR 42 torrentfile: CONFIG.STORAGE.TMP_DIR
43 } 43 }
44) 44)
45 45
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 89fd0432f..4e4697ef4 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -67,17 +67,17 @@ const reqVideoFileAdd = createReqFiles(
67 [ 'videofile', 'thumbnailfile', 'previewfile' ], 67 [ 'videofile', 'thumbnailfile', 'previewfile' ],
68 Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), 68 Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT),
69 { 69 {
70 videofile: CONFIG.STORAGE.VIDEOS_DIR, 70 videofile: CONFIG.STORAGE.TMP_DIR,
71 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 71 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
72 previewfile: CONFIG.STORAGE.PREVIEWS_DIR 72 previewfile: CONFIG.STORAGE.TMP_DIR
73 } 73 }
74) 74)
75const reqVideoFileUpdate = createReqFiles( 75const reqVideoFileUpdate = createReqFiles(
76 [ 'thumbnailfile', 'previewfile' ], 76 [ 'thumbnailfile', 'previewfile' ],
77 IMAGE_MIMETYPE_EXT, 77 IMAGE_MIMETYPE_EXT,
78 { 78 {
79 thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, 79 thumbnailfile: CONFIG.STORAGE.TMP_DIR,
80 previewfile: CONFIG.STORAGE.PREVIEWS_DIR 80 previewfile: CONFIG.STORAGE.TMP_DIR
81 } 81 }
82) 82)
83 83
@@ -387,6 +387,11 @@ async function updateVideo (req: express.Request, res: express.Response) {
387function getVideo (req: express.Request, res: express.Response) { 387function getVideo (req: express.Request, res: express.Response) {
388 const videoInstance = res.locals.video 388 const videoInstance = res.locals.video
389 389
390 if (videoInstance.isOutdated()) {
391 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoInstance.url } })
392 .catch(err => logger.error('Cannot create AP refresher job for video %s.', videoInstance.url, { err }))
393 }
394
390 return res.json(videoInstance.toFormattedDetailsJSON()) 395 return res.json(videoInstance.toFormattedDetailsJSON())
391} 396}
392 397
@@ -406,12 +411,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
406 ]) 411 ])
407 412
408 const serverActor = await getServerActor() 413 const serverActor = await getServerActor()
409 414 await sendCreateView(serverActor, videoInstance, undefined)
410 // Send the event to the origin server
411 // If we own the video, we'll send an update event when we'll process the views (in our job queue)
412 if (videoInstance.isOwned() === false) {
413 await sendCreateView(serverActor, videoInstance, undefined)
414 }
415 415
416 return res.status(204).end() 416 return res.status(204).end()
417} 417}
@@ -429,7 +429,7 @@ async function getVideoDescription (req: express.Request, res: express.Response)
429 return res.json({ description }) 429 return res.json({ description })
430} 430}
431 431
432async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 432async function listVideos (req: express.Request, res: express.Response) {
433 const resultList = await VideoModel.listForApi({ 433 const resultList = await VideoModel.listForApi({
434 start: req.query.start, 434 start: req.query.start,
435 count: req.query.count, 435 count: req.query.count,
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
new file mode 100644
index 000000000..2db86a2d8
--- /dev/null
+++ b/server/controllers/bots.ts
@@ -0,0 +1,101 @@
1import * as express from 'express'
2import { asyncMiddleware } from '../middlewares'
3import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers'
4import * as sitemapModule from 'sitemap'
5import { logger } from '../helpers/logger'
6import { VideoModel } from '../models/video/video'
7import { VideoChannelModel } from '../models/video/video-channel'
8import { AccountModel } from '../models/account/account'
9import { cacheRoute } from '../middlewares/cache'
10import { buildNSFWFilter } from '../helpers/express-utils'
11import { truncate } from 'lodash'
12
13const botsRouter = express.Router()
14
15// Special route that add OpenGraph and oEmbed tags
16// Do not use a template engine for a so little thing
17botsRouter.use('/sitemap.xml',
18 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)),
19 asyncMiddleware(getSitemap)
20)
21
22// ---------------------------------------------------------------------------
23
24export {
25 botsRouter
26}
27
28// ---------------------------------------------------------------------------
29
30async function getSitemap (req: express.Request, res: express.Response) {
31 let urls = getSitemapBasicUrls()
32
33 urls = urls.concat(await getSitemapLocalVideoUrls())
34 urls = urls.concat(await getSitemapVideoChannelUrls())
35 urls = urls.concat(await getSitemapAccountUrls())
36
37 const sitemap = sitemapModule.createSitemap({
38 hostname: CONFIG.WEBSERVER.URL,
39 urls: urls
40 })
41
42 sitemap.toXML((err, xml) => {
43 if (err) {
44 logger.error('Cannot generate sitemap.', { err })
45 return res.sendStatus(500)
46 }
47
48 res.header('Content-Type', 'application/xml')
49 res.send(xml)
50 })
51}
52
53async function getSitemapVideoChannelUrls () {
54 const rows = await VideoChannelModel.listLocalsForSitemap('createdAt')
55
56 return rows.map(channel => ({
57 url: CONFIG.WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername
58 }))
59}
60
61async function getSitemapAccountUrls () {
62 const rows = await AccountModel.listLocalsForSitemap('createdAt')
63
64 return rows.map(channel => ({
65 url: CONFIG.WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername
66 }))
67}
68
69async function getSitemapLocalVideoUrls () {
70 const resultList = await VideoModel.listForApi({
71 start: 0,
72 count: undefined,
73 sort: 'createdAt',
74 includeLocalVideos: true,
75 nsfw: buildNSFWFilter(),
76 filter: 'local',
77 withFiles: false
78 })
79
80 return resultList.data.map(v => ({
81 url: CONFIG.WEBSERVER.URL + '/videos/watch/' + v.uuid,
82 video: [
83 {
84 title: v.name,
85 // Sitemap description should be < 2000 characters
86 description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
87 player_loc: CONFIG.WEBSERVER.URL + '/videos/embed/' + v.uuid,
88 thumbnail_loc: CONFIG.WEBSERVER.URL + v.getThumbnailStaticPath()
89 }
90 ]
91 }))
92}
93
94function getSitemapBasicUrls () {
95 const paths = [
96 '/about/instance',
97 '/videos/local'
98 ]
99
100 return paths.map(p => ({ url: CONFIG.WEBSERVER.URL + p }))
101}
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 197fa897a..a88a03c79 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -6,3 +6,4 @@ export * from './services'
6export * from './static' 6export * from './static'
7export * from './webfinger' 7export * from './webfinger'
8export * from './tracker' 8export * from './tracker'
9export * from './bots'
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 75e30353c..4fd58f70c 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -34,13 +34,18 @@ staticRouter.use(
34) 34)
35 35
36// Videos path for webseeding 36// Videos path for webseeding
37const videosPhysicalPath = CONFIG.STORAGE.VIDEOS_DIR
38staticRouter.use( 37staticRouter.use(
39 STATIC_PATHS.WEBSEED, 38 STATIC_PATHS.WEBSEED,
40 cors(), 39 cors(),
41 express.static(videosPhysicalPath) 40 express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video
42) 41)
43staticRouter.use( 42staticRouter.use(
43 STATIC_PATHS.REDUNDANCY,
44 cors(),
45 express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video
46)
47
48staticRouter.use(
44 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 49 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
45 asyncMiddleware(videosGetValidator), 50 asyncMiddleware(videosGetValidator),
46 asyncMiddleware(downloadVideoFile) 51 asyncMiddleware(downloadVideoFile)
@@ -131,6 +136,12 @@ staticRouter.use('/.well-known/dnt/',
131 } 136 }
132) 137)
133 138
139staticRouter.use('/.well-known/change-password',
140 (_, res: express.Response) => {
141 res.redirect('/my-account/settings')
142 }
143)
144
134// --------------------------------------------------------------------------- 145// ---------------------------------------------------------------------------
135 146
136export { 147export {
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 162fe2244..9a72ee96d 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -7,12 +7,12 @@ import { extname } from 'path'
7import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
8import { UserModel } from '../models/account/user' 8import { UserModel } from '../models/account/user'
9 9
10function buildNSFWFilter (res: express.Response, paramNSFW?: string) { 10function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
11 if (paramNSFW === 'true') return true 11 if (paramNSFW === 'true') return true
12 if (paramNSFW === 'false') return false 12 if (paramNSFW === 'false') return false
13 if (paramNSFW === 'both') return undefined 13 if (paramNSFW === 'both') return undefined
14 14
15 if (res.locals.oauth) { 15 if (res && res.locals.oauth) {
16 const user: UserModel = res.locals.oauth.token.User 16 const user: UserModel = res.locals.oauth.token.User
17 17
18 // User does not want NSFW videos 18 // User does not want NSFW videos
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index da3285b13..e43ea3f1d 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -1,6 +1,7 @@
1import 'multer' 1import 'multer'
2import * as sharp from 'sharp' 2import * as sharp from 'sharp'
3import { move, 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 },
@@ -11,14 +12,11 @@ async function processImage (
11 throw new Error('Sharp needs an input path different that the output path.') 12 throw new Error('Sharp needs an input path different that the output path.')
12 } 13 }
13 14
14 const sharpInstance = sharp(physicalFile.path) 15 logger.debug('Processing image %s to %s.', physicalFile.path, destination)
15 const metadata = await sharpInstance.metadata()
16 16
17 // No need to resize 17 // Avoid sharp cache
18 if (metadata.width === newSize.width && metadata.height === newSize.height) { 18 const buf = await readFile(physicalFile.path)
19 await move(physicalFile.path, destination, { overwrite: true }) 19 const sharpInstance = sharp(buf)
20 return
21 }
22 20
23 await remove(destination) 21 await remove(destination)
24 22
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 805930a9f..3fc776f1a 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,8 +1,9 @@
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' 5import { processImage } from './image-utils'
6import { join } from 'path'
6 7
7function doRequest <T> ( 8function doRequest <T> (
8 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 9 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
@@ -28,11 +29,11 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
28 }) 29 })
29} 30}
30 31
31async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) { 32async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
32 const tmpPath = destPath + '.tmp' 33 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
33
34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) 34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
35 35
36 const destPath = join(destDir, destName)
36 await processImage({ path: tmpPath }, destPath, size) 37 await processImage({ path: tmpPath }, destPath, size)
37} 38}
38 39
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 049c3f8bc..9b89e3e61 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -46,18 +46,18 @@ const getServerActor = memoizee(async function () {
46 return actor 46 return actor
47}) 47})
48 48
49function generateVideoTmpPath (target: string | ParseTorrent) { 49function generateVideoImportTmpPath (target: string | ParseTorrent) {
50 const id = typeof target === 'string' ? target : target.infoHash 50 const id = typeof target === 'string' ? target : target.infoHash
51 51
52 const hash = sha256(id) 52 const hash = sha256(id)
53 return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') 53 return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4')
54} 54}
55 55
56function getSecureTorrentName (originalName: string) { 56function getSecureTorrentName (originalName: string) {
57 return sha256(originalName) + '.torrent' 57 return sha256(originalName) + '.torrent'
58} 58}
59 59
60async function getVersion () { 60async function getServerCommit () {
61 try { 61 try {
62 const tag = await execPromise2( 62 const tag = await execPromise2(
63 '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true', 63 '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true',
@@ -77,7 +77,7 @@ async function getVersion () {
77 logger.debug('Cannot get version from git HEAD.', { err }) 77 logger.debug('Cannot get version from git HEAD.', { err })
78 } 78 }
79 79
80 return require('../../../package.json').version 80 return ''
81} 81}
82 82
83/** 83/**
@@ -102,7 +102,7 @@ export {
102 getFormattedObjects, 102 getFormattedObjects,
103 getSecureTorrentName, 103 getSecureTorrentName,
104 getServerActor, 104 getServerActor,
105 getVersion, 105 getServerCommit,
106 generateVideoTmpPath, 106 generateVideoImportTmpPath,
107 getUUIDFromFilename 107 getUUIDFromFilename
108} 108}
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)
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 9dfb5d68c..b51c7cfba 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -12,6 +12,7 @@ function checkMissedConfig () {
12 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', 12 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max',
13 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', 13 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
14 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', 14 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache',
15 'storage.redundancy', 'storage.tmp',
15 'log.level', 16 'log.level',
16 'user.video_quota', 'user.video_quota_daily', 17 'user.video_quota', 'user.video_quota_daily',
17 'cache.previews.size', 'admin.email', 18 'cache.previews.size', 'admin.email',
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index ae3d671bb..d4496bc34 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -61,6 +61,7 @@ const OAUTH_LIFETIME = {
61const ROUTE_CACHE_LIFETIME = { 61const ROUTE_CACHE_LIFETIME = {
62 FEEDS: '15 minutes', 62 FEEDS: '15 minutes',
63 ROBOTS: '2 hours', 63 ROBOTS: '2 hours',
64 SITEMAP: '1 day',
64 SECURITYTXT: '2 hours', 65 SECURITYTXT: '2 hours',
65 NODEINFO: '10 minutes', 66 NODEINFO: '10 minutes',
66 DNT_POLICY: '1 week', 67 DNT_POLICY: '1 week',
@@ -102,7 +103,8 @@ const JOB_ATTEMPTS: { [ id in JobType ]: number } = {
102 'video-file': 1, 103 'video-file': 1,
103 'video-import': 1, 104 'video-import': 1,
104 'email': 5, 105 'email': 5,
105 'videos-views': 1 106 'videos-views': 1,
107 'activitypub-refresher': 1
106} 108}
107const JOB_CONCURRENCY: { [ id in JobType ]: number } = { 109const JOB_CONCURRENCY: { [ id in JobType ]: number } = {
108 'activitypub-http-broadcast': 1, 110 'activitypub-http-broadcast': 1,
@@ -113,7 +115,8 @@ const JOB_CONCURRENCY: { [ id in JobType ]: number } = {
113 'video-file': 1, 115 'video-file': 1,
114 'video-import': 1, 116 'video-import': 1,
115 'email': 5, 117 'email': 5,
116 'videos-views': 1 118 'videos-views': 1,
119 'activitypub-refresher': 1
117} 120}
118const JOB_TTL: { [ id in JobType ]: number } = { 121const JOB_TTL: { [ id in JobType ]: number } = {
119 'activitypub-http-broadcast': 60000 * 10, // 10 minutes 122 'activitypub-http-broadcast': 60000 * 10, // 10 minutes
@@ -124,11 +127,12 @@ const JOB_TTL: { [ id in JobType ]: number } = {
124 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long 127 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long
125 'video-import': 1000 * 3600 * 2, // hours 128 'video-import': 1000 * 3600 * 2, // hours
126 'email': 60000 * 10, // 10 minutes 129 'email': 60000 * 10, // 10 minutes
127 'videos-views': undefined // Unlimited 130 'videos-views': undefined, // Unlimited
131 'activitypub-refresher': 60000 * 10 // 10 minutes
128} 132}
129const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { 133const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = {
130 'videos-views': { 134 'videos-views': {
131 cron: '1 * * * *' // At 1 minutes past the hour 135 cron: '1 * * * *' // At 1 minute past the hour
132 } 136 }
133} 137}
134 138
@@ -182,9 +186,11 @@ const CONFIG = {
182 FROM_ADDRESS: config.get<string>('smtp.from_address') 186 FROM_ADDRESS: config.get<string>('smtp.from_address')
183 }, 187 },
184 STORAGE: { 188 STORAGE: {
189 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
185 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), 190 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')),
186 LOG_DIR: buildPath(config.get<string>('storage.logs')), 191 LOG_DIR: buildPath(config.get<string>('storage.logs')),
187 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), 192 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
193 REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')),
188 THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), 194 THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')),
189 PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), 195 PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')),
190 CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), 196 CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')),
@@ -294,9 +300,9 @@ const CONFIG = {
294 300
295const CONSTRAINTS_FIELDS = { 301const CONSTRAINTS_FIELDS = {
296 USERS: { 302 USERS: {
297 NAME: { min: 3, max: 120 }, // Length 303 NAME: { min: 1, max: 50 }, // Length
298 DESCRIPTION: { min: 3, max: 1000 }, // Length 304 DESCRIPTION: { min: 3, max: 1000 }, // Length
299 USERNAME: { min: 3, max: 20 }, // Length 305 USERNAME: { min: 1, max: 50 }, // Length
300 PASSWORD: { min: 6, max: 255 }, // Length 306 PASSWORD: { min: 6, max: 255 }, // Length
301 VIDEO_QUOTA: { min: -1 }, 307 VIDEO_QUOTA: { min: -1 },
302 VIDEO_QUOTA_DAILY: { min: -1 }, 308 VIDEO_QUOTA_DAILY: { min: -1 },
@@ -310,7 +316,7 @@ const CONSTRAINTS_FIELDS = {
310 REASON: { min: 2, max: 300 } // Length 316 REASON: { min: 2, max: 300 } // Length
311 }, 317 },
312 VIDEO_CHANNELS: { 318 VIDEO_CHANNELS: {
313 NAME: { min: 3, max: 120 }, // Length 319 NAME: { min: 1, max: 50 }, // Length
314 DESCRIPTION: { min: 3, max: 1000 }, // Length 320 DESCRIPTION: { min: 3, max: 1000 }, // Length
315 SUPPORT: { min: 3, max: 1000 }, // Length 321 SUPPORT: { min: 3, max: 1000 }, // Length
316 URL: { min: 3, max: 2000 } // Length 322 URL: { min: 3, max: 2000 } // Length
@@ -543,7 +549,7 @@ const HTTP_SIGNATURE = {
543 549
544// --------------------------------------------------------------------------- 550// ---------------------------------------------------------------------------
545 551
546const PRIVATE_RSA_KEY_SIZE = 2048 552let PRIVATE_RSA_KEY_SIZE = 2048
547 553
548// Password encryption 554// Password encryption
549const BCRYPT_SALT_SIZE = 10 555const BCRYPT_SALT_SIZE = 10
@@ -566,6 +572,7 @@ const STATIC_PATHS = {
566 THUMBNAILS: '/static/thumbnails/', 572 THUMBNAILS: '/static/thumbnails/',
567 TORRENTS: '/static/torrents/', 573 TORRENTS: '/static/torrents/',
568 WEBSEED: '/static/webseed/', 574 WEBSEED: '/static/webseed/',
575 REDUNDANCY: '/static/redundancy/',
569 AVATARS: '/static/avatars/', 576 AVATARS: '/static/avatars/',
570 VIDEO_CAPTIONS: '/static/video-captions/' 577 VIDEO_CAPTIONS: '/static/video-captions/'
571} 578}
@@ -647,6 +654,8 @@ const TRACKER_RATE_LIMITS = {
647 654
648// Special constants for a test instance 655// Special constants for a test instance
649if (isTestInstance() === true) { 656if (isTestInstance() === true) {
657 PRIVATE_RSA_KEY_SIZE = 1024
658
650 ACTOR_FOLLOW_SCORE.BASE = 20 659 ACTOR_FOLLOW_SCORE.BASE = 20
651 660
652 REMOTE_SCHEME.HTTP = 'http' 661 REMOTE_SCHEME.HTTP = 'http'
@@ -768,7 +777,7 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancy[] {
768 if (!objs) return [] 777 if (!objs) return []
769 778
770 return objs.map(obj => { 779 return objs.map(obj => {
771 return Object.assign(obj, { 780 return Object.assign({}, obj, {
772 minLifetime: parseDuration(obj.min_lifetime), 781 minLifetime: parseDuration(obj.min_lifetime),
773 size: bytes.parse(obj.size), 782 size: bytes.parse(obj.size),
774 minViews: obj.min_views 783 minViews: obj.min_views
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 504263c99..bbe48833d 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -178,9 +178,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] 178 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
179 179
180 const avatarName = uuidv4() + extension 180 const avatarName = uuidv4() + extension
181 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 181 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
182
183 await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
184 182
185 return avatarName 183 return avatarName
186 } 184 }
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 9a72cb899..df05ee452 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -12,9 +12,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { forwardVideoRelatedActivity } from '../send/utils'
13import { Redis } from '../../redis' 13import { Redis } from '../../redis'
14import { createOrUpdateCacheFile } from '../cache-file' 14import { createOrUpdateCacheFile } from '../cache-file'
15import { immutableAssign } from '../../../../shared/utils'
16import { getVideoDislikeActivityPubUrl } from '../url' 15import { getVideoDislikeActivityPubUrl } from '../url'
17import { VideoModel } from '../../../models/video/video'
18 16
19async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 17async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
20 const activityObject = activity.object 18 const activityObject = activity.object
@@ -71,7 +69,7 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
71 69
72 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 70 const [ , created ] = await AccountVideoRateModel.findOrCreate({
73 where: rate, 71 where: rate,
74 defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }), 72 defaults: Object.assign({}, rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
75 transaction: t 73 transaction: t
76 }) 74 })
77 if (created === true) await video.increment('dislikes', { transaction: t }) 75 if (created === true) await video.increment('dislikes', { transaction: t })
@@ -88,10 +86,19 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
88async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { 86async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
89 const view = activity.object as ViewObject 87 const view = activity.object as ViewObject
90 88
91 const video = await VideoModel.loadByUrl(view.object) 89 const options = {
92 if (!video || video.isOwned() === false) return 90 videoObject: view.object,
91 fetchType: 'only-video' as 'only-video'
92 }
93 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
93 94
94 await Redis.Instance.addVideoView(video.id) 95 await Redis.Instance.addVideoView(video.id)
96
97 if (video.isOwned()) {
98 // Don't resend the activity to the sender
99 const exceptions = [ byActor ]
100 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
101 }
95} 102}
96 103
97async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { 104async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) {
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index be86665e9..e8e97eece 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -5,8 +5,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { immutableAssign } from '../../../../shared/utils' 8import { getVideoLikeActivityPubUrl } from '../url'
9import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
10 9
11async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { 10async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
12 return retryTransactionWrapper(processLikeVideo, byActor, activity) 11 return retryTransactionWrapper(processLikeVideo, byActor, activity)
@@ -36,7 +35,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
36 } 35 }
37 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 36 const [ , created ] = await AccountVideoRateModel.findOrCreate({
38 where: rate, 37 where: rate,
39 defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }), 38 defaults: Object.assign({}, rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
40 transaction: t 39 transaction: t
41 }) 40 })
42 if (created === true) await video.increment('likes', { transaction: t }) 41 if (created === true) await video.increment('likes', { transaction: t })
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index bd4013555..c6b42d846 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -51,7 +51,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
51 return undefined 51 return undefined
52 } 52 }
53 53
54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id }) 54 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false })
55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) 55 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
56 56
57 const updateOptions = { 57 const updateOptions = {
@@ -59,7 +59,6 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate)
59 videoObject, 59 videoObject,
60 account: actor.Account, 60 account: actor.Account,
61 channel: channelActor.VideoChannel, 61 channel: channelActor.VideoChannel,
62 updateViews: true,
63 overrideTo: activity.to 62 overrideTo: activity.to
64 } 63 }
65 return updateVideoFromAP(updateOptions) 64 return updateVideoFromAP(updateOptions)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 4cecf9345..3d17e6846 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -95,9 +95,8 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu
95 95
96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) {
97 const thumbnailName = video.getThumbnailName() 97 const thumbnailName = video.getThumbnailName()
98 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
99 98
100 return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE) 99 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
101} 100}
102 101
103function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 102function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
@@ -117,7 +116,7 @@ type SyncParam = {
117 shares: boolean 116 shares: boolean
118 comments: boolean 117 comments: boolean
119 thumbnail: boolean 118 thumbnail: boolean
120 refreshVideo: boolean 119 refreshVideo?: boolean
121} 120}
122async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { 121async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
123 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) 122 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
@@ -159,26 +158,29 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
159 videoObject: VideoTorrentObject | string, 158 videoObject: VideoTorrentObject | string,
160 syncParam?: SyncParam, 159 syncParam?: SyncParam,
161 fetchType?: VideoFetchByUrlType, 160 fetchType?: VideoFetchByUrlType,
162 refreshViews?: boolean 161 allowRefresh?: boolean // true by default
163}) { 162}) {
164 // Default params 163 // Default params
165 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 164 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
166 const fetchType = options.fetchType || 'all' 165 const fetchType = options.fetchType || 'all'
167 const refreshViews = options.refreshViews || false 166 const allowRefresh = options.allowRefresh !== false
168 167
169 // Get video url 168 // Get video url
170 const videoUrl = getAPUrl(options.videoObject) 169 const videoUrl = getAPUrl(options.videoObject)
171 170
172 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 171 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
173 if (videoFromDatabase) { 172 if (videoFromDatabase) {
174 const refreshOptions = { 173
175 video: videoFromDatabase, 174 if (allowRefresh === true) {
176 fetchedType: fetchType, 175 const refreshOptions = {
177 syncParam, 176 video: videoFromDatabase,
178 refreshViews 177 fetchedType: fetchType,
178 syncParam
179 }
180
181 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
182 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } })
179 } 183 }
180 const p = refreshVideoIfNeeded(refreshOptions)
181 if (syncParam.refreshVideo === true) videoFromDatabase = await p
182 184
183 return { video: videoFromDatabase } 185 return { video: videoFromDatabase }
184 } 186 }
@@ -199,7 +201,6 @@ async function updateVideoFromAP (options: {
199 videoObject: VideoTorrentObject, 201 videoObject: VideoTorrentObject,
200 account: AccountModel, 202 account: AccountModel,
201 channel: VideoChannelModel, 203 channel: VideoChannelModel,
202 updateViews: boolean,
203 overrideTo?: string[] 204 overrideTo?: string[]
204}) { 205}) {
205 logger.debug('Updating remote video "%s".', options.videoObject.uuid) 206 logger.debug('Updating remote video "%s".', options.videoObject.uuid)
@@ -238,8 +239,8 @@ async function updateVideoFromAP (options: {
238 options.video.set('publishedAt', videoData.publishedAt) 239 options.video.set('publishedAt', videoData.publishedAt)
239 options.video.set('privacy', videoData.privacy) 240 options.video.set('privacy', videoData.privacy)
240 options.video.set('channelId', videoData.channelId) 241 options.video.set('channelId', videoData.channelId)
242 options.video.set('views', videoData.views)
241 243
242 if (options.updateViews === true) options.video.set('views', videoData.views)
243 await options.video.save(sequelizeOptions) 244 await options.video.save(sequelizeOptions)
244 245
245 { 246 {
@@ -297,8 +298,58 @@ async function updateVideoFromAP (options: {
297 } 298 }
298} 299}
299 300
301async function refreshVideoIfNeeded (options: {
302 video: VideoModel,
303 fetchedType: VideoFetchByUrlType,
304 syncParam: SyncParam
305}): Promise<VideoModel> {
306 if (!options.video.isOutdated()) return options.video
307
308 // We need more attributes if the argument video was fetched with not enough joints
309 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
310
311 try {
312 const { response, videoObject } = await fetchRemoteVideo(video.url)
313 if (response.statusCode === 404) {
314 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
315
316 // Video does not exist anymore
317 await video.destroy()
318 return undefined
319 }
320
321 if (videoObject === undefined) {
322 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
323
324 await video.setAsRefreshed()
325 return video
326 }
327
328 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
329 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
330
331 const updateOptions = {
332 video,
333 videoObject,
334 account,
335 channel: channelActor.VideoChannel
336 }
337 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
338 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
339
340 return video
341 } catch (err) {
342 logger.warn('Cannot refresh video %s.', options.video.url, { err })
343
344 // Don't refresh in loop
345 await video.setAsRefreshed()
346 return video
347 }
348}
349
300export { 350export {
301 updateVideoFromAP, 351 updateVideoFromAP,
352 refreshVideoIfNeeded,
302 federateVideoIfNeeded, 353 federateVideoIfNeeded,
303 fetchRemoteVideo, 354 fetchRemoteVideo,
304 getOrCreateVideoAndAccountAndChannel, 355 getOrCreateVideoAndAccountAndChannel,
@@ -362,52 +413,6 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
362 return videoCreated 413 return videoCreated
363} 414}
364 415
365async function refreshVideoIfNeeded (options: {
366 video: VideoModel,
367 fetchedType: VideoFetchByUrlType,
368 syncParam: SyncParam,
369 refreshViews: boolean
370}): Promise<VideoModel> {
371 if (!options.video.isOutdated()) return options.video
372
373 // We need more attributes if the argument video was fetched with not enough joints
374 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
375
376 try {
377 const { response, videoObject } = await fetchRemoteVideo(video.url)
378 if (response.statusCode === 404) {
379 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
380
381 // Video does not exist anymore
382 await video.destroy()
383 return undefined
384 }
385
386 if (videoObject === undefined) {
387 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
388 return video
389 }
390
391 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
392 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
393
394 const updateOptions = {
395 video,
396 videoObject,
397 account,
398 channel: channelActor.VideoChannel,
399 updateViews: options.refreshViews
400 }
401 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
402 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
403
404 return video
405 } catch (err) {
406 logger.warn('Cannot refresh video %s.', options.video.url, { err })
407 return video
408 }
409}
410
411async function videoActivityObjectToDBAttributes ( 416async function videoActivityObjectToDBAttributes (
412 videoChannel: VideoChannelModel, 417 videoChannel: VideoChannelModel,
413 videoObject: VideoTorrentObject, 418 videoObject: VideoTorrentObject,
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 9327792fb..074d4ad44 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -14,6 +14,7 @@ class Emailer {
14 private static instance: Emailer 14 private static instance: Emailer
15 private initialized = false 15 private initialized = false
16 private transporter: Transporter 16 private transporter: Transporter
17 private enabled = false
17 18
18 private constructor () {} 19 private constructor () {}
19 20
@@ -50,6 +51,8 @@ class Emailer {
50 tls, 51 tls,
51 auth 52 auth
52 }) 53 })
54
55 this.enabled = true
53 } else { 56 } else {
54 if (!isTestInstance()) { 57 if (!isTestInstance()) {
55 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') 58 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@@ -57,6 +60,10 @@ class Emailer {
57 } 60 }
58 } 61 }
59 62
63 isEnabled () {
64 return this.enabled
65 }
66
60 async checkConnectionOrDie () { 67 async checkConnectionOrDie () {
61 if (!this.transporter) return 68 if (!this.transporter) return
62 69
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
new file mode 100644
index 000000000..671b0f487
--- /dev/null
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -0,0 +1,41 @@
1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { fetchVideoByUrl } from '../../../helpers/video'
4import { refreshVideoIfNeeded } from '../../activitypub'
5
6export type RefreshPayload = {
7 videoUrl: string
8 type: 'video'
9}
10
11async function refreshAPObject (job: Bull.Job) {
12 const payload = job.data as RefreshPayload
13
14 logger.info('Processing AP refresher in job %d for video %s.', job.id, payload.videoUrl)
15
16 if (payload.type === 'video') return refreshAPVideo(payload.videoUrl)
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 refreshAPObject
23}
24
25// ---------------------------------------------------------------------------
26
27async function refreshAPVideo (videoUrl: string) {
28 const fetchType = 'all' as 'all'
29 const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true }
30
31 const videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
32 if (videoFromDatabase) {
33 const refreshOptions = {
34 video: videoFromDatabase,
35 fetchedType: fetchType,
36 syncParam
37 }
38
39 await refreshVideoIfNeeded(refreshOptions)
40 }
41}
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index adc0a2a15..ddbf6d1c2 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -1,5 +1,5 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { VideoResolution, VideoState } from '../../../../shared' 2import { VideoResolution, VideoState, Job } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { VideoModel } from '../../../models/video/video' 4import { VideoModel } from '../../../models/video/video'
5import { JobQueue } from '../job-queue' 5import { JobQueue } from '../job-queue'
@@ -111,7 +111,7 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole
111 ) 111 )
112 112
113 if (resolutionsEnabled.length !== 0) { 113 if (resolutionsEnabled.length !== 0) {
114 const tasks: Bluebird<any>[] = [] 114 const tasks: Bluebird<Bull.Job<any>>[] = []
115 115
116 for (const resolution of resolutionsEnabled) { 116 for (const resolution of resolutionsEnabled) {
117 const dataInput = { 117 const dataInput = {
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 4de901c0c..51a0b5faf 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -7,7 +7,7 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro
7import { extname, join } from 'path' 7import { extname, join } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' 9import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
10import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests' 10import { downloadImage } from '../../../helpers/requests'
11import { VideoState } from '../../../../shared' 11import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index' 12import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub' 13import { federateVideoIfNeeded } from '../../activitypub'
@@ -109,6 +109,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
109 let tempVideoPath: string 109 let tempVideoPath: string
110 let videoDestFile: string 110 let videoDestFile: string
111 let videoFile: VideoFileModel 111 let videoFile: VideoFileModel
112
112 try { 113 try {
113 // Download video from youtubeDL 114 // Download video from youtubeDL
114 tempVideoPath = await downloader() 115 tempVideoPath = await downloader()
@@ -144,8 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
144 // Process thumbnail 145 // Process thumbnail
145 if (options.downloadThumbnail) { 146 if (options.downloadThumbnail) {
146 if (options.thumbnailUrl) { 147 if (options.thumbnailUrl) {
147 const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) 148 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE)
148 await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE)
149 } else { 149 } else {
150 await videoImport.Video.createThumbnail(videoFile) 150 await videoImport.Video.createThumbnail(videoFile)
151 } 151 }
@@ -156,8 +156,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
156 // Process preview 156 // Process preview
157 if (options.downloadPreview) { 157 if (options.downloadPreview) {
158 if (options.thumbnailUrl) { 158 if (options.thumbnailUrl) {
159 const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) 159 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE)
160 await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE)
161 } else { 160 } else {
162 await videoImport.Video.createPreview(videoFile) 161 await videoImport.Video.createPreview(videoFile)
163 } 162 }
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts
index f44c3c727..fa1fd13b3 100644
--- a/server/lib/job-queue/handlers/video-views.ts
+++ b/server/lib/job-queue/handlers/video-views.ts
@@ -23,13 +23,9 @@ 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 if (isNaN(views)) { 26 if (views) {
27 logger.error('Cannot process videos views of video %d in hour %d: views number is NaN.', videoId, hour)
28 } else {
29 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) 27 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour)
30 28
31 await VideoModel.incrementViews(videoId, views)
32
33 try { 29 try {
34 await VideoViewModel.create({ 30 await VideoViewModel.create({
35 startDate, 31 startDate,
@@ -39,7 +35,14 @@ async function processVideosViews () {
39 }) 35 })
40 36
41 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 37 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
42 if (video.isOwned()) await federateVideoIfNeeded(video, false) 38 if (video.isOwned()) {
39 // If this is a remote video, the origin instance will send us an update
40 await VideoModel.incrementViews(videoId, views)
41
42 // Send video update
43 video.views += views
44 await federateVideoIfNeeded(video, false)
45 }
43 } catch (err) { 46 } catch (err) {
44 logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour) 47 logger.debug('Cannot create video views for video %d in hour %d. Maybe the video does not exist anymore?', videoId, hour)
45 } 48 }
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index 4cfd4d253..5862e178f 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -11,6 +11,7 @@ import { processVideoFile, processVideoFileImport, VideoFileImportPayload, Video
11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' 11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow'
12import { processVideoImport, VideoImportPayload } from './handlers/video-import' 12import { 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'
14 15
15type CreateJobArgument = 16type CreateJobArgument =
16 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 17 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -21,6 +22,7 @@ type CreateJobArgument =
21 { type: 'video-file', payload: VideoFilePayload } | 22 { type: 'video-file', payload: VideoFilePayload } |
22 { type: 'email', payload: EmailPayload } | 23 { type: 'email', payload: EmailPayload } |
23 { type: 'video-import', payload: VideoImportPayload } | 24 { type: 'video-import', payload: VideoImportPayload } |
25 { type: 'activitypub-refresher', payload: RefreshPayload } |
24 { type: 'videos-views', payload: {} } 26 { type: 'videos-views', payload: {} }
25 27
26const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { 28const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
@@ -32,7 +34,8 @@ const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = {
32 'video-file': processVideoFile, 34 'video-file': processVideoFile,
33 'email': processEmail, 35 'email': processEmail,
34 'video-import': processVideoImport, 36 'video-import': processVideoImport,
35 'videos-views': processVideosViews 37 'videos-views': processVideosViews,
38 'activitypub-refresher': refreshAPObject
36} 39}
37 40
38const jobTypes: JobType[] = [ 41const jobTypes: JobType[] = [
@@ -44,7 +47,8 @@ const jobTypes: JobType[] = [
44 'video-file', 47 'video-file',
45 'video-file-import', 48 'video-file-import',
46 'video-import', 49 'video-import',
47 'videos-views' 50 'videos-views',
51 'activitypub-refresher'
48] 52]
49 53
50class JobQueue { 54class JobQueue {
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index abd75d512..3e25e6a2c 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -121,7 +121,14 @@ class Redis {
121 const key = this.generateVideoViewKey(videoId, hour) 121 const key = this.generateVideoViewKey(videoId, hour)
122 122
123 const valueString = await this.getValue(key) 123 const valueString = await this.getValue(key)
124 return parseInt(valueString, 10) 124 const valueInt = parseInt(valueString, 10)
125
126 if (isNaN(valueInt)) {
127 logger.error('Cannot get videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
128 return undefined
129 }
130
131 return valueInt
125 } 132 }
126 133
127 async getVideosIdViewed (hour: number) { 134 async getVideosIdViewed (hour: number) {
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index 8b7f33539..2a99a665d 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -145,13 +145,13 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
145 145
146 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) 146 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
147 147
148 const destPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file)) 148 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
149 await rename(tmpPath, destPath) 149 await rename(tmpPath, destPath)
150 150
151 const createdModel = await VideoRedundancyModel.create({ 151 const createdModel = await VideoRedundancyModel.create({
152 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 152 expiresOn: this.buildNewExpiration(redundancy.minLifetime),
153 url: getVideoCacheFileActivityPubUrl(file), 153 url: getVideoCacheFileActivityPubUrl(file),
154 fileUrl: video.getVideoFileUrl(file, CONFIG.WEBSERVER.URL), 154 fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL),
155 strategy: redundancy.strategy, 155 strategy: redundancy.strategy,
156 videoFileId: file.id, 156 videoFileId: file.id,
157 actorId: serverActor.id 157 actorId: serverActor.id
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 61297120a..ccaf2eeb6 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -114,6 +114,7 @@ const deleteMeValidator = [
114const usersUpdateValidator = [ 114const usersUpdateValidator = [
115 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 115 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
116 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 116 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
117 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
117 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 118 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
118 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), 119 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
119 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), 120 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 5a237d733..a99e9b1ad 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -241,6 +241,27 @@ export class AccountModel extends Model<AccountModel> {
241 }) 241 })
242 } 242 }
243 243
244 static listLocalsForSitemap (sort: string) {
245 const query = {
246 attributes: [ ],
247 offset: 0,
248 order: getSort(sort),
249 include: [
250 {
251 attributes: [ 'preferredUsername', 'serverId' ],
252 model: ActorModel.unscoped(),
253 where: {
254 serverId: null
255 }
256 }
257 ]
258 }
259
260 return AccountModel
261 .unscoped()
262 .findAll(query)
263 }
264
244 toFormattedJSON (): Account { 265 toFormattedJSON (): Account {
245 const actor = this.Actor.toFormattedJSON() 266 const actor = this.Actor.toFormattedJSON()
246 const account = { 267 const account = {
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 9de4356b4..dd37dad22 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -15,7 +15,7 @@ import {
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' 18import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils' 20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 21import { VideoModel } from '../video/video'
@@ -124,7 +124,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` 124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
125 logger.info('Removing duplicated video file %s.', logIdentifier) 125 logger.info('Removing duplicated video file %s.', logIdentifier)
126 126
127 videoFile.Video.removeFile(videoFile) 127 videoFile.Video.removeFile(videoFile, true)
128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) 128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
129 129
130 return undefined 130 return undefined
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index f4586917e..86bf0461a 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -233,6 +233,27 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
233 }) 233 })
234 } 234 }
235 235
236 static listLocalsForSitemap (sort: string) {
237 const query = {
238 attributes: [ ],
239 offset: 0,
240 order: getSort(sort),
241 include: [
242 {
243 attributes: [ 'preferredUsername', 'serverId' ],
244 model: ActorModel.unscoped(),
245 where: {
246 serverId: null
247 }
248 }
249 ]
250 }
251
252 return VideoChannelModel
253 .unscoped()
254 .findAll(query)
255 }
256
236 static searchForApi (options: { 257 static searchForApi (options: {
237 actorId: number 258 actorId: number
238 search: string 259 search: string
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 1e68b380c..adef37937 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -140,7 +140,7 @@ type ForAPIOptions = {
140 140
141type AvailableForListIDsOptions = { 141type AvailableForListIDsOptions = {
142 serverAccountId: number 142 serverAccountId: number
143 actorId: number 143 followerActorId: number
144 includeLocalVideos: boolean 144 includeLocalVideos: boolean
145 filter?: VideoFilter 145 filter?: VideoFilter
146 categoryOneOf?: number[] 146 categoryOneOf?: number[]
@@ -315,7 +315,7 @@ type AvailableForListIDsOptions = {
315 query.include.push(videoChannelInclude) 315 query.include.push(videoChannelInclude)
316 } 316 }
317 317
318 if (options.actorId) { 318 if (options.followerActorId) {
319 let localVideosReq = '' 319 let localVideosReq = ''
320 if (options.includeLocalVideos === true) { 320 if (options.includeLocalVideos === true) {
321 localVideosReq = ' UNION ALL ' + 321 localVideosReq = ' UNION ALL ' +
@@ -327,7 +327,7 @@ type AvailableForListIDsOptions = {
327 } 327 }
328 328
329 // Force actorId to be a number to avoid SQL injections 329 // Force actorId to be a number to avoid SQL injections
330 const actorIdNumber = parseInt(options.actorId.toString(), 10) 330 const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
331 query.where[ 'id' ][ Sequelize.Op.and ].push({ 331 query.where[ 'id' ][ Sequelize.Op.and ].push({
332 [ Sequelize.Op.in ]: Sequelize.literal( 332 [ Sequelize.Op.in ]: Sequelize.literal(
333 '(' + 333 '(' +
@@ -985,7 +985,7 @@ export class VideoModel extends Model<VideoModel> {
985 filter?: VideoFilter, 985 filter?: VideoFilter,
986 accountId?: number, 986 accountId?: number,
987 videoChannelId?: number, 987 videoChannelId?: number,
988 actorId?: number 988 followerActorId?: number
989 trendingDays?: number, 989 trendingDays?: number,
990 user?: UserModel 990 user?: UserModel
991 }, countVideos = true) { 991 }, countVideos = true) {
@@ -1008,11 +1008,11 @@ export class VideoModel extends Model<VideoModel> {
1008 1008
1009 const serverActor = await getServerActor() 1009 const serverActor = await getServerActor()
1010 1010
1011 // actorId === null has a meaning, so just check undefined 1011 // followerActorId === null has a meaning, so just check undefined
1012 const actorId = options.actorId !== undefined ? options.actorId : serverActor.id 1012 const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id
1013 1013
1014 const queryOptions = { 1014 const queryOptions = {
1015 actorId, 1015 followerActorId,
1016 serverAccountId: serverActor.Account.id, 1016 serverAccountId: serverActor.Account.id,
1017 nsfw: options.nsfw, 1017 nsfw: options.nsfw,
1018 categoryOneOf: options.categoryOneOf, 1018 categoryOneOf: options.categoryOneOf,
@@ -1118,7 +1118,7 @@ export class VideoModel extends Model<VideoModel> {
1118 1118
1119 const serverActor = await getServerActor() 1119 const serverActor = await getServerActor()
1120 const queryOptions = { 1120 const queryOptions = {
1121 actorId: serverActor.id, 1121 followerActorId: serverActor.id,
1122 serverAccountId: serverActor.Account.id, 1122 serverAccountId: serverActor.Account.id,
1123 includeLocalVideos: options.includeLocalVideos, 1123 includeLocalVideos: options.includeLocalVideos,
1124 nsfw: options.nsfw, 1124 nsfw: options.nsfw,
@@ -1273,11 +1273,11 @@ export class VideoModel extends Model<VideoModel> {
1273 // threshold corresponds to how many video the field should have to be returned 1273 // threshold corresponds to how many video the field should have to be returned
1274 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1274 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1275 const serverActor = await getServerActor() 1275 const serverActor = await getServerActor()
1276 const actorId = serverActor.id 1276 const followerActorId = serverActor.id
1277 1277
1278 const scopeOptions: AvailableForListIDsOptions = { 1278 const scopeOptions: AvailableForListIDsOptions = {
1279 serverAccountId: serverActor.Account.id, 1279 serverAccountId: serverActor.Account.id,
1280 actorId, 1280 followerActorId,
1281 includeLocalVideos: true 1281 includeLocalVideos: true
1282 } 1282 }
1283 1283
@@ -1538,8 +1538,10 @@ export class VideoModel extends Model<VideoModel> {
1538 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) 1538 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err }))
1539 } 1539 }
1540 1540
1541 removeFile (videoFile: VideoFileModel) { 1541 removeFile (videoFile: VideoFileModel, isRedundancy = false) {
1542 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 1542 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
1543
1544 const filePath = join(baseDir, this.getVideoFilename(videoFile))
1543 return remove(filePath) 1545 return remove(filePath)
1544 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1546 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1545 } 1547 }
@@ -1561,6 +1563,12 @@ export class VideoModel extends Model<VideoModel> {
1561 (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL 1563 (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL
1562 } 1564 }
1563 1565
1566 setAsRefreshed () {
1567 this.changed('updatedAt', true)
1568
1569 return this.save()
1570 }
1571
1564 getBaseUrls () { 1572 getBaseUrls () {
1565 let baseUrlHttp 1573 let baseUrlHttp
1566 let baseUrlWs 1574 let baseUrlWs
@@ -1611,6 +1619,10 @@ export class VideoModel extends Model<VideoModel> {
1611 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) 1619 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
1612 } 1620 }
1613 1621
1622 getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1623 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
1624 }
1625
1614 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1626 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1615 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 1627 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
1616 } 1628 }
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
index d45232c8d..6d90d8643 100644
--- a/server/tests/api/activitypub/client.ts
+++ b/server/tests/api/activitypub/client.ts
@@ -3,32 +3,41 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 doubleFollow,
7 flushAndRunMultipleServers,
6 flushTests, 8 flushTests,
7 killallServers, 9 killallServers,
8 makeActivityPubGetRequest, 10 makeActivityPubGetRequest,
9 runServer,
10 ServerInfo, 11 ServerInfo,
11 setAccessTokensToServers 12 setAccessTokensToServers,
13 uploadVideo
12} from '../../../../shared/utils' 14} from '../../../../shared/utils'
13 15
14
15const expect = chai.expect 16const expect = chai.expect
16 17
17describe('Test activitypub', function () { 18describe('Test activitypub', function () {
18 let server: ServerInfo = null 19 let servers: ServerInfo[] = []
20 let videoUUID: string
19 21
20 before(async function () { 22 before(async function () {
21 this.timeout(30000) 23 this.timeout(30000)
22 24
23 await flushTests() 25 await flushTests()
24 26
25 server = await runServer(1) 27 servers = await flushAndRunMultipleServers(2)
28
29 await setAccessTokensToServers(servers)
30
31 {
32 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' })
33 videoUUID = res.body.video.uuid
34 }
26 35
27 await setAccessTokensToServers([ server ]) 36 await doubleFollow(servers[0], servers[1])
28 }) 37 })
29 38
30 it('Should return the account object', async function () { 39 it('Should return the account object', async function () {
31 const res = await makeActivityPubGetRequest(server.url, '/accounts/root') 40 const res = await makeActivityPubGetRequest(servers[0].url, '/accounts/root')
32 const object = res.body 41 const object = res.body
33 42
34 expect(object.type).to.equal('Person') 43 expect(object.type).to.equal('Person')
@@ -37,7 +46,22 @@ describe('Test activitypub', function () {
37 expect(object.preferredUsername).to.equal('root') 46 expect(object.preferredUsername).to.equal('root')
38 }) 47 })
39 48
49 it('Should return the video object', async function () {
50 const res = await makeActivityPubGetRequest(servers[0].url, '/videos/watch/' + videoUUID)
51 const object = res.body
52
53 expect(object.type).to.equal('Video')
54 expect(object.id).to.equal('http://localhost:9001/videos/watch/' + videoUUID)
55 expect(object.name).to.equal('video')
56 })
57
58 it('Should redirect to the origin video object', async function () {
59 const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + videoUUID, 302)
60
61 expect(res.header.location).to.equal('http://localhost:9001/videos/watch/' + videoUUID)
62 })
63
40 after(async function () { 64 after(async function () {
41 killallServers([ server ]) 65 killallServers(servers)
42 }) 66 })
43}) 67})
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts
index e84eb18bb..03609c1a9 100644
--- a/server/tests/api/activitypub/fetch.ts
+++ b/server/tests/api/activitypub/fetch.ts
@@ -11,12 +11,13 @@ import {
11 killallServers, 11 killallServers,
12 ServerInfo, 12 ServerInfo,
13 setAccessTokensToServers, 13 setAccessTokensToServers,
14 setActorField,
15 setVideoField,
14 uploadVideo, 16 uploadVideo,
15 userLogin 17 userLogin,
18 waitJobs
16} from '../../../../shared/utils' 19} from '../../../../shared/utils'
17import * as chai from 'chai' 20import * as chai from 'chai'
18import { setActorField, setVideoField } from '../../utils/miscs/sql'
19import { waitJobs } from '../../../../shared/utils/server/jobs'
20import { Video } from '../../../../shared/models/videos' 21import { Video } from '../../../../shared/models/videos'
21 22
22const expect = chai.expect 23const expect = chai.expect
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
index 4c42f3d67..ac6e755c3 100644
--- a/server/tests/api/activitypub/helpers.ts
+++ b/server/tests/api/activitypub/helpers.ts
@@ -2,7 +2,7 @@
2 2
3import 'mocha' 3import 'mocha'
4import { expect } from 'chai' 4import { expect } from 'chai'
5import { buildRequestStub } from '../../utils/miscs/stubs' 5import { buildRequestStub } from '../../../../shared/utils/miscs/stubs'
6import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' 6import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
7import { cloneDeep } from 'lodash' 7import { cloneDeep } from 'lodash'
8import { buildSignedActivity } from '../../../helpers/activitypub' 8import { buildSignedActivity } from '../../../helpers/activitypub'
@@ -91,7 +91,7 @@ describe('Test activity pub helpers', function () {
91 req.headers = mastodonObject.headers 91 req.headers = mastodonObject.headers
92 req.headers.signature = 'Signature ' + req.headers.signature 92 req.headers.signature = 'Signature ' + req.headers.signature
93 93
94 const parsed = parseHTTPSignature(req, 3600 * 365 * 3) 94 const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
95 const publicKey = require('./json/mastodon/public-key.json').publicKey 95 const publicKey = require('./json/mastodon/public-key.json').publicKey
96 96
97 const actor = { publicKey } 97 const actor = { publicKey }
@@ -110,7 +110,7 @@ describe('Test activity pub helpers', function () {
110 req.headers = mastodonObject.headers 110 req.headers = mastodonObject.headers
111 req.headers.signature = 'Signature ' + req.headers.signature 111 req.headers.signature = 'Signature ' + req.headers.signature
112 112
113 const parsed = parseHTTPSignature(req, 3600 * 365 * 3) 113 const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
114 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey 114 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
115 115
116 const actor = { publicKey } 116 const actor = { publicKey }
@@ -150,7 +150,7 @@ describe('Test activity pub helpers', function () {
150 150
151 let errored = false 151 let errored = false
152 try { 152 try {
153 parseHTTPSignature(req, 3600 * 365 * 3) 153 parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
154 } catch { 154 } catch {
155 errored = true 155 errored = true
156 } 156 }
@@ -168,7 +168,7 @@ describe('Test activity pub helpers', function () {
168 req.headers = mastodonObject.headers 168 req.headers = mastodonObject.headers
169 req.headers.signature = 'Signature ' + req.headers.signature 169 req.headers.signature = 'Signature ' + req.headers.signature
170 170
171 const parsed = parseHTTPSignature(req, 3600 * 365 * 3) 171 const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10)
172 const publicKey = require('./json/mastodon/public-key.json').publicKey 172 const publicKey = require('./json/mastodon/public-key.json').publicKey
173 173
174 const actor = { publicKey } 174 const actor = { publicKey }
diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts
index e748f32e9..450053309 100644
--- a/server/tests/api/activitypub/index.ts
+++ b/server/tests/api/activitypub/index.ts
@@ -1,4 +1,5 @@
1import './client' 1import './client'
2import './fetch' 2import './fetch'
3import './helpers' 3import './helpers'
4import './refresher'
4import './security' 5import './security'
diff --git a/server/tests/api/activitypub/refresher.ts b/server/tests/api/activitypub/refresher.ts
new file mode 100644
index 000000000..332ea7ed1
--- /dev/null
+++ b/server/tests/api/activitypub/refresher.ts
@@ -0,0 +1,93 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import {
5 doubleFollow,
6 flushAndRunMultipleServers,
7 getVideo,
8 killallServers,
9 reRunServer,
10 ServerInfo,
11 setAccessTokensToServers,
12 uploadVideo,
13 wait,
14 setVideoField,
15 waitJobs
16} from '../../../../shared/utils'
17
18describe('Test AP refresher', function () {
19 let servers: ServerInfo[] = []
20 let videoUUID1: string
21 let videoUUID2: string
22 let videoUUID3: string
23
24 before(async function () {
25 this.timeout(30000)
26
27 servers = await flushAndRunMultipleServers(2)
28
29 // Get the access tokens
30 await setAccessTokensToServers(servers)
31
32 {
33 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' })
34 videoUUID1 = res.body.video.uuid
35 }
36
37 {
38 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
39 videoUUID2 = res.body.video.uuid
40 }
41
42 {
43 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video3' })
44 videoUUID3 = res.body.video.uuid
45 }
46
47 await doubleFollow(servers[0], servers[1])
48 })
49
50 it('Should remove a deleted remote video', async function () {
51 this.timeout(60000)
52
53 await wait(10000)
54
55 // Change UUID so the remote server returns a 404
56 await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
57
58 await getVideo(servers[0].url, videoUUID1)
59 await getVideo(servers[0].url, videoUUID2)
60
61 await waitJobs(servers)
62
63 await getVideo(servers[0].url, videoUUID1, 404)
64 await getVideo(servers[0].url, videoUUID2, 200)
65 })
66
67 it('Should not update a remote video if the remote instance is down', async function () {
68 this.timeout(60000)
69
70 killallServers([ servers[1] ])
71
72 await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
73
74 // Video will need a refresh
75 await wait(10000)
76
77 await getVideo(servers[0].url, videoUUID3)
78 // The refresh should fail
79 await waitJobs([ servers[0] ])
80
81 await reRunServer(servers[1])
82
83 // Should not refresh the video, even if the last refresh failed (to avoir a loop on dead instances)
84 await getVideo(servers[0].url, videoUUID3)
85 await waitJobs(servers)
86
87 await getVideo(servers[0].url, videoUUID3, 200)
88 })
89
90 after(async function () {
91 killallServers(servers)
92 })
93})
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
index b71a61c8c..342ae0fa1 100644
--- a/server/tests/api/activitypub/security.ts
+++ b/server/tests/api/activitypub/security.ts
@@ -6,14 +6,15 @@ import {
6 flushAndRunMultipleServers, 6 flushAndRunMultipleServers,
7 flushTests, 7 flushTests,
8 killallServers, 8 killallServers,
9 ServerInfo 9 makeFollowRequest,
10 makePOSTAPRequest,
11 ServerInfo,
12 setActorField
10} from '../../../../shared/utils' 13} from '../../../../shared/utils'
11import { HTTP_SIGNATURE } from '../../../initializers' 14import { HTTP_SIGNATURE } from '../../../initializers'
12import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' 15import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
13import * as chai from 'chai' 16import * as chai from 'chai'
14import { setActorField } from '../../utils/miscs/sql'
15import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' 17import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
16import { makeFollowRequest, makePOSTAPRequest } from '../../utils/requests/activitypub'
17 18
18const expect = chai.expect 19const expect = chai.expect
19 20
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index f4c177621..05f42bca9 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -103,13 +103,13 @@ describe('Test users API validators', function () {
103 } 103 }
104 104
105 it('Should fail with a too small username', async function () { 105 it('Should fail with a too small username', async function () {
106 const fields = immutableAssign(baseCorrectParams, { username: 'fi' }) 106 const fields = immutableAssign(baseCorrectParams, { username: '' })
107 107
108 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 108 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
109 }) 109 })
110 110
111 it('Should fail with a too long username', async function () { 111 it('Should fail with a too long username', async function () {
112 const fields = immutableAssign(baseCorrectParams, { username: 'my_super_username_which_is_very_long' }) 112 const fields = immutableAssign(baseCorrectParams, { username: 'super'.repeat(11) })
113 113
114 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 114 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
115 }) 115 })
@@ -432,6 +432,14 @@ describe('Test users API validators', function () {
432 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) 432 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
433 }) 433 })
434 434
435 it('Should fail with an invalid emailVerified attribute', async function () {
436 const fields = {
437 emailVerified: 'yes'
438 }
439
440 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
441 })
442
435 it('Should fail with an invalid videoQuota attribute', async function () { 443 it('Should fail with an invalid videoQuota attribute', async function () {
436 const fields = { 444 const fields = {
437 videoQuota: -90 445 videoQuota: -90
@@ -467,6 +475,7 @@ describe('Test users API validators', function () {
467 it('Should succeed with the correct params', async function () { 475 it('Should succeed with the correct params', async function () {
468 const fields = { 476 const fields = {
469 email: 'email@example.com', 477 email: 'email@example.com',
478 emailVerified: true,
470 videoQuota: 42, 479 videoQuota: 42,
471 role: UserRole.MODERATOR 480 role: UserRole.MODERATOR
472 } 481 }
@@ -545,13 +554,13 @@ describe('Test users API validators', function () {
545 } 554 }
546 555
547 it('Should fail with a too small username', async function () { 556 it('Should fail with a too small username', async function () {
548 const fields = immutableAssign(baseCorrectParams, { username: 'ji' }) 557 const fields = immutableAssign(baseCorrectParams, { username: '' })
549 558
550 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) 559 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
551 }) 560 })
552 561
553 it('Should fail with a too long username', async function () { 562 it('Should fail with a too long username', async function () {
554 const fields = immutableAssign(baseCorrectParams, { username: 'my_super_username_which_is_very_long' }) 563 const fields = immutableAssign(baseCorrectParams, { username: 'super'.repeat(11) })
555 564
556 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) 565 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
557 }) 566 })
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 2bc1b60ce..9d3ce8153 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -137,7 +137,7 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st
137 if (!videoUUID) videoUUID = video1Server2UUID 137 if (!videoUUID) videoUUID = video1Server2UUID
138 138
139 const webseeds = [ 139 const webseeds = [
140 'http://localhost:9001/static/webseed/' + videoUUID, 140 'http://localhost:9001/static/redundancy/' + videoUUID,
141 'http://localhost:9002/static/webseed/' + videoUUID 141 'http://localhost:9002/static/webseed/' + videoUUID
142 ] 142 ]
143 143
@@ -149,20 +149,23 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st
149 for (const file of video.files) { 149 for (const file of video.files) {
150 checkMagnetWebseeds(file, webseeds, server) 150 checkMagnetWebseeds(file, webseeds, server)
151 151
152 // Only servers 1 and 2 have the video 152 await makeGetRequest({
153 if (server.serverNumber !== 3) { 153 url: servers[0].url,
154 await makeGetRequest({ 154 statusCodeExpected: 200,
155 url: server.url, 155 path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
156 statusCodeExpected: 200, 156 contentType: null
157 path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, 157 })
158 contentType: null 158 await makeGetRequest({
159 }) 159 url: servers[1].url,
160 } 160 statusCodeExpected: 200,
161 path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`,
162 contentType: null
163 })
161 } 164 }
162 } 165 }
163 166
164 for (const directory of [ 'test1', 'test2' ]) { 167 for (const directory of [ 'test1/redundancy', 'test2/videos' ]) {
165 const files = await readdir(join(root(), directory, 'videos')) 168 const files = await readdir(join(root(), directory))
166 expect(files).to.have.length.at.least(4) 169 expect(files).to.have.length.at.least(4)
167 170
168 for (const resolution of [ 240, 360, 480, 720 ]) { 171 for (const resolution of [ 240, 360, 480, 720 ]) {
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 7dffbb0b1..4914c8ed5 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -478,6 +478,7 @@ describe('Test users', function () {
478 userId, 478 userId,
479 accessToken, 479 accessToken,
480 email: 'updated2@example.com', 480 email: 'updated2@example.com',
481 emailVerified: true,
481 videoQuota: 42, 482 videoQuota: 42,
482 role: UserRole.MODERATOR 483 role: UserRole.MODERATOR
483 }) 484 })
@@ -487,6 +488,7 @@ describe('Test users', function () {
487 488
488 expect(user.username).to.equal('user_1') 489 expect(user.username).to.equal('user_1')
489 expect(user.email).to.equal('updated2@example.com') 490 expect(user.email).to.equal('updated2@example.com')
491 expect(user.emailVerified).to.be.true
490 expect(user.nsfwPolicy).to.equal('do_not_list') 492 expect(user.nsfwPolicy).to.equal('do_not_list')
491 expect(user.videoQuota).to.equal(42) 493 expect(user.videoQuota).to.equal(42)
492 expect(user.roleLabel).to.equal('Moderator') 494 expect(user.roleLabel).to.equal('Moderator')
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts
index 6201314ce..c6b7ec078 100644
--- a/server/tests/cli/index.ts
+++ b/server/tests/cli/index.ts
@@ -1,6 +1,7 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './create-import-video-file-job' 2import './create-import-video-file-job'
3import './create-transcoding-job' 3import './create-transcoding-job'
4import './optimize-old-videos'
4import './peertube' 5import './peertube'
5import './reset-password' 6import './reset-password'
6import './update-host' 7import './update-host'
diff --git a/server/tests/misc-endpoints.ts b/server/tests/misc-endpoints.ts
index f948fdfd0..5f82719da 100644
--- a/server/tests/misc-endpoints.ts
+++ b/server/tests/misc-endpoints.ts
@@ -2,7 +2,18 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from '../../shared/utils' 5import {
6 addVideoChannel,
7 createUser,
8 flushTests,
9 killallServers,
10 makeGetRequest,
11 runServer,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo
15} from '../../shared/utils'
16import { VideoPrivacy } from '../../shared/models/videos'
6 17
7const expect = chai.expect 18const expect = chai.expect
8 19
@@ -15,6 +26,7 @@ describe('Test misc endpoints', function () {
15 await flushTests() 26 await flushTests()
16 27
17 server = await runServer(1) 28 server = await runServer(1)
29 await setAccessTokensToServers([ server ])
18 }) 30 })
19 31
20 describe('Test a well known endpoints', function () { 32 describe('Test a well known endpoints', function () {
@@ -60,6 +72,16 @@ describe('Test misc endpoints', function () {
60 72
61 expect(res.body.tracking).to.equal('N') 73 expect(res.body.tracking).to.equal('N')
62 }) 74 })
75
76 it('Should get change-password location', async function () {
77 const res = await makeGetRequest({
78 url: server.url,
79 path: '/.well-known/change-password',
80 statusCodeExpected: 302
81 })
82
83 expect(res.header.location).to.equal('/my-account/settings')
84 })
63 }) 85 })
64 86
65 describe('Test classic static endpoints', function () { 87 describe('Test classic static endpoints', function () {
@@ -93,6 +115,64 @@ describe('Test misc endpoints', function () {
93 }) 115 })
94 }) 116 })
95 117
118 describe('Test bots endpoints', function () {
119
120 it('Should get the empty sitemap', async function () {
121 const res = await makeGetRequest({
122 url: server.url,
123 path: '/sitemap.xml',
124 statusCodeExpected: 200
125 })
126
127 expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
128 expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
129 })
130
131 it('Should get the empty cached sitemap', async function () {
132 const res = await makeGetRequest({
133 url: server.url,
134 path: '/sitemap.xml',
135 statusCodeExpected: 200
136 })
137
138 expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
139 expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
140 })
141
142 it('Should add videos, channel and accounts and get sitemap', async function () {
143 this.timeout(35000)
144
145 await uploadVideo(server.url, server.accessToken, { name: 'video 1', nsfw: false })
146 await uploadVideo(server.url, server.accessToken, { name: 'video 2', nsfw: false })
147 await uploadVideo(server.url, server.accessToken, { name: 'video 3', privacy: VideoPrivacy.PRIVATE })
148
149 await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' })
150 await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' })
151
152 await createUser(server.url, server.accessToken, 'user1', 'password')
153 await createUser(server.url, server.accessToken, 'user2', 'password')
154
155 const res = await makeGetRequest({
156 url: server.url,
157 path: '/sitemap.xml?t=1', // avoid using cache
158 statusCodeExpected: 200
159 })
160
161 expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
162 expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>')
163
164 expect(res.text).to.contain('<video:title><![CDATA[video 1]]></video:title>')
165 expect(res.text).to.contain('<video:title><![CDATA[video 2]]></video:title>')
166 expect(res.text).to.not.contain('<video:title><![CDATA[video 3]]></video:title>')
167
168 expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel1</loc></url>')
169 expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel2</loc></url>')
170
171 expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user1</loc></url>')
172 expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user2</loc></url>')
173 })
174 })
175
96 after(async function () { 176 after(async function () {
97 killallServers([ server ]) 177 killallServers([ server ])
98 }) 178 })
diff --git a/server/tests/utils/miscs/sql.ts b/server/tests/utils/miscs/sql.ts
deleted file mode 100644
index 027f78131..000000000
--- a/server/tests/utils/miscs/sql.ts
+++ /dev/null
@@ -1,38 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3function getSequelize (serverNumber: number) {
4 const dbname = 'peertube_test' + serverNumber
5 const username = 'peertube'
6 const password = 'peertube'
7 const host = 'localhost'
8 const port = 5432
9
10 return new Sequelize(dbname, username, password, {
11 dialect: 'postgres',
12 host,
13 port,
14 operatorsAliases: false,
15 logging: false
16 })
17}
18
19function setActorField (serverNumber: number, to: string, field: string, value: string) {
20 const seq = getSequelize(serverNumber)
21
22 const options = { type: Sequelize.QueryTypes.UPDATE }
23
24 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
25}
26
27function setVideoField (serverNumber: number, uuid: string, field: string, value: string) {
28 const seq = getSequelize(serverNumber)
29
30 const options = { type: Sequelize.QueryTypes.UPDATE }
31
32 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
33}
34
35export {
36 setVideoField,
37 setActorField
38}
diff --git a/server/tests/utils/miscs/stubs.ts b/server/tests/utils/miscs/stubs.ts
deleted file mode 100644
index d1eb0e3b2..000000000
--- a/server/tests/utils/miscs/stubs.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
12 buildResponseStub,
13 buildRequestStub
14}
diff --git a/server/tests/utils/requests/activitypub.ts b/server/tests/utils/requests/activitypub.ts
deleted file mode 100644
index 96fee60a8..000000000
--- a/server/tests/utils/requests/activitypub.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { doRequest } from '../../../helpers/requests'
2import { HTTP_SIGNATURE } from '../../../initializers'
3import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../helpers/activitypub'
5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = {
8 method: 'POST',
9 uri: url,
10 json: body,
11 httpSignature,
12 headers
13 }
14
15 return doRequest(options)
16}
17
18async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
19 const follow = {
20 type: 'Follow',
21 id: by.url + '/toto',
22 actor: by.url,
23 object: to.url
24 }
25
26 const body = activityPubContextify(follow)
27
28 const httpSignature = {
29 algorithm: HTTP_SIGNATURE.ALGORITHM,
30 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
31 keyId: by.url,
32 key: by.privateKey,
33 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
34 }
35 const headers = buildGlobalHeaders(body)
36
37 return makePOSTAPRequest(to.url, body, httpSignature, headers)
38}
39
40export {
41 makePOSTAPRequest,
42 makeFollowRequest
43}
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts
index 6800ff8ab..04d8b95a3 100644
--- a/server/tools/peertube-repl.ts
+++ b/server/tools/peertube-repl.ts
@@ -20,14 +20,10 @@ import * as signupUtils from '../helpers/signup'
20import * as utils from '../helpers/utils' 20import * as utils from '../helpers/utils'
21import * as YoutubeDLUtils from '../helpers/youtube-dl' 21import * as YoutubeDLUtils from '../helpers/youtube-dl'
22 22
23let versionCommitHash
24
25const start = async () => { 23const start = async () => {
26 await initDatabaseModels(true) 24 await initDatabaseModels(true)
27 25
28 await utils.getVersion().then((data) => { 26 const versionCommitHash = await utils.getServerCommit()
29 versionCommitHash = data
30 })
31 27
32 const initContext = (replServer) => { 28 const initContext = (replServer) => {
33 return (context) => { 29 return (context) => {
@@ -59,6 +55,7 @@ const start = async () => {
59 55
60 initContext(replServer)(replServer.context) 56 initContext(replServer)(replServer.context)
61 replServer.on('reset', initContext(replServer)) 57 replServer.on('reset', initContext(replServer))
58 replServer.on('exit', () => process.exit())
62 59
63 const resetCommand = { 60 const resetCommand = {
64 help: 'Reset REPL', 61 help: 'Reset REPL',