diff options
Diffstat (limited to 'server')
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 | ||
165 | async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { | 165 | async 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 | ||
184 | async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { | 184 | async 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 | ||
194 | async function videoAnnouncesController (req: express.Request, res: express.Response, next: express.NextFunction) { | 194 | async 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 | ||
209 | async function videoLikesController (req: express.Request, res: express.Response, next: express.NextFunction) { | 209 | async 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 | ||
216 | async function videoDislikesController (req: express.Request, res: express.Response, next: express.NextFunction) { | 216 | async 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 | ||
223 | async function videoCommentsController (req: express.Request, res: express.Response, next: express.NextFunction) { | 223 | async 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 | ||
238 | async function videoChannelController (req: express.Request, res: express.Response, next: express.NextFunction) { | 238 | async 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 | ||
244 | async function videoChannelFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) { | 244 | async 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 | ||
251 | async function videoChannelFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) { | 251 | async 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 | ||
258 | async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { | 258 | async 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 | ||
277 | async function videoRedundancyController (req: express.Request, res: express.Response) { | 277 | async 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 | ||
75 | async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 75 | async 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 | |||
10 | import { ClientHtml } from '../../lib/client-html' | 10 | import { ClientHtml } from '../../lib/client-html' |
11 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' | 11 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' |
12 | import { remove, writeJSON } from 'fs-extra' | 12 | import { remove, writeJSON } from 'fs-extra' |
13 | import { getVersion } from '../../helpers/utils' | 13 | import { getServerCommit } from '../../helpers/utils' |
14 | import { Emailer } from '../../lib/emailer' | ||
14 | 15 | ||
15 | const packageJSON = require('../../../../package.json') | 16 | const packageJSON = require('../../../../package.json') |
16 | const configRouter = express.Router() | 17 | const configRouter = express.Router() |
@@ -40,11 +41,11 @@ configRouter.delete('/custom', | |||
40 | ) | 41 | ) |
41 | 42 | ||
42 | let serverCommit: string | 43 | let serverCommit: string |
43 | async function getConfig (req: express.Request, res: express.Response, next: express.NextFunction) { | 44 | async 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 | ||
43 | const auditLogger = auditLoggerFactory('users-me') | 43 | const auditLogger = auditLoggerFactory('users-me') |
44 | 44 | ||
45 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 45 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
46 | 46 | ||
47 | const meRouter = express.Router() | 47 | const 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 | ||
351 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | 351 | async 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' | |||
32 | import { UserModel } from '../../models/account/user' | 32 | import { UserModel } from '../../models/account/user' |
33 | 33 | ||
34 | const auditLogger = auditLoggerFactory('channels') | 34 | const auditLogger = auditLoggerFactory('channels') |
35 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 35 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
36 | 36 | ||
37 | const videoChannelRouter = express.Router() | 37 | const videoChannelRouter = express.Router() |
38 | 38 | ||
@@ -202,10 +202,10 @@ async function getVideoChannel (req: express.Request, res: express.Response, nex | |||
202 | 202 | ||
203 | async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 203 | async 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 | ) |
75 | const reqVideoFileUpdate = createReqFiles( | 75 | const 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) { | |||
387 | function getVideo (req: express.Request, res: express.Response) { | 387 | function 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 | ||
432 | async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 432 | async 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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { asyncMiddleware } from '../middlewares' | ||
3 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers' | ||
4 | import * as sitemapModule from 'sitemap' | ||
5 | import { logger } from '../helpers/logger' | ||
6 | import { VideoModel } from '../models/video/video' | ||
7 | import { VideoChannelModel } from '../models/video/video-channel' | ||
8 | import { AccountModel } from '../models/account/account' | ||
9 | import { cacheRoute } from '../middlewares/cache' | ||
10 | import { buildNSFWFilter } from '../helpers/express-utils' | ||
11 | import { truncate } from 'lodash' | ||
12 | |||
13 | const 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 | ||
17 | botsRouter.use('/sitemap.xml', | ||
18 | asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)), | ||
19 | asyncMiddleware(getSitemap) | ||
20 | ) | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | botsRouter | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | async 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 | |||
53 | async 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 | |||
61 | async 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 | |||
69 | async 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 | |||
94 | function 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' | |||
6 | export * from './static' | 6 | export * from './static' |
7 | export * from './webfinger' | 7 | export * from './webfinger' |
8 | export * from './tracker' | 8 | export * from './tracker' |
9 | export * 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 |
37 | const videosPhysicalPath = CONFIG.STORAGE.VIDEOS_DIR | ||
38 | staticRouter.use( | 37 | staticRouter.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 | ) |
43 | staticRouter.use( | 42 | staticRouter.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 | |||
48 | staticRouter.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 | ||
139 | staticRouter.use('/.well-known/change-password', | ||
140 | (_, res: express.Response) => { | ||
141 | res.redirect('/my-account/settings') | ||
142 | } | ||
143 | ) | ||
144 | |||
134 | // --------------------------------------------------------------------------- | 145 | // --------------------------------------------------------------------------- |
135 | 146 | ||
136 | export { | 147 | export { |
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' | |||
7 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
8 | import { UserModel } from '../models/account/user' | 8 | import { UserModel } from '../models/account/user' |
9 | 9 | ||
10 | function buildNSFWFilter (res: express.Response, paramNSFW?: string) { | 10 | function 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 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as sharp from 'sharp' | 2 | import * as sharp from 'sharp' |
3 | import { move, remove } from 'fs-extra' | 3 | import { readFile, remove } from 'fs-extra' |
4 | import { logger } from './logger' | ||
4 | 5 | ||
5 | async function processImage ( | 6 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { createWriteStream } from 'fs-extra' | 2 | import { createWriteStream } from 'fs-extra' |
3 | import * as request from 'request' | 3 | import * as request from 'request' |
4 | import { ACTIVITY_PUB } from '../initializers' | 4 | import { ACTIVITY_PUB, CONFIG } from '../initializers' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { join } from 'path' | ||
6 | 7 | ||
7 | function doRequest <T> ( | 8 | function 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 | ||
31 | async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) { | 32 | async 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 | ||
49 | function generateVideoTmpPath (target: string | ParseTorrent) { | 49 | function 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 | ||
56 | function getSecureTorrentName (originalName: string) { | 56 | function getSecureTorrentName (originalName: string) { |
57 | return sha256(originalName) + '.torrent' | 57 | return sha256(originalName) + '.torrent' |
58 | } | 58 | } |
59 | 59 | ||
60 | async function getVersion () { | 60 | async 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 @@ | |||
1 | import { logger } from './logger' | 1 | import { logger } from './logger' |
2 | import { generateVideoTmpPath } from './utils' | 2 | import { generateVideoImportTmpPath } from './utils' |
3 | import * as WebTorrent from 'webtorrent' | 3 | import * as WebTorrent from 'webtorrent' |
4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' | 4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' |
5 | import { CONFIG } from '../initializers' | 5 | import { 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 @@ | |||
1 | import { truncate } from 'lodash' | 1 | import { truncate } from 'lodash' |
2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' | 2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' |
3 | import { logger } from './logger' | 3 | import { logger } from './logger' |
4 | import { generateVideoTmpPath } from './utils' | 4 | import { generateVideoImportTmpPath } from './utils' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { root } from './core-utils' | 6 | import { root } from './core-utils' |
7 | import { ensureDir, writeFile, remove } from 'fs-extra' | 7 | import { ensureDir, writeFile, remove } from 'fs-extra' |
@@ -24,10 +24,10 @@ const processOptions = { | |||
24 | 24 | ||
25 | function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { | 25 | function 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 | ||
42 | function downloadYoutubeDLVideo (url: string, timeout: number) { | 42 | function 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 = { | |||
61 | const ROUTE_CACHE_LIFETIME = { | 61 | const 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 | } |
107 | const JOB_CONCURRENCY: { [ id in JobType ]: number } = { | 109 | const 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 | } |
118 | const JOB_TTL: { [ id in JobType ]: number } = { | 121 | const 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 | } |
129 | const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { | 133 | const 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 | ||
295 | const CONSTRAINTS_FIELDS = { | 301 | const 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 | ||
546 | const PRIVATE_RSA_KEY_SIZE = 2048 | 552 | let PRIVATE_RSA_KEY_SIZE = 2048 |
547 | 553 | ||
548 | // Password encryption | 554 | // Password encryption |
549 | const BCRYPT_SALT_SIZE = 10 | 555 | const 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 |
649 | if (isTestInstance() === true) { | 656 | if (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' | |||
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 12 | import { forwardVideoRelatedActivity } from '../send/utils' |
13 | import { Redis } from '../../redis' | 13 | import { Redis } from '../../redis' |
14 | import { createOrUpdateCacheFile } from '../cache-file' | 14 | import { createOrUpdateCacheFile } from '../cache-file' |
15 | import { immutableAssign } from '../../../../shared/utils' | ||
16 | import { getVideoDislikeActivityPubUrl } from '../url' | 15 | import { getVideoDislikeActivityPubUrl } from '../url' |
17 | import { VideoModel } from '../../../models/video/video' | ||
18 | 16 | ||
19 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { | 17 | async 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 | |||
88 | async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { | 86 | async 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 | ||
97 | async function processCacheFile (byActor: ActorModel, activity: ActivityCreate) { | 104 | async 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 | |||
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
6 | import { forwardVideoRelatedActivity } from '../send/utils' | 6 | import { forwardVideoRelatedActivity } from '../send/utils' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { immutableAssign } from '../../../../shared/utils' | 8 | import { getVideoLikeActivityPubUrl } from '../url' |
9 | import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' | ||
10 | 9 | ||
11 | async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { | 10 | async 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 | ||
96 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 96 | function 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 | ||
103 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 102 | function 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 | } |
122 | async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { | 121 | async 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 | ||
301 | async 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 | |||
300 | export { | 350 | export { |
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 | ||
365 | async 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 | |||
411 | async function videoActivityObjectToDBAttributes ( | 416 | async 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 @@ | |||
1 | import * as Bull from 'bull' | ||
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { fetchVideoByUrl } from '../../../helpers/video' | ||
4 | import { refreshVideoIfNeeded } from '../../activitypub' | ||
5 | |||
6 | export type RefreshPayload = { | ||
7 | videoUrl: string | ||
8 | type: 'video' | ||
9 | } | ||
10 | |||
11 | async 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 | |||
21 | export { | ||
22 | refreshAPObject | ||
23 | } | ||
24 | |||
25 | // --------------------------------------------------------------------------- | ||
26 | |||
27 | async 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 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { VideoResolution, VideoState } from '../../../../shared' | 2 | import { VideoResolution, VideoState, Job } from '../../../../shared' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
5 | import { JobQueue } from '../job-queue' | 5 | import { 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 | |||
7 | import { extname, join } from 'path' | 7 | import { extname, join } from 'path' |
8 | import { VideoFileModel } from '../../../models/video/video-file' | 8 | import { VideoFileModel } from '../../../models/video/video-file' |
9 | import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' | 9 | import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' |
10 | import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests' | 10 | import { downloadImage } from '../../../helpers/requests' |
11 | import { VideoState } from '../../../../shared' | 11 | import { VideoState } from '../../../../shared' |
12 | import { JobQueue } from '../index' | 12 | import { JobQueue } from '../index' |
13 | import { federateVideoIfNeeded } from '../../activitypub' | 13 | import { 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 | |||
11 | import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' | 11 | import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' |
12 | import { processVideoImport, VideoImportPayload } from './handlers/video-import' | 12 | import { processVideoImport, VideoImportPayload } from './handlers/video-import' |
13 | import { processVideosViews } from './handlers/video-views' | 13 | import { processVideosViews } from './handlers/video-views' |
14 | import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' | ||
14 | 15 | ||
15 | type CreateJobArgument = | 16 | type 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 | ||
26 | const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { | 28 | const 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 | ||
38 | const jobTypes: JobType[] = [ | 41 | const 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 | ||
50 | class JobQueue { | 54 | class 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 = [ | |||
114 | const usersUpdateValidator = [ | 114 | const 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 { | |||
15 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../activitypub/actor' |
16 | import { getVideoSort, throwIfNotValid } from '../utils' | 16 | import { getVideoSort, throwIfNotValid } from '../utils' |
17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
18 | import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' | 18 | import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers' |
19 | import { VideoFileModel } from '../video/video-file' | 19 | import { VideoFileModel } from '../video/video-file' |
20 | import { getServerActor } from '../../helpers/utils' | 20 | import { getServerActor } from '../../helpers/utils' |
21 | import { VideoModel } from '../video/video' | 21 | import { 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 | ||
141 | type AvailableForListIDsOptions = { | 141 | type 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 @@ | |||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { |
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 | |||
15 | const expect = chai.expect | 16 | const expect = chai.expect |
16 | 17 | ||
17 | describe('Test activitypub', function () { | 18 | describe('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' |
17 | import * as chai from 'chai' | 20 | import * as chai from 'chai' |
18 | import { setActorField, setVideoField } from '../../utils/miscs/sql' | ||
19 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
20 | import { Video } from '../../../../shared/models/videos' | 21 | import { Video } from '../../../../shared/models/videos' |
21 | 22 | ||
22 | const expect = chai.expect | 23 | const 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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { expect } from 'chai' | 4 | import { expect } from 'chai' |
5 | import { buildRequestStub } from '../../utils/miscs/stubs' | 5 | import { buildRequestStub } from '../../../../shared/utils/miscs/stubs' |
6 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' | 6 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' |
7 | import { cloneDeep } from 'lodash' | 7 | import { cloneDeep } from 'lodash' |
8 | import { buildSignedActivity } from '../../../helpers/activitypub' | 8 | import { 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 @@ | |||
1 | import './client' | 1 | import './client' |
2 | import './fetch' | 2 | import './fetch' |
3 | import './helpers' | 3 | import './helpers' |
4 | import './refresher' | ||
4 | import './security' | 5 | import './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 | |||
3 | import 'mocha' | ||
4 | import { | ||
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 | |||
18 | describe('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' |
11 | import { HTTP_SIGNATURE } from '../../../initializers' | 14 | import { HTTP_SIGNATURE } from '../../../initializers' |
12 | import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' | 15 | import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' |
13 | import * as chai from 'chai' | 16 | import * as chai from 'chai' |
14 | import { setActorField } from '../../utils/miscs/sql' | ||
15 | import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' | 17 | import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' |
16 | import { makeFollowRequest, makePOSTAPRequest } from '../../utils/requests/activitypub' | ||
17 | 18 | ||
18 | const expect = chai.expect | 19 | const 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 |
2 | import './create-import-video-file-job' | 2 | import './create-import-video-file-job' |
3 | import './create-transcoding-job' | 3 | import './create-transcoding-job' |
4 | import './optimize-old-videos' | ||
4 | import './peertube' | 5 | import './peertube' |
5 | import './reset-password' | 6 | import './reset-password' |
6 | import './update-host' | 7 | import './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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from '../../shared/utils' | 5 | import { |
6 | addVideoChannel, | ||
7 | createUser, | ||
8 | flushTests, | ||
9 | killallServers, | ||
10 | makeGetRequest, | ||
11 | runServer, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | uploadVideo | ||
15 | } from '../../shared/utils' | ||
16 | import { VideoPrivacy } from '../../shared/models/videos' | ||
6 | 17 | ||
7 | const expect = chai.expect | 18 | const 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | function 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 | |||
19 | function 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 | |||
27 | function 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 | |||
35 | export { | ||
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 @@ | |||
1 | function buildRequestStub (): any { | ||
2 | return { } | ||
3 | } | ||
4 | |||
5 | function buildResponseStub (): any { | ||
6 | return { | ||
7 | locals: {} | ||
8 | } | ||
9 | } | ||
10 | |||
11 | export { | ||
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 @@ | |||
1 | import { doRequest } from '../../../helpers/requests' | ||
2 | import { HTTP_SIGNATURE } from '../../../initializers' | ||
3 | import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' | ||
4 | import { activityPubContextify } from '../../../helpers/activitypub' | ||
5 | |||
6 | function 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 | |||
18 | async 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 | |||
40 | export { | ||
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' | |||
20 | import * as utils from '../helpers/utils' | 20 | import * as utils from '../helpers/utils' |
21 | import * as YoutubeDLUtils from '../helpers/youtube-dl' | 21 | import * as YoutubeDLUtils from '../helpers/youtube-dl' |
22 | 22 | ||
23 | let versionCommitHash | ||
24 | |||
25 | const start = async () => { | 23 | const 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', |