diff options
Diffstat (limited to 'server')
417 files changed, 12345 insertions, 8138 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 1b4acc234..d7de1b9bd 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -30,8 +30,7 @@ import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } | |||
30 | import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' | 30 | import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' |
31 | import { AccountModel } from '../../models/account/account' | 31 | import { AccountModel } from '../../models/account/account' |
32 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 32 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
33 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 33 | import { ActorFollowModel } from '../../models/actor/actor-follow' |
34 | import { VideoModel } from '../../models/video/video' | ||
35 | import { VideoCaptionModel } from '../../models/video/video-caption' | 34 | import { VideoCaptionModel } from '../../models/video/video-caption' |
36 | import { VideoCommentModel } from '../../models/video/video-comment' | 35 | import { VideoCommentModel } from '../../models/video/video-comment' |
37 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 36 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
@@ -44,7 +43,7 @@ activityPubClientRouter.use(cors()) | |||
44 | // Intercept ActivityPub client requests | 43 | // Intercept ActivityPub client requests |
45 | 44 | ||
46 | activityPubClientRouter.get( | 45 | activityPubClientRouter.get( |
47 | [ '/accounts?/:name', '/accounts?/:name/video-channels' ], | 46 | [ '/accounts?/:name', '/accounts?/:name/video-channels', '/a/:name', '/a/:name/video-channels' ], |
48 | executeIfActivityPub, | 47 | executeIfActivityPub, |
49 | asyncMiddleware(localAccountValidator), | 48 | asyncMiddleware(localAccountValidator), |
50 | accountController | 49 | accountController |
@@ -75,15 +74,16 @@ activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', | |||
75 | getAccountVideoRateFactory('dislike') | 74 | getAccountVideoRateFactory('dislike') |
76 | ) | 75 | ) |
77 | 76 | ||
78 | activityPubClientRouter.get('/videos/watch/:id', | 77 | activityPubClientRouter.get( |
78 | [ '/videos/watch/:id', '/w/:id' ], | ||
79 | executeIfActivityPub, | 79 | executeIfActivityPub, |
80 | asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS)), | 80 | asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS)), |
81 | asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), | 81 | asyncMiddleware(videosCustomGetValidator('all')), |
82 | asyncMiddleware(videoController) | 82 | asyncMiddleware(videoController) |
83 | ) | 83 | ) |
84 | activityPubClientRouter.get('/videos/watch/:id/activity', | 84 | activityPubClientRouter.get('/videos/watch/:id/activity', |
85 | executeIfActivityPub, | 85 | executeIfActivityPub, |
86 | asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), | 86 | asyncMiddleware(videosCustomGetValidator('all')), |
87 | asyncMiddleware(videoController) | 87 | asyncMiddleware(videoController) |
88 | ) | 88 | ) |
89 | activityPubClientRouter.get('/videos/watch/:id/announces', | 89 | activityPubClientRouter.get('/videos/watch/:id/announces', |
@@ -123,7 +123,7 @@ activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity | |||
123 | ) | 123 | ) |
124 | 124 | ||
125 | activityPubClientRouter.get( | 125 | activityPubClientRouter.get( |
126 | [ '/video-channels/:name', '/video-channels/:name/videos' ], | 126 | [ '/video-channels/:name', '/video-channels/:name/videos', '/c/:name', '/c/:name/videos' ], |
127 | executeIfActivityPub, | 127 | executeIfActivityPub, |
128 | asyncMiddleware(localVideoChannelValidator), | 128 | asyncMiddleware(localVideoChannelValidator), |
129 | videoChannelController | 129 | videoChannelController |
@@ -155,7 +155,8 @@ activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistT | |||
155 | asyncMiddleware(videoRedundancyController) | 155 | asyncMiddleware(videoRedundancyController) |
156 | ) | 156 | ) |
157 | 157 | ||
158 | activityPubClientRouter.get('/video-playlists/:playlistId', | 158 | activityPubClientRouter.get( |
159 | [ '/video-playlists/:playlistId', '/videos/watch/playlist/:playlistId', '/w/p/:playlistId' ], | ||
159 | executeIfActivityPub, | 160 | executeIfActivityPub, |
160 | asyncMiddleware(videoPlaylistsGetValidator('all')), | 161 | asyncMiddleware(videoPlaylistsGetValidator('all')), |
161 | asyncMiddleware(videoPlaylistController) | 162 | asyncMiddleware(videoPlaylistController) |
@@ -222,8 +223,7 @@ function getAccountVideoRateFactory (rateType: VideoRateType) { | |||
222 | } | 223 | } |
223 | 224 | ||
224 | async function videoController (req: express.Request, res: express.Response) { | 225 | async function videoController (req: express.Request, res: express.Response) { |
225 | // We need more attributes | 226 | const video = res.locals.videoAll |
226 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(res.locals.onlyVideoWithRights.id) | ||
227 | 227 | ||
228 | if (redirectIfNotOwned(video.url, res)) return | 228 | if (redirectIfNotOwned(video.url, res)) return |
229 | 229 | ||
diff --git a/server/controllers/activitypub/utils.ts b/server/controllers/activitypub/utils.ts index 599cf48ab..19bdd58eb 100644 --- a/server/controllers/activitypub/utils.ts +++ b/server/controllers/activitypub/utils.ts | |||
@@ -3,7 +3,6 @@ import * as express from 'express' | |||
3 | function activityPubResponse (data: any, res: express.Response) { | 3 | function activityPubResponse (data: any, res: express.Response) { |
4 | return res.type('application/activity+json; charset=utf-8') | 4 | return res.type('application/activity+json; charset=utf-8') |
5 | .json(data) | 5 | .json(data) |
6 | .end() | ||
7 | } | 6 | } |
8 | 7 | ||
9 | export { | 8 | export { |
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts index 0ab74bdff..ba5b94840 100644 --- a/server/controllers/api/abuse.ts +++ b/server/controllers/api/abuse.ts | |||
@@ -24,6 +24,7 @@ import { | |||
24 | deleteAbuseMessageValidator, | 24 | deleteAbuseMessageValidator, |
25 | ensureUserHasRight, | 25 | ensureUserHasRight, |
26 | getAbuseValidator, | 26 | getAbuseValidator, |
27 | openapiOperationDoc, | ||
27 | paginationValidator, | 28 | paginationValidator, |
28 | setDefaultPagination, | 29 | setDefaultPagination, |
29 | setDefaultSort | 30 | setDefaultSort |
@@ -33,6 +34,7 @@ import { AccountModel } from '../../models/account/account' | |||
33 | const abuseRouter = express.Router() | 34 | const abuseRouter = express.Router() |
34 | 35 | ||
35 | abuseRouter.get('/', | 36 | abuseRouter.get('/', |
37 | openapiOperationDoc({ operationId: 'getAbuses' }), | ||
36 | authenticate, | 38 | authenticate, |
37 | ensureUserHasRight(UserRight.MANAGE_ABUSES), | 39 | ensureUserHasRight(UserRight.MANAGE_ABUSES), |
38 | paginationValidator, | 40 | paginationValidator, |
@@ -142,7 +144,7 @@ async function updateAbuse (req: express.Request, res: express.Response) { | |||
142 | 144 | ||
143 | // Do not send the delete to other instances, we updated OUR copy of this abuse | 145 | // Do not send the delete to other instances, we updated OUR copy of this abuse |
144 | 146 | ||
145 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 147 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
146 | } | 148 | } |
147 | 149 | ||
148 | async function deleteAbuse (req: express.Request, res: express.Response) { | 150 | async function deleteAbuse (req: express.Request, res: express.Response) { |
@@ -154,7 +156,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) { | |||
154 | 156 | ||
155 | // Do not send the delete to other instances, we delete OUR copy of this abuse | 157 | // Do not send the delete to other instances, we delete OUR copy of this abuse |
156 | 158 | ||
157 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 159 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
158 | } | 160 | } |
159 | 161 | ||
160 | async function reportAbuse (req: express.Request, res: express.Response) { | 162 | async function reportAbuse (req: express.Request, res: express.Response) { |
@@ -244,5 +246,5 @@ async function deleteAbuseMessage (req: express.Request, res: express.Response) | |||
244 | return abuseMessage.destroy({ transaction: t }) | 246 | return abuseMessage.destroy({ transaction: t }) |
245 | }) | 247 | }) |
246 | 248 | ||
247 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 249 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
248 | } | 250 | } |
diff --git a/server/controllers/api/bulk.ts b/server/controllers/api/bulk.ts index 649351029..192daccde 100644 --- a/server/controllers/api/bulk.ts +++ b/server/controllers/api/bulk.ts | |||
@@ -34,7 +34,7 @@ async function bulkRemoveCommentsOf (req: express.Request, res: express.Response | |||
34 | const comments = await VideoCommentModel.listForBulkDelete(account, filter) | 34 | const comments = await VideoCommentModel.listForBulkDelete(account, filter) |
35 | 35 | ||
36 | // Don't wait result | 36 | // Don't wait result |
37 | res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 37 | res.status(HttpStatusCode.NO_CONTENT_204).end() |
38 | 38 | ||
39 | for (const comment of comments) { | 39 | for (const comment of comments) { |
40 | await removeComment(comment) | 40 | await removeComment(comment) |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 2ddb73519..9bd8c21c5 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { ServerConfigManager } from '@server/lib/server-config-manager' | ||
1 | import * as express from 'express' | 2 | import * as express from 'express' |
2 | import { remove, writeJSON } from 'fs-extra' | 3 | import { remove, writeJSON } from 'fs-extra' |
3 | import { snakeCase } from 'lodash' | 4 | import { snakeCase } from 'lodash' |
4 | import validator from 'validator' | 5 | import validator from 'validator' |
5 | import { getServerConfig } from '@server/lib/config' | ||
6 | import { UserRight } from '../../../shared' | 6 | import { UserRight } from '../../../shared' |
7 | import { About } from '../../../shared/models/server/about.model' | 7 | import { About } from '../../../shared/models/server/about.model' |
8 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 8 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
@@ -10,37 +10,47 @@ import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '.. | |||
10 | import { objectConverter } from '../../helpers/core-utils' | 10 | import { objectConverter } from '../../helpers/core-utils' |
11 | import { CONFIG, reloadConfig } from '../../initializers/config' | 11 | import { CONFIG, reloadConfig } from '../../initializers/config' |
12 | import { ClientHtml } from '../../lib/client-html' | 12 | import { ClientHtml } from '../../lib/client-html' |
13 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' | 13 | import { asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares' |
14 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' | 14 | import { customConfigUpdateValidator } from '../../middlewares/validators/config' |
15 | 15 | ||
16 | const configRouter = express.Router() | 16 | const configRouter = express.Router() |
17 | 17 | ||
18 | const auditLogger = auditLoggerFactory('config') | 18 | const auditLogger = auditLoggerFactory('config') |
19 | 19 | ||
20 | configRouter.get('/about', getAbout) | ||
21 | configRouter.get('/', | 20 | configRouter.get('/', |
21 | openapiOperationDoc({ operationId: 'getConfig' }), | ||
22 | asyncMiddleware(getConfig) | 22 | asyncMiddleware(getConfig) |
23 | ) | 23 | ) |
24 | 24 | ||
25 | configRouter.get('/about', | ||
26 | openapiOperationDoc({ operationId: 'getAbout' }), | ||
27 | getAbout | ||
28 | ) | ||
29 | |||
25 | configRouter.get('/custom', | 30 | configRouter.get('/custom', |
31 | openapiOperationDoc({ operationId: 'getCustomConfig' }), | ||
26 | authenticate, | 32 | authenticate, |
27 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), | 33 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), |
28 | getCustomConfig | 34 | getCustomConfig |
29 | ) | 35 | ) |
36 | |||
30 | configRouter.put('/custom', | 37 | configRouter.put('/custom', |
38 | openapiOperationDoc({ operationId: 'putCustomConfig' }), | ||
31 | authenticate, | 39 | authenticate, |
32 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), | 40 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), |
33 | customConfigUpdateValidator, | 41 | customConfigUpdateValidator, |
34 | asyncMiddleware(updateCustomConfig) | 42 | asyncMiddleware(updateCustomConfig) |
35 | ) | 43 | ) |
44 | |||
36 | configRouter.delete('/custom', | 45 | configRouter.delete('/custom', |
46 | openapiOperationDoc({ operationId: 'delCustomConfig' }), | ||
37 | authenticate, | 47 | authenticate, |
38 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), | 48 | ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), |
39 | asyncMiddleware(deleteCustomConfig) | 49 | asyncMiddleware(deleteCustomConfig) |
40 | ) | 50 | ) |
41 | 51 | ||
42 | async function getConfig (req: express.Request, res: express.Response) { | 52 | async function getConfig (req: express.Request, res: express.Response) { |
43 | const json = await getServerConfig(req.ip) | 53 | const json = await ServerConfigManager.Instance.getServerConfig(req.ip) |
44 | 54 | ||
45 | return res.json(json) | 55 | return res.json(json) |
46 | } | 56 | } |
@@ -67,13 +77,13 @@ function getAbout (req: express.Request, res: express.Response) { | |||
67 | } | 77 | } |
68 | } | 78 | } |
69 | 79 | ||
70 | return res.json(about).end() | 80 | return res.json(about) |
71 | } | 81 | } |
72 | 82 | ||
73 | function getCustomConfig (req: express.Request, res: express.Response) { | 83 | function getCustomConfig (req: express.Request, res: express.Response) { |
74 | const data = customConfig() | 84 | const data = customConfig() |
75 | 85 | ||
76 | return res.json(data).end() | 86 | return res.json(data) |
77 | } | 87 | } |
78 | 88 | ||
79 | async function deleteCustomConfig (req: express.Request, res: express.Response) { | 89 | async function deleteCustomConfig (req: express.Request, res: express.Response) { |
@@ -171,7 +181,8 @@ function customConfig (): CustomConfig { | |||
171 | signup: { | 181 | signup: { |
172 | enabled: CONFIG.SIGNUP.ENABLED, | 182 | enabled: CONFIG.SIGNUP.ENABLED, |
173 | limit: CONFIG.SIGNUP.LIMIT, | 183 | limit: CONFIG.SIGNUP.LIMIT, |
174 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | 184 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION, |
185 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE | ||
175 | }, | 186 | }, |
176 | admin: { | 187 | admin: { |
177 | email: CONFIG.ADMIN.EMAIL | 188 | email: CONFIG.ADMIN.EMAIL |
diff --git a/server/controllers/api/custom-page.ts b/server/controllers/api/custom-page.ts new file mode 100644 index 000000000..c19f03c56 --- /dev/null +++ b/server/controllers/api/custom-page.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import * as express from 'express' | ||
2 | import { ServerConfigManager } from '@server/lib/server-config-manager' | ||
3 | import { ActorCustomPageModel } from '@server/models/account/actor-custom-page' | ||
4 | import { HttpStatusCode } from '@shared/core-utils' | ||
5 | import { UserRight } from '@shared/models' | ||
6 | import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' | ||
7 | |||
8 | const customPageRouter = express.Router() | ||
9 | |||
10 | customPageRouter.get('/homepage/instance', | ||
11 | asyncMiddleware(getInstanceHomepage) | ||
12 | ) | ||
13 | |||
14 | customPageRouter.put('/homepage/instance', | ||
15 | authenticate, | ||
16 | ensureUserHasRight(UserRight.MANAGE_INSTANCE_CUSTOM_PAGE), | ||
17 | asyncMiddleware(updateInstanceHomepage) | ||
18 | ) | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | customPageRouter | ||
24 | } | ||
25 | |||
26 | // --------------------------------------------------------------------------- | ||
27 | |||
28 | async function getInstanceHomepage (req: express.Request, res: express.Response) { | ||
29 | const page = await ActorCustomPageModel.loadInstanceHomepage() | ||
30 | if (!page) { | ||
31 | return res.fail({ | ||
32 | status: HttpStatusCode.NOT_FOUND_404, | ||
33 | message: 'Instance homepage could not be found' | ||
34 | }) | ||
35 | } | ||
36 | |||
37 | return res.json(page.toFormattedJSON()) | ||
38 | } | ||
39 | |||
40 | async function updateInstanceHomepage (req: express.Request, res: express.Response) { | ||
41 | const content = req.body.content | ||
42 | |||
43 | await ActorCustomPageModel.updateInstanceHomepage(content) | ||
44 | ServerConfigManager.Instance.updateHomepageState(content) | ||
45 | |||
46 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
47 | } | ||
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 7ade1df3a..28378654a 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts | |||
@@ -8,6 +8,7 @@ import { abuseRouter } from './abuse' | |||
8 | import { accountsRouter } from './accounts' | 8 | import { accountsRouter } from './accounts' |
9 | import { bulkRouter } from './bulk' | 9 | import { bulkRouter } from './bulk' |
10 | import { configRouter } from './config' | 10 | import { configRouter } from './config' |
11 | import { customPageRouter } from './custom-page' | ||
11 | import { jobsRouter } from './jobs' | 12 | import { jobsRouter } from './jobs' |
12 | import { oauthClientsRouter } from './oauth-clients' | 13 | import { oauthClientsRouter } from './oauth-clients' |
13 | import { overviewsRouter } from './overviews' | 14 | import { overviewsRouter } from './overviews' |
@@ -47,6 +48,7 @@ apiRouter.use('/jobs', jobsRouter) | |||
47 | apiRouter.use('/search', searchRouter) | 48 | apiRouter.use('/search', searchRouter) |
48 | apiRouter.use('/overviews', overviewsRouter) | 49 | apiRouter.use('/overviews', overviewsRouter) |
49 | apiRouter.use('/plugins', pluginRouter) | 50 | apiRouter.use('/plugins', pluginRouter) |
51 | apiRouter.use('/custom-pages', customPageRouter) | ||
50 | apiRouter.use('/ping', pong) | 52 | apiRouter.use('/ping', pong) |
51 | apiRouter.use('/*', badRequest) | 53 | apiRouter.use('/*', badRequest) |
52 | 54 | ||
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts index d7cee1605..9e333322b 100644 --- a/server/controllers/api/jobs.ts +++ b/server/controllers/api/jobs.ts | |||
@@ -9,6 +9,7 @@ import { | |||
9 | authenticate, | 9 | authenticate, |
10 | ensureUserHasRight, | 10 | ensureUserHasRight, |
11 | jobsSortValidator, | 11 | jobsSortValidator, |
12 | openapiOperationDoc, | ||
12 | paginationValidatorBuilder, | 13 | paginationValidatorBuilder, |
13 | setDefaultPagination, | 14 | setDefaultPagination, |
14 | setDefaultSort | 15 | setDefaultSort |
@@ -18,6 +19,7 @@ import { listJobsValidator } from '../../middlewares/validators/jobs' | |||
18 | const jobsRouter = express.Router() | 19 | const jobsRouter = express.Router() |
19 | 20 | ||
20 | jobsRouter.get('/:state?', | 21 | jobsRouter.get('/:state?', |
22 | openapiOperationDoc({ operationId: 'getJobs' }), | ||
21 | authenticate, | 23 | authenticate, |
22 | ensureUserHasRight(UserRight.MANAGE_JOBS), | 24 | ensureUserHasRight(UserRight.MANAGE_JOBS), |
23 | paginationValidatorBuilder([ 'jobs' ]), | 25 | paginationValidatorBuilder([ 'jobs' ]), |
diff --git a/server/controllers/api/oauth-clients.ts b/server/controllers/api/oauth-clients.ts index c21e2298d..15bbf5c4d 100644 --- a/server/controllers/api/oauth-clients.ts +++ b/server/controllers/api/oauth-clients.ts | |||
@@ -3,12 +3,13 @@ import { OAuthClientLocal } from '../../../shared' | |||
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { CONFIG } from '../../initializers/config' | 5 | import { CONFIG } from '../../initializers/config' |
6 | import { asyncMiddleware } from '../../middlewares' | 6 | import { asyncMiddleware, openapiOperationDoc } from '../../middlewares' |
7 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 7 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
8 | 8 | ||
9 | const oauthClientsRouter = express.Router() | 9 | const oauthClientsRouter = express.Router() |
10 | 10 | ||
11 | oauthClientsRouter.get('/local', | 11 | oauthClientsRouter.get('/local', |
12 | openapiOperationDoc({ operationId: 'getOAuthClient' }), | ||
12 | asyncMiddleware(getLocalClient) | 13 | asyncMiddleware(getLocalClient) |
13 | ) | 14 | ) |
14 | 15 | ||
@@ -24,7 +25,10 @@ async function getLocalClient (req: express.Request, res: express.Response, next | |||
24 | // Don't make this check if this is a test instance | 25 | // Don't make this check if this is a test instance |
25 | if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { | 26 | if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { |
26 | logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) | 27 | logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) |
27 | return res.type('json').status(HttpStatusCode.FORBIDDEN_403).end() | 28 | return res.fail({ |
29 | status: HttpStatusCode.FORBIDDEN_403, | ||
30 | message: `Getting client tokens for host ${req.get('host')} is forbidden` | ||
31 | }) | ||
28 | } | 32 | } |
29 | 33 | ||
30 | const client = await OAuthClientModel.loadFirstClient() | 34 | const client = await OAuthClientModel.loadFirstClient() |
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index a186de010..1e6a02c49 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts | |||
@@ -1,16 +1,19 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { getFormattedObjects } from '../../helpers/utils' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { getFormattedObjects } from '@server/helpers/utils' | ||
4 | import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index' | ||
5 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | ||
3 | import { | 6 | import { |
4 | asyncMiddleware, | 7 | asyncMiddleware, |
5 | authenticate, | 8 | authenticate, |
9 | availablePluginsSortValidator, | ||
6 | ensureUserHasRight, | 10 | ensureUserHasRight, |
11 | openapiOperationDoc, | ||
7 | paginationValidator, | 12 | paginationValidator, |
13 | pluginsSortValidator, | ||
8 | setDefaultPagination, | 14 | setDefaultPagination, |
9 | setDefaultSort | 15 | setDefaultSort |
10 | } from '../../middlewares' | 16 | } from '@server/middlewares' |
11 | import { availablePluginsSortValidator, pluginsSortValidator } from '../../middlewares/validators' | ||
12 | import { PluginModel } from '../../models/server/plugin' | ||
13 | import { UserRight } from '../../../shared/models/users' | ||
14 | import { | 17 | import { |
15 | existingPluginValidator, | 18 | existingPluginValidator, |
16 | installOrUpdatePluginValidator, | 19 | installOrUpdatePluginValidator, |
@@ -18,20 +21,22 @@ import { | |||
18 | listPluginsValidator, | 21 | listPluginsValidator, |
19 | uninstallPluginValidator, | 22 | uninstallPluginValidator, |
20 | updatePluginSettingsValidator | 23 | updatePluginSettingsValidator |
21 | } from '../../middlewares/validators/plugins' | 24 | } from '@server/middlewares/validators/plugins' |
22 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 25 | import { PluginModel } from '@server/models/server/plugin' |
23 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' | 26 | import { HttpStatusCode } from '@shared/core-utils' |
24 | import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' | 27 | import { |
25 | import { logger } from '../../helpers/logger' | 28 | InstallOrUpdatePlugin, |
26 | import { listAvailablePluginsFromIndex } from '../../lib/plugins/plugin-index' | 29 | ManagePlugin, |
27 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | 30 | PeertubePluginIndexList, |
28 | import { RegisteredServerSettings } from '../../../shared/models/plugins/register-server-setting.model' | 31 | PublicServerSetting, |
29 | import { PublicServerSetting } from '../../../shared/models/plugins/public-server.setting' | 32 | RegisteredServerSettings, |
30 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 33 | UserRight |
34 | } from '@shared/models' | ||
31 | 35 | ||
32 | const pluginRouter = express.Router() | 36 | const pluginRouter = express.Router() |
33 | 37 | ||
34 | pluginRouter.get('/available', | 38 | pluginRouter.get('/available', |
39 | openapiOperationDoc({ operationId: 'getAvailablePlugins' }), | ||
35 | authenticate, | 40 | authenticate, |
36 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 41 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
37 | listAvailablePluginsValidator, | 42 | listAvailablePluginsValidator, |
@@ -43,6 +48,7 @@ pluginRouter.get('/available', | |||
43 | ) | 48 | ) |
44 | 49 | ||
45 | pluginRouter.get('/', | 50 | pluginRouter.get('/', |
51 | openapiOperationDoc({ operationId: 'getPlugins' }), | ||
46 | authenticate, | 52 | authenticate, |
47 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 53 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
48 | listPluginsValidator, | 54 | listPluginsValidator, |
@@ -81,6 +87,7 @@ pluginRouter.get('/:npmName', | |||
81 | ) | 87 | ) |
82 | 88 | ||
83 | pluginRouter.post('/install', | 89 | pluginRouter.post('/install', |
90 | openapiOperationDoc({ operationId: 'addPlugin' }), | ||
84 | authenticate, | 91 | authenticate, |
85 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 92 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
86 | installOrUpdatePluginValidator, | 93 | installOrUpdatePluginValidator, |
@@ -88,6 +95,7 @@ pluginRouter.post('/install', | |||
88 | ) | 95 | ) |
89 | 96 | ||
90 | pluginRouter.post('/update', | 97 | pluginRouter.post('/update', |
98 | openapiOperationDoc({ operationId: 'updatePlugin' }), | ||
91 | authenticate, | 99 | authenticate, |
92 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 100 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
93 | installOrUpdatePluginValidator, | 101 | installOrUpdatePluginValidator, |
@@ -95,6 +103,7 @@ pluginRouter.post('/update', | |||
95 | ) | 103 | ) |
96 | 104 | ||
97 | pluginRouter.post('/uninstall', | 105 | pluginRouter.post('/uninstall', |
106 | openapiOperationDoc({ operationId: 'uninstallPlugin' }), | ||
98 | authenticate, | 107 | authenticate, |
99 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), | 108 | ensureUserHasRight(UserRight.MANAGE_PLUGINS), |
100 | uninstallPluginValidator, | 109 | uninstallPluginValidator, |
@@ -141,7 +150,7 @@ async function installPlugin (req: express.Request, res: express.Response) { | |||
141 | return res.json(plugin.toFormattedJSON()) | 150 | return res.json(plugin.toFormattedJSON()) |
142 | } catch (err) { | 151 | } catch (err) { |
143 | logger.warn('Cannot install plugin %s.', toInstall, { err }) | 152 | logger.warn('Cannot install plugin %s.', toInstall, { err }) |
144 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | 153 | return res.fail({ message: 'Cannot install plugin ' + toInstall }) |
145 | } | 154 | } |
146 | } | 155 | } |
147 | 156 | ||
@@ -156,7 +165,7 @@ async function updatePlugin (req: express.Request, res: express.Response) { | |||
156 | return res.json(plugin.toFormattedJSON()) | 165 | return res.json(plugin.toFormattedJSON()) |
157 | } catch (err) { | 166 | } catch (err) { |
158 | logger.warn('Cannot update plugin %s.', toUpdate, { err }) | 167 | logger.warn('Cannot update plugin %s.', toUpdate, { err }) |
159 | return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) | 168 | return res.fail({ message: 'Cannot update plugin ' + toUpdate }) |
160 | } | 169 | } |
161 | } | 170 | } |
162 | 171 | ||
@@ -165,7 +174,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) { | |||
165 | 174 | ||
166 | await PluginManager.Instance.uninstall(body.npmName) | 175 | await PluginManager.Instance.uninstall(body.npmName) |
167 | 176 | ||
168 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 177 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
169 | } | 178 | } |
170 | 179 | ||
171 | function getPublicPluginSettings (req: express.Request, res: express.Response) { | 180 | function getPublicPluginSettings (req: express.Request, res: express.Response) { |
@@ -194,7 +203,7 @@ async function updatePluginSettings (req: express.Request, res: express.Response | |||
194 | 203 | ||
195 | await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings) | 204 | await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings) |
196 | 205 | ||
197 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 206 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
198 | } | 207 | } |
199 | 208 | ||
200 | async function listAvailablePlugins (req: express.Request, res: express.Response) { | 209 | async function listAvailablePlugins (req: express.Request, res: express.Response) { |
@@ -203,8 +212,10 @@ async function listAvailablePlugins (req: express.Request, res: express.Response | |||
203 | const resultList = await listAvailablePluginsFromIndex(query) | 212 | const resultList = await listAvailablePluginsFromIndex(query) |
204 | 213 | ||
205 | if (!resultList) { | 214 | if (!resultList) { |
206 | return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) | 215 | return res.fail({ |
207 | .json({ error: 'Plugin index unavailable. Please retry later' }) | 216 | status: HttpStatusCode.SERVICE_UNAVAILABLE_503, |
217 | message: 'Plugin index unavailable. Please retry later' | ||
218 | }) | ||
208 | } | 219 | } |
209 | 220 | ||
210 | return res.json(resultList) | 221 | return res.json(resultList) |
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts deleted file mode 100644 index f0cdf3a89..000000000 --- a/server/controllers/api/search.ts +++ /dev/null | |||
@@ -1,286 +0,0 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | ||
8 | import { getServerActor } from '@server/models/application/application' | ||
9 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
11 | import { ResultList, Video, VideoChannel } from '@shared/models' | ||
12 | import { SearchTargetQuery } from '@shared/models/search/search-target-query.model' | ||
13 | import { VideoChannelsSearchQuery, VideosSearchQuery } from '../../../shared/models/search' | ||
14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | ||
15 | import { logger } from '../../helpers/logger' | ||
16 | import { getFormattedObjects } from '../../helpers/utils' | ||
17 | import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' | ||
18 | import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub/actor' | ||
19 | import { | ||
20 | asyncMiddleware, | ||
21 | commonVideosFiltersValidator, | ||
22 | optionalAuthenticate, | ||
23 | paginationValidator, | ||
24 | setDefaultPagination, | ||
25 | setDefaultSearchSort, | ||
26 | videoChannelsListSearchValidator, | ||
27 | videoChannelsSearchSortValidator, | ||
28 | videosSearchSortValidator, | ||
29 | videosSearchValidator | ||
30 | } from '../../middlewares' | ||
31 | import { VideoModel } from '../../models/video/video' | ||
32 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
33 | import { MChannelAccountDefault, MVideoAccountLightBlacklistAllFiles } from '../../types/models' | ||
34 | |||
35 | const searchRouter = express.Router() | ||
36 | |||
37 | searchRouter.get('/videos', | ||
38 | paginationValidator, | ||
39 | setDefaultPagination, | ||
40 | videosSearchSortValidator, | ||
41 | setDefaultSearchSort, | ||
42 | optionalAuthenticate, | ||
43 | commonVideosFiltersValidator, | ||
44 | videosSearchValidator, | ||
45 | asyncMiddleware(searchVideos) | ||
46 | ) | ||
47 | |||
48 | searchRouter.get('/video-channels', | ||
49 | paginationValidator, | ||
50 | setDefaultPagination, | ||
51 | videoChannelsSearchSortValidator, | ||
52 | setDefaultSearchSort, | ||
53 | optionalAuthenticate, | ||
54 | videoChannelsListSearchValidator, | ||
55 | asyncMiddleware(searchVideoChannels) | ||
56 | ) | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
59 | |||
60 | export { searchRouter } | ||
61 | |||
62 | // --------------------------------------------------------------------------- | ||
63 | |||
64 | function searchVideoChannels (req: express.Request, res: express.Response) { | ||
65 | const query: VideoChannelsSearchQuery = req.query | ||
66 | const search = query.search | ||
67 | |||
68 | const isURISearch = search.startsWith('http://') || search.startsWith('https://') | ||
69 | |||
70 | const parts = search.split('@') | ||
71 | |||
72 | // Handle strings like @toto@example.com | ||
73 | if (parts.length === 3 && parts[0].length === 0) parts.shift() | ||
74 | const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' ')) | ||
75 | |||
76 | if (isURISearch || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) | ||
77 | |||
78 | // @username -> username to search in DB | ||
79 | if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') | ||
80 | |||
81 | if (isSearchIndexSearch(query)) { | ||
82 | return searchVideoChannelsIndex(query, res) | ||
83 | } | ||
84 | |||
85 | return searchVideoChannelsDB(query, res) | ||
86 | } | ||
87 | |||
88 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { | ||
89 | const result = await buildMutedForSearchIndex(res) | ||
90 | |||
91 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') | ||
92 | |||
93 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' | ||
94 | |||
95 | try { | ||
96 | logger.debug('Doing video channels search index request on %s.', url, { body }) | ||
97 | |||
98 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body }) | ||
99 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') | ||
100 | |||
101 | return res.json(jsonResult) | ||
102 | } catch (err) { | ||
103 | logger.warn('Cannot use search index to make video channels search.', { err }) | ||
104 | |||
105 | return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) | ||
106 | } | ||
107 | } | ||
108 | |||
109 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { | ||
110 | const serverActor = await getServerActor() | ||
111 | |||
112 | const apiOptions = await Hooks.wrapObject({ | ||
113 | actorId: serverActor.id, | ||
114 | search: query.search, | ||
115 | start: query.start, | ||
116 | count: query.count, | ||
117 | sort: query.sort | ||
118 | }, 'filter:api.search.video-channels.local.list.params') | ||
119 | |||
120 | const resultList = await Hooks.wrapPromiseFun( | ||
121 | VideoChannelModel.searchForApi, | ||
122 | apiOptions, | ||
123 | 'filter:api.search.video-channels.local.list.result' | ||
124 | ) | ||
125 | |||
126 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
127 | } | ||
128 | |||
129 | async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { | ||
130 | let videoChannel: MChannelAccountDefault | ||
131 | let uri = search | ||
132 | |||
133 | if (isWebfingerSearch) { | ||
134 | try { | ||
135 | uri = await loadActorUrlOrGetFromWebfinger(search) | ||
136 | } catch (err) { | ||
137 | logger.warn('Cannot load actor URL or get from webfinger.', { search, err }) | ||
138 | |||
139 | return res.json({ total: 0, data: [] }) | ||
140 | } | ||
141 | } | ||
142 | |||
143 | if (isUserAbleToSearchRemoteURI(res)) { | ||
144 | try { | ||
145 | const actor = await getOrCreateActorAndServerAndModel(uri, 'all', true, true) | ||
146 | videoChannel = actor.VideoChannel | ||
147 | } catch (err) { | ||
148 | logger.info('Cannot search remote video channel %s.', uri, { err }) | ||
149 | } | ||
150 | } else { | ||
151 | videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(uri) | ||
152 | } | ||
153 | |||
154 | return res.json({ | ||
155 | total: videoChannel ? 1 : 0, | ||
156 | data: videoChannel ? [ videoChannel.toFormattedJSON() ] : [] | ||
157 | }) | ||
158 | } | ||
159 | |||
160 | function searchVideos (req: express.Request, res: express.Response) { | ||
161 | const query: VideosSearchQuery = req.query | ||
162 | const search = query.search | ||
163 | |||
164 | if (search && (search.startsWith('http://') || search.startsWith('https://'))) { | ||
165 | return searchVideoURI(search, res) | ||
166 | } | ||
167 | |||
168 | if (isSearchIndexSearch(query)) { | ||
169 | return searchVideosIndex(query, res) | ||
170 | } | ||
171 | |||
172 | return searchVideosDB(query, res) | ||
173 | } | ||
174 | |||
175 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { | ||
176 | const result = await buildMutedForSearchIndex(res) | ||
177 | |||
178 | let body: VideosSearchQuery = Object.assign(query, result) | ||
179 | |||
180 | // Use the default instance NSFW policy if not specified | ||
181 | if (!body.nsfw) { | ||
182 | const nsfwPolicy = res.locals.oauth | ||
183 | ? res.locals.oauth.token.User.nsfwPolicy | ||
184 | : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY | ||
185 | |||
186 | body.nsfw = nsfwPolicy === 'do_not_list' | ||
187 | ? 'false' | ||
188 | : 'both' | ||
189 | } | ||
190 | |||
191 | body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') | ||
192 | |||
193 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' | ||
194 | |||
195 | try { | ||
196 | logger.debug('Doing videos search index request on %s.', url, { body }) | ||
197 | |||
198 | const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body }) | ||
199 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') | ||
200 | |||
201 | return res.json(jsonResult) | ||
202 | } catch (err) { | ||
203 | logger.warn('Cannot use search index to make video search.', { err }) | ||
204 | |||
205 | return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) | ||
206 | } | ||
207 | } | ||
208 | |||
209 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | ||
210 | const apiOptions = await Hooks.wrapObject(Object.assign(query, { | ||
211 | includeLocalVideos: true, | ||
212 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
213 | filter: query.filter, | ||
214 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
215 | }), 'filter:api.search.videos.local.list.params') | ||
216 | |||
217 | const resultList = await Hooks.wrapPromiseFun( | ||
218 | VideoModel.searchAndPopulateAccountAndServer, | ||
219 | apiOptions, | ||
220 | 'filter:api.search.videos.local.list.result' | ||
221 | ) | ||
222 | |||
223 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
224 | } | ||
225 | |||
226 | async function searchVideoURI (url: string, res: express.Response) { | ||
227 | let video: MVideoAccountLightBlacklistAllFiles | ||
228 | |||
229 | // Check if we can fetch a remote video with the URL | ||
230 | if (isUserAbleToSearchRemoteURI(res)) { | ||
231 | try { | ||
232 | const syncParam = { | ||
233 | likes: false, | ||
234 | dislikes: false, | ||
235 | shares: false, | ||
236 | comments: false, | ||
237 | thumbnail: true, | ||
238 | refreshVideo: false | ||
239 | } | ||
240 | |||
241 | const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam }) | ||
242 | video = result ? result.video : undefined | ||
243 | } catch (err) { | ||
244 | logger.info('Cannot search remote video %s.', url, { err }) | ||
245 | } | ||
246 | } else { | ||
247 | video = await VideoModel.loadByUrlAndPopulateAccount(url) | ||
248 | } | ||
249 | |||
250 | return res.json({ | ||
251 | total: video ? 1 : 0, | ||
252 | data: video ? [ video.toFormattedJSON() ] : [] | ||
253 | }) | ||
254 | } | ||
255 | |||
256 | function isSearchIndexSearch (query: SearchTargetQuery) { | ||
257 | if (query.searchTarget === 'search-index') return true | ||
258 | |||
259 | const searchIndexConfig = CONFIG.SEARCH.SEARCH_INDEX | ||
260 | |||
261 | if (searchIndexConfig.ENABLED !== true) return false | ||
262 | |||
263 | if (searchIndexConfig.DISABLE_LOCAL_SEARCH) return true | ||
264 | if (searchIndexConfig.IS_DEFAULT_SEARCH && !query.searchTarget) return true | ||
265 | |||
266 | return false | ||
267 | } | ||
268 | |||
269 | async function buildMutedForSearchIndex (res: express.Response) { | ||
270 | const serverActor = await getServerActor() | ||
271 | const accountIds = [ serverActor.Account.id ] | ||
272 | |||
273 | if (res.locals.oauth) { | ||
274 | accountIds.push(res.locals.oauth.token.User.Account.id) | ||
275 | } | ||
276 | |||
277 | const [ blockedHosts, blockedAccounts ] = await Promise.all([ | ||
278 | ServerBlocklistModel.listHostsBlockedBy(accountIds), | ||
279 | AccountBlocklistModel.listHandlesBlockedBy(accountIds) | ||
280 | ]) | ||
281 | |||
282 | return { | ||
283 | blockedHosts, | ||
284 | blockedAccounts | ||
285 | } | ||
286 | } | ||
diff --git a/server/controllers/api/search/index.ts b/server/controllers/api/search/index.ts new file mode 100644 index 000000000..67adbb307 --- /dev/null +++ b/server/controllers/api/search/index.ts | |||
@@ -0,0 +1,16 @@ | |||
1 | import * as express from 'express' | ||
2 | import { searchChannelsRouter } from './search-video-channels' | ||
3 | import { searchPlaylistsRouter } from './search-video-playlists' | ||
4 | import { searchVideosRouter } from './search-videos' | ||
5 | |||
6 | const searchRouter = express.Router() | ||
7 | |||
8 | searchRouter.use('/', searchVideosRouter) | ||
9 | searchRouter.use('/', searchChannelsRouter) | ||
10 | searchRouter.use('/', searchPlaylistsRouter) | ||
11 | |||
12 | // --------------------------------------------------------------------------- | ||
13 | |||
14 | export { | ||
15 | searchRouter | ||
16 | } | ||
diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts new file mode 100644 index 000000000..16beeed60 --- /dev/null +++ b/server/controllers/api/search/search-video-channels.ts | |||
@@ -0,0 +1,150 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { WEBSERVER } from '@server/initializers/constants' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | ||
8 | import { getServerActor } from '@server/models/application/application' | ||
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
10 | import { ResultList, VideoChannel } from '@shared/models' | ||
11 | import { VideoChannelsSearchQuery } from '../../../../shared/models/search' | ||
12 | import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' | ||
13 | import { logger } from '../../../helpers/logger' | ||
14 | import { getFormattedObjects } from '../../../helpers/utils' | ||
15 | import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../../lib/activitypub/actors' | ||
16 | import { | ||
17 | asyncMiddleware, | ||
18 | openapiOperationDoc, | ||
19 | optionalAuthenticate, | ||
20 | paginationValidator, | ||
21 | setDefaultPagination, | ||
22 | setDefaultSearchSort, | ||
23 | videoChannelsListSearchValidator, | ||
24 | videoChannelsSearchSortValidator | ||
25 | } from '../../../middlewares' | ||
26 | import { VideoChannelModel } from '../../../models/video/video-channel' | ||
27 | import { MChannelAccountDefault } from '../../../types/models' | ||
28 | |||
29 | const searchChannelsRouter = express.Router() | ||
30 | |||
31 | searchChannelsRouter.get('/video-channels', | ||
32 | openapiOperationDoc({ operationId: 'searchChannels' }), | ||
33 | paginationValidator, | ||
34 | setDefaultPagination, | ||
35 | videoChannelsSearchSortValidator, | ||
36 | setDefaultSearchSort, | ||
37 | optionalAuthenticate, | ||
38 | videoChannelsListSearchValidator, | ||
39 | asyncMiddleware(searchVideoChannels) | ||
40 | ) | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | export { searchChannelsRouter } | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | function searchVideoChannels (req: express.Request, res: express.Response) { | ||
49 | const query: VideoChannelsSearchQuery = req.query | ||
50 | const search = query.search | ||
51 | |||
52 | const parts = search.split('@') | ||
53 | |||
54 | // Handle strings like @toto@example.com | ||
55 | if (parts.length === 3 && parts[0].length === 0) parts.shift() | ||
56 | const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' ')) | ||
57 | |||
58 | if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) | ||
59 | |||
60 | // @username -> username to search in DB | ||
61 | if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') | ||
62 | |||
63 | if (isSearchIndexSearch(query)) { | ||
64 | return searchVideoChannelsIndex(query, res) | ||
65 | } | ||
66 | |||
67 | return searchVideoChannelsDB(query, res) | ||
68 | } | ||
69 | |||
70 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { | ||
71 | const result = await buildMutedForSearchIndex(res) | ||
72 | |||
73 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') | ||
74 | |||
75 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' | ||
76 | |||
77 | try { | ||
78 | logger.debug('Doing video channels search index request on %s.', url, { body }) | ||
79 | |||
80 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body }) | ||
81 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') | ||
82 | |||
83 | return res.json(jsonResult) | ||
84 | } catch (err) { | ||
85 | logger.warn('Cannot use search index to make video channels search.', { err }) | ||
86 | |||
87 | return res.fail({ | ||
88 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
89 | message: 'Cannot use search index to make video channels search' | ||
90 | }) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { | ||
95 | const serverActor = await getServerActor() | ||
96 | |||
97 | const apiOptions = await Hooks.wrapObject({ | ||
98 | actorId: serverActor.id, | ||
99 | search: query.search, | ||
100 | start: query.start, | ||
101 | count: query.count, | ||
102 | sort: query.sort | ||
103 | }, 'filter:api.search.video-channels.local.list.params') | ||
104 | |||
105 | const resultList = await Hooks.wrapPromiseFun( | ||
106 | VideoChannelModel.searchForApi, | ||
107 | apiOptions, | ||
108 | 'filter:api.search.video-channels.local.list.result' | ||
109 | ) | ||
110 | |||
111 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
112 | } | ||
113 | |||
114 | async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { | ||
115 | let videoChannel: MChannelAccountDefault | ||
116 | let uri = search | ||
117 | |||
118 | if (isWebfingerSearch) { | ||
119 | try { | ||
120 | uri = await loadActorUrlOrGetFromWebfinger(search) | ||
121 | } catch (err) { | ||
122 | logger.warn('Cannot load actor URL or get from webfinger.', { search, err }) | ||
123 | |||
124 | return res.json({ total: 0, data: [] }) | ||
125 | } | ||
126 | } | ||
127 | |||
128 | if (isUserAbleToSearchRemoteURI(res)) { | ||
129 | try { | ||
130 | const actor = await getOrCreateAPActor(uri, 'all', true, true) | ||
131 | videoChannel = actor.VideoChannel | ||
132 | } catch (err) { | ||
133 | logger.info('Cannot search remote video channel %s.', uri, { err }) | ||
134 | } | ||
135 | } else { | ||
136 | videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(uri)) | ||
137 | } | ||
138 | |||
139 | return res.json({ | ||
140 | total: videoChannel ? 1 : 0, | ||
141 | data: videoChannel ? [ videoChannel.toFormattedJSON() ] : [] | ||
142 | }) | ||
143 | } | ||
144 | |||
145 | function sanitizeLocalUrl (url: string) { | ||
146 | if (!url) return '' | ||
147 | |||
148 | // Handle alternative channel URLs | ||
149 | return url.replace(new RegExp('^' + WEBSERVER.URL + '/c/'), WEBSERVER.URL + '/video-channels/') | ||
150 | } | ||
diff --git a/server/controllers/api/search/search-video-playlists.ts b/server/controllers/api/search/search-video-playlists.ts new file mode 100644 index 000000000..b231ff1e2 --- /dev/null +++ b/server/controllers/api/search/search-video-playlists.ts | |||
@@ -0,0 +1,129 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils' | ||
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { doJSONRequest } from '@server/helpers/requests' | ||
6 | import { getFormattedObjects } from '@server/helpers/utils' | ||
7 | import { CONFIG } from '@server/initializers/config' | ||
8 | import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get' | ||
9 | import { Hooks } from '@server/lib/plugins/hooks' | ||
10 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | ||
11 | import { getServerActor } from '@server/models/application/application' | ||
12 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
13 | import { MVideoPlaylistFullSummary } from '@server/types/models' | ||
14 | import { HttpStatusCode } from '@shared/core-utils' | ||
15 | import { ResultList, VideoPlaylist, VideoPlaylistsSearchQuery } from '@shared/models' | ||
16 | import { | ||
17 | asyncMiddleware, | ||
18 | openapiOperationDoc, | ||
19 | optionalAuthenticate, | ||
20 | paginationValidator, | ||
21 | setDefaultPagination, | ||
22 | setDefaultSearchSort, | ||
23 | videoPlaylistsListSearchValidator, | ||
24 | videoPlaylistsSearchSortValidator | ||
25 | } from '../../../middlewares' | ||
26 | import { WEBSERVER } from '@server/initializers/constants' | ||
27 | |||
28 | const searchPlaylistsRouter = express.Router() | ||
29 | |||
30 | searchPlaylistsRouter.get('/video-playlists', | ||
31 | openapiOperationDoc({ operationId: 'searchPlaylists' }), | ||
32 | paginationValidator, | ||
33 | setDefaultPagination, | ||
34 | videoPlaylistsSearchSortValidator, | ||
35 | setDefaultSearchSort, | ||
36 | optionalAuthenticate, | ||
37 | videoPlaylistsListSearchValidator, | ||
38 | asyncMiddleware(searchVideoPlaylists) | ||
39 | ) | ||
40 | |||
41 | // --------------------------------------------------------------------------- | ||
42 | |||
43 | export { searchPlaylistsRouter } | ||
44 | |||
45 | // --------------------------------------------------------------------------- | ||
46 | |||
47 | function searchVideoPlaylists (req: express.Request, res: express.Response) { | ||
48 | const query: VideoPlaylistsSearchQuery = req.query | ||
49 | const search = query.search | ||
50 | |||
51 | if (isURISearch(search)) return searchVideoPlaylistsURI(search, res) | ||
52 | |||
53 | if (isSearchIndexSearch(query)) { | ||
54 | return searchVideoPlaylistsIndex(query, res) | ||
55 | } | ||
56 | |||
57 | return searchVideoPlaylistsDB(query, res) | ||
58 | } | ||
59 | |||
60 | async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res: express.Response) { | ||
61 | const result = await buildMutedForSearchIndex(res) | ||
62 | |||
63 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params') | ||
64 | |||
65 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-playlists' | ||
66 | |||
67 | try { | ||
68 | logger.debug('Doing video playlists search index request on %s.', url, { body }) | ||
69 | |||
70 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoPlaylist>>(url, { method: 'POST', json: body }) | ||
71 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-playlists.index.list.result') | ||
72 | |||
73 | return res.json(jsonResult) | ||
74 | } catch (err) { | ||
75 | logger.warn('Cannot use search index to make video playlists search.', { err }) | ||
76 | |||
77 | return res.fail({ | ||
78 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
79 | message: 'Cannot use search index to make video playlists search' | ||
80 | }) | ||
81 | } | ||
82 | } | ||
83 | |||
84 | async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: express.Response) { | ||
85 | const serverActor = await getServerActor() | ||
86 | |||
87 | const apiOptions = await Hooks.wrapObject({ | ||
88 | followerActorId: serverActor.id, | ||
89 | search: query.search, | ||
90 | start: query.start, | ||
91 | count: query.count, | ||
92 | sort: query.sort | ||
93 | }, 'filter:api.search.video-playlists.local.list.params') | ||
94 | |||
95 | const resultList = await Hooks.wrapPromiseFun( | ||
96 | VideoPlaylistModel.searchForApi, | ||
97 | apiOptions, | ||
98 | 'filter:api.search.video-playlists.local.list.result' | ||
99 | ) | ||
100 | |||
101 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
102 | } | ||
103 | |||
104 | async function searchVideoPlaylistsURI (search: string, res: express.Response) { | ||
105 | let videoPlaylist: MVideoPlaylistFullSummary | ||
106 | |||
107 | if (isUserAbleToSearchRemoteURI(res)) { | ||
108 | try { | ||
109 | videoPlaylist = await getOrCreateAPVideoPlaylist(search) | ||
110 | } catch (err) { | ||
111 | logger.info('Cannot search remote video playlist %s.', search, { err }) | ||
112 | } | ||
113 | } else { | ||
114 | videoPlaylist = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(sanitizeLocalUrl(search)) | ||
115 | } | ||
116 | |||
117 | return res.json({ | ||
118 | total: videoPlaylist ? 1 : 0, | ||
119 | data: videoPlaylist ? [ videoPlaylist.toFormattedJSON() ] : [] | ||
120 | }) | ||
121 | } | ||
122 | |||
123 | function sanitizeLocalUrl (url: string) { | ||
124 | if (!url) return '' | ||
125 | |||
126 | // Handle alternative channel URLs | ||
127 | return url.replace(new RegExp('^' + WEBSERVER.URL + '/videos/watch/playlist/'), WEBSERVER.URL + '/video-playlists/') | ||
128 | .replace(new RegExp('^' + WEBSERVER.URL + '/w/p/'), WEBSERVER.URL + '/video-playlists/') | ||
129 | } | ||
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts new file mode 100644 index 000000000..b626baa28 --- /dev/null +++ b/server/controllers/api/search/search-videos.ts | |||
@@ -0,0 +1,153 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { WEBSERVER } from '@server/initializers/constants' | ||
6 | import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' | ||
7 | import { Hooks } from '@server/lib/plugins/hooks' | ||
8 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | ||
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
10 | import { ResultList, Video } from '@shared/models' | ||
11 | import { VideosSearchQuery } from '../../../../shared/models/search' | ||
12 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' | ||
13 | import { logger } from '../../../helpers/logger' | ||
14 | import { getFormattedObjects } from '../../../helpers/utils' | ||
15 | import { | ||
16 | asyncMiddleware, | ||
17 | commonVideosFiltersValidator, | ||
18 | openapiOperationDoc, | ||
19 | optionalAuthenticate, | ||
20 | paginationValidator, | ||
21 | setDefaultPagination, | ||
22 | setDefaultSearchSort, | ||
23 | videosSearchSortValidator, | ||
24 | videosSearchValidator | ||
25 | } from '../../../middlewares' | ||
26 | import { VideoModel } from '../../../models/video/video' | ||
27 | import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | ||
28 | |||
29 | const searchVideosRouter = express.Router() | ||
30 | |||
31 | searchVideosRouter.get('/videos', | ||
32 | openapiOperationDoc({ operationId: 'searchVideos' }), | ||
33 | paginationValidator, | ||
34 | setDefaultPagination, | ||
35 | videosSearchSortValidator, | ||
36 | setDefaultSearchSort, | ||
37 | optionalAuthenticate, | ||
38 | commonVideosFiltersValidator, | ||
39 | videosSearchValidator, | ||
40 | asyncMiddleware(searchVideos) | ||
41 | ) | ||
42 | |||
43 | // --------------------------------------------------------------------------- | ||
44 | |||
45 | export { searchVideosRouter } | ||
46 | |||
47 | // --------------------------------------------------------------------------- | ||
48 | |||
49 | function searchVideos (req: express.Request, res: express.Response) { | ||
50 | const query: VideosSearchQuery = req.query | ||
51 | const search = query.search | ||
52 | |||
53 | if (isURISearch(search)) { | ||
54 | return searchVideoURI(search, res) | ||
55 | } | ||
56 | |||
57 | if (isSearchIndexSearch(query)) { | ||
58 | return searchVideosIndex(query, res) | ||
59 | } | ||
60 | |||
61 | return searchVideosDB(query, res) | ||
62 | } | ||
63 | |||
64 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { | ||
65 | const result = await buildMutedForSearchIndex(res) | ||
66 | |||
67 | let body: VideosSearchQuery = Object.assign(query, result) | ||
68 | |||
69 | // Use the default instance NSFW policy if not specified | ||
70 | if (!body.nsfw) { | ||
71 | const nsfwPolicy = res.locals.oauth | ||
72 | ? res.locals.oauth.token.User.nsfwPolicy | ||
73 | : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY | ||
74 | |||
75 | body.nsfw = nsfwPolicy === 'do_not_list' | ||
76 | ? 'false' | ||
77 | : 'both' | ||
78 | } | ||
79 | |||
80 | body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') | ||
81 | |||
82 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' | ||
83 | |||
84 | try { | ||
85 | logger.debug('Doing videos search index request on %s.', url, { body }) | ||
86 | |||
87 | const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body }) | ||
88 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') | ||
89 | |||
90 | return res.json(jsonResult) | ||
91 | } catch (err) { | ||
92 | logger.warn('Cannot use search index to make video search.', { err }) | ||
93 | |||
94 | return res.fail({ | ||
95 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
96 | message: 'Cannot use search index to make video search' | ||
97 | }) | ||
98 | } | ||
99 | } | ||
100 | |||
101 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | ||
102 | const apiOptions = await Hooks.wrapObject(Object.assign(query, { | ||
103 | includeLocalVideos: true, | ||
104 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
105 | filter: query.filter, | ||
106 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
107 | }), 'filter:api.search.videos.local.list.params') | ||
108 | |||
109 | const resultList = await Hooks.wrapPromiseFun( | ||
110 | VideoModel.searchAndPopulateAccountAndServer, | ||
111 | apiOptions, | ||
112 | 'filter:api.search.videos.local.list.result' | ||
113 | ) | ||
114 | |||
115 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
116 | } | ||
117 | |||
118 | async function searchVideoURI (url: string, res: express.Response) { | ||
119 | let video: MVideoAccountLightBlacklistAllFiles | ||
120 | |||
121 | // Check if we can fetch a remote video with the URL | ||
122 | if (isUserAbleToSearchRemoteURI(res)) { | ||
123 | try { | ||
124 | const syncParam = { | ||
125 | likes: false, | ||
126 | dislikes: false, | ||
127 | shares: false, | ||
128 | comments: false, | ||
129 | thumbnail: true, | ||
130 | refreshVideo: false | ||
131 | } | ||
132 | |||
133 | const result = await getOrCreateAPVideo({ videoObject: url, syncParam }) | ||
134 | video = result ? result.video : undefined | ||
135 | } catch (err) { | ||
136 | logger.info('Cannot search remote video %s.', url, { err }) | ||
137 | } | ||
138 | } else { | ||
139 | video = await VideoModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(url)) | ||
140 | } | ||
141 | |||
142 | return res.json({ | ||
143 | total: video ? 1 : 0, | ||
144 | data: video ? [ video.toFormattedJSON() ] : [] | ||
145 | }) | ||
146 | } | ||
147 | |||
148 | function sanitizeLocalUrl (url: string) { | ||
149 | if (!url) return '' | ||
150 | |||
151 | // Handle alternative video URLs | ||
152 | return url.replace(new RegExp('^' + WEBSERVER.URL + '/w/'), WEBSERVER.URL + '/videos/watch/') | ||
153 | } | ||
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts index ff0d9ca3c..a6e9147f3 100644 --- a/server/controllers/api/server/debug.ts +++ b/server/controllers/api/server/debug.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' | 1 | import { InboxManager } from '@server/lib/activitypub/inbox-manager' |
2 | import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' | 2 | import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' |
3 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { SendDebugCommand } from '@shared/models' | 4 | import { SendDebugCommand } from '@shared/models' |
4 | import * as express from 'express' | 5 | import * as express from 'express' |
5 | import { UserRight } from '../../../../shared/models/users' | 6 | import { UserRight } from '../../../../shared/models/users' |
@@ -41,5 +42,5 @@ async function runCommand (req: express.Request, res: express.Response) { | |||
41 | await RemoveDanglingResumableUploadsScheduler.Instance.execute() | 42 | await RemoveDanglingResumableUploadsScheduler.Instance.execute() |
42 | } | 43 | } |
43 | 44 | ||
44 | return res.sendStatus(204) | 45 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
45 | } | 46 | } |
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 80025bc5b..12357a2ca 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts | |||
@@ -1,9 +1,15 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
2 | import { UserRight } from '../../../../shared/models/users' | 4 | import { UserRight } from '../../../../shared/models/users' |
3 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
4 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { getFormattedObjects } from '../../../helpers/utils' |
5 | import { SERVER_ACTOR_NAME } from '../../../initializers/constants' | 7 | import { SERVER_ACTOR_NAME } from '../../../initializers/constants' |
8 | import { sequelizeTypescript } from '../../../initializers/database' | ||
9 | import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' | ||
6 | import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' | 10 | import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' |
11 | import { JobQueue } from '../../../lib/job-queue' | ||
12 | import { removeRedundanciesOfServer } from '../../../lib/redundancy' | ||
7 | import { | 13 | import { |
8 | asyncMiddleware, | 14 | asyncMiddleware, |
9 | authenticate, | 15 | authenticate, |
@@ -19,16 +25,10 @@ import { | |||
19 | followingSortValidator, | 25 | followingSortValidator, |
20 | followValidator, | 26 | followValidator, |
21 | getFollowerValidator, | 27 | getFollowerValidator, |
22 | removeFollowingValidator, | 28 | listFollowsValidator, |
23 | listFollowsValidator | 29 | removeFollowingValidator |
24 | } from '../../../middlewares/validators' | 30 | } from '../../../middlewares/validators' |
25 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 31 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
26 | import { JobQueue } from '../../../lib/job-queue' | ||
27 | import { removeRedundanciesOfServer } from '../../../lib/redundancy' | ||
28 | import { sequelizeTypescript } from '../../../initializers/database' | ||
29 | import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' | ||
30 | import { getServerActor } from '@server/models/application/application' | ||
31 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
32 | 32 | ||
33 | const serverFollowsRouter = express.Router() | 33 | const serverFollowsRouter = express.Router() |
34 | serverFollowsRouter.get('/following', | 34 | serverFollowsRouter.get('/following', |
@@ -176,7 +176,7 @@ async function removeOrRejectFollower (req: express.Request, res: express.Respon | |||
176 | async function acceptFollower (req: express.Request, res: express.Response) { | 176 | async function acceptFollower (req: express.Request, res: express.Response) { |
177 | const follow = res.locals.follow | 177 | const follow = res.locals.follow |
178 | 178 | ||
179 | await sendAccept(follow) | 179 | sendAccept(follow) |
180 | 180 | ||
181 | follow.state = 'accepted' | 181 | follow.state = 'accepted' |
182 | await follow.save() | 182 | await follow.save() |
diff --git a/server/controllers/api/server/redundancy.ts b/server/controllers/api/server/redundancy.ts index 7c13dc21b..bc593ad43 100644 --- a/server/controllers/api/server/redundancy.ts +++ b/server/controllers/api/server/redundancy.ts | |||
@@ -90,13 +90,13 @@ async function addVideoRedundancy (req: express.Request, res: express.Response) | |||
90 | payload | 90 | payload |
91 | }) | 91 | }) |
92 | 92 | ||
93 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 93 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
94 | } | 94 | } |
95 | 95 | ||
96 | async function removeVideoRedundancyController (req: express.Request, res: express.Response) { | 96 | async function removeVideoRedundancyController (req: express.Request, res: express.Response) { |
97 | await removeVideoRedundancy(res.locals.videoRedundancy) | 97 | await removeVideoRedundancy(res.locals.videoRedundancy) |
98 | 98 | ||
99 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 99 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
100 | } | 100 | } |
101 | 101 | ||
102 | async function updateRedundancy (req: express.Request, res: express.Response) { | 102 | async function updateRedundancy (req: express.Request, res: express.Response) { |
@@ -110,5 +110,5 @@ async function updateRedundancy (req: express.Request, res: express.Response) { | |||
110 | removeRedundanciesOfServer(server.id) | 110 | removeRedundanciesOfServer(server.id) |
111 | .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) | 111 | .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) |
112 | 112 | ||
113 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 113 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
114 | } | 114 | } |
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts index 6e341c0fb..a86bc7d19 100644 --- a/server/controllers/api/server/server-blocklist.ts +++ b/server/controllers/api/server/server-blocklist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | import { UserNotificationModel } from '@server/models/account/user-notification' | 4 | import { UserNotificationModel } from '@server/models/user/user-notification' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { UserRight } from '../../../../shared/models/users' | 6 | import { UserRight } from '../../../../shared/models/users' |
7 | import { getFormattedObjects } from '../../../helpers/utils' | 7 | import { getFormattedObjects } from '../../../helpers/utils' |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index e2b1ea7cd..d907b49bf 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -45,7 +45,7 @@ import { | |||
45 | usersResetPasswordValidator, | 45 | usersResetPasswordValidator, |
46 | usersVerifyEmailValidator | 46 | usersVerifyEmailValidator |
47 | } from '../../../middlewares/validators' | 47 | } from '../../../middlewares/validators' |
48 | import { UserModel } from '../../../models/account/user' | 48 | import { UserModel } from '../../../models/user/user' |
49 | import { meRouter } from './me' | 49 | import { meRouter } from './me' |
50 | import { myAbusesRouter } from './my-abuses' | 50 | import { myAbusesRouter } from './my-abuses' |
51 | import { myBlocklistRouter } from './my-blocklist' | 51 | import { myBlocklistRouter } from './my-blocklist' |
@@ -314,7 +314,7 @@ async function removeUser (req: express.Request, res: express.Response) { | |||
314 | 314 | ||
315 | Hooks.runAction('action:api.user.deleted', { user }) | 315 | Hooks.runAction('action:api.user.deleted', { user }) |
316 | 316 | ||
317 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 317 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
318 | } | 318 | } |
319 | 319 | ||
320 | async function updateUser (req: express.Request, res: express.Response) { | 320 | async function updateUser (req: express.Request, res: express.Response) { |
@@ -323,14 +323,20 @@ async function updateUser (req: express.Request, res: express.Response) { | |||
323 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) | 323 | const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) |
324 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role | 324 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role |
325 | 325 | ||
326 | if (body.password !== undefined) userToUpdate.password = body.password | 326 | const keysToUpdate: (keyof UserUpdate)[] = [ |
327 | if (body.email !== undefined) userToUpdate.email = body.email | 327 | 'password', |
328 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified | 328 | 'email', |
329 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota | 329 | 'emailVerified', |
330 | if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily | 330 | 'videoQuota', |
331 | if (body.role !== undefined) userToUpdate.role = body.role | 331 | 'videoQuotaDaily', |
332 | if (body.adminFlags !== undefined) userToUpdate.adminFlags = body.adminFlags | 332 | 'role', |
333 | if (body.pluginAuth !== undefined) userToUpdate.pluginAuth = body.pluginAuth | 333 | 'adminFlags', |
334 | 'pluginAuth' | ||
335 | ] | ||
336 | |||
337 | for (const key of keysToUpdate) { | ||
338 | if (body[key] !== undefined) userToUpdate.set(key, body[key]) | ||
339 | } | ||
334 | 340 | ||
335 | const user = await userToUpdate.save() | 341 | const user = await userToUpdate.save() |
336 | 342 | ||
@@ -343,7 +349,7 @@ async function updateUser (req: express.Request, res: express.Response) { | |||
343 | 349 | ||
344 | // Don't need to send this update to followers, these attributes are not federated | 350 | // Don't need to send this update to followers, these attributes are not federated |
345 | 351 | ||
346 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 352 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
347 | } | 353 | } |
348 | 354 | ||
349 | async function askResetUserPassword (req: express.Request, res: express.Response) { | 355 | async function askResetUserPassword (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 0763d1900..1f2b2f9dd 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config' | |||
11 | import { MIMETYPES } from '../../../initializers/constants' | 11 | import { MIMETYPES } from '../../../initializers/constants' |
12 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
13 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 13 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
14 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image' | 14 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/local-actor' |
15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' | 15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' |
16 | import { | 16 | import { |
17 | asyncMiddleware, | 17 | asyncMiddleware, |
@@ -28,9 +28,10 @@ import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } fro | |||
28 | import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' | 28 | import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' |
29 | import { AccountModel } from '../../../models/account/account' | 29 | import { AccountModel } from '../../../models/account/account' |
30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 30 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
31 | import { UserModel } from '../../../models/account/user' | 31 | import { UserModel } from '../../../models/user/user' |
32 | import { VideoModel } from '../../../models/video/video' | 32 | import { VideoModel } from '../../../models/video/video' |
33 | import { VideoImportModel } from '../../../models/video/video-import' | 33 | import { VideoImportModel } from '../../../models/video/video-import' |
34 | import { AttributesOnly } from '@shared/core-utils' | ||
34 | 35 | ||
35 | const auditLogger = auditLoggerFactory('users') | 36 | const auditLogger = auditLoggerFactory('users') |
36 | 37 | ||
@@ -182,7 +183,7 @@ async function deleteMe (req: express.Request, res: express.Response) { | |||
182 | 183 | ||
183 | await user.destroy() | 184 | await user.destroy() |
184 | 185 | ||
185 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 186 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
186 | } | 187 | } |
187 | 188 | ||
188 | async function updateMe (req: express.Request, res: express.Response) { | 189 | async function updateMe (req: express.Request, res: express.Response) { |
@@ -191,17 +192,23 @@ async function updateMe (req: express.Request, res: express.Response) { | |||
191 | 192 | ||
192 | const user = res.locals.oauth.token.user | 193 | const user = res.locals.oauth.token.user |
193 | 194 | ||
194 | if (body.password !== undefined) user.password = body.password | 195 | const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly<UserModel>)[] = [ |
195 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy | 196 | 'password', |
196 | if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled | 197 | 'nsfwPolicy', |
197 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo | 198 | 'webTorrentEnabled', |
198 | if (body.autoPlayNextVideo !== undefined) user.autoPlayNextVideo = body.autoPlayNextVideo | 199 | 'autoPlayVideo', |
199 | if (body.autoPlayNextVideoPlaylist !== undefined) user.autoPlayNextVideoPlaylist = body.autoPlayNextVideoPlaylist | 200 | 'autoPlayNextVideo', |
200 | if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled | 201 | 'autoPlayNextVideoPlaylist', |
201 | if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages | 202 | 'videosHistoryEnabled', |
202 | if (body.theme !== undefined) user.theme = body.theme | 203 | 'videoLanguages', |
203 | if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal | 204 | 'theme', |
204 | if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal | 205 | 'noInstanceConfigWarningModal', |
206 | 'noWelcomeModal' | ||
207 | ] | ||
208 | |||
209 | for (const key of keysToUpdate) { | ||
210 | if (body[key] !== undefined) user.set(key, body[key]) | ||
211 | } | ||
205 | 212 | ||
206 | if (body.email !== undefined) { | 213 | if (body.email !== undefined) { |
207 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | 214 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { |
@@ -215,22 +222,22 @@ async function updateMe (req: express.Request, res: express.Response) { | |||
215 | await sequelizeTypescript.transaction(async t => { | 222 | await sequelizeTypescript.transaction(async t => { |
216 | await user.save({ transaction: t }) | 223 | await user.save({ transaction: t }) |
217 | 224 | ||
218 | if (body.displayName !== undefined || body.description !== undefined) { | 225 | if (body.displayName === undefined && body.description === undefined) return |
219 | const userAccount = await AccountModel.load(user.Account.id, t) | ||
220 | 226 | ||
221 | if (body.displayName !== undefined) userAccount.name = body.displayName | 227 | const userAccount = await AccountModel.load(user.Account.id, t) |
222 | if (body.description !== undefined) userAccount.description = body.description | ||
223 | await userAccount.save({ transaction: t }) | ||
224 | 228 | ||
225 | await sendUpdateActor(userAccount, t) | 229 | if (body.displayName !== undefined) userAccount.name = body.displayName |
226 | } | 230 | if (body.description !== undefined) userAccount.description = body.description |
231 | await userAccount.save({ transaction: t }) | ||
232 | |||
233 | await sendUpdateActor(userAccount, t) | ||
227 | }) | 234 | }) |
228 | 235 | ||
229 | if (sendVerificationEmail === true) { | 236 | if (sendVerificationEmail === true) { |
230 | await sendVerifyUserEmail(user, true) | 237 | await sendVerifyUserEmail(user, true) |
231 | } | 238 | } |
232 | 239 | ||
233 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 240 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
234 | } | 241 | } |
235 | 242 | ||
236 | async function updateMyAvatar (req: express.Request, res: express.Response) { | 243 | async function updateMyAvatar (req: express.Request, res: express.Response) { |
@@ -250,5 +257,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) { | |||
250 | const userAccount = await AccountModel.load(user.Account.id) | 257 | const userAccount = await AccountModel.load(user.Account.id) |
251 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) | 258 | await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) |
252 | 259 | ||
253 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 260 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
254 | } | 261 | } |
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts index faaef3ac0..a1561b751 100644 --- a/server/controllers/api/users/my-blocklist.ts +++ b/server/controllers/api/users/my-blocklist.ts | |||
@@ -20,7 +20,7 @@ import { | |||
20 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' | 20 | import { AccountBlocklistModel } from '../../../models/account/account-blocklist' |
21 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' | 21 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' |
22 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' | 22 | import { ServerBlocklistModel } from '../../../models/server/server-blocklist' |
23 | import { UserNotificationModel } from '@server/models/account/user-notification' | 23 | import { UserNotificationModel } from '@server/models/user/user-notification' |
24 | import { logger } from '@server/helpers/logger' | 24 | import { logger } from '@server/helpers/logger' |
25 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 25 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
26 | 26 | ||
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts index 72c7da373..cff1697ab 100644 --- a/server/controllers/api/users/my-history.ts +++ b/server/controllers/api/users/my-history.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | userHistoryRemoveValidator | 9 | userHistoryRemoveValidator |
10 | } from '../../../middlewares' | 10 | } from '../../../middlewares' |
11 | import { getFormattedObjects } from '../../../helpers/utils' | 11 | import { getFormattedObjects } from '../../../helpers/utils' |
12 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | 12 | import { UserVideoHistoryModel } from '../../../models/user/user-video-history' |
13 | import { sequelizeTypescript } from '../../../initializers/database' | 13 | import { sequelizeTypescript } from '../../../initializers/database' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
15 | 15 | ||
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts index 0a9101a46..2909770da 100644 --- a/server/controllers/api/users/my-notifications.ts +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -1,5 +1,9 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | ||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
4 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { UserNotificationSetting } from '../../../../shared/models/users' | ||
6 | import { getFormattedObjects } from '../../../helpers/utils' | ||
3 | import { | 7 | import { |
4 | asyncMiddleware, | 8 | asyncMiddleware, |
5 | asyncRetryTransactionMiddleware, | 9 | asyncRetryTransactionMiddleware, |
@@ -9,17 +13,13 @@ import { | |||
9 | setDefaultSort, | 13 | setDefaultSort, |
10 | userNotificationsSortValidator | 14 | userNotificationsSortValidator |
11 | } from '../../../middlewares' | 15 | } from '../../../middlewares' |
12 | import { getFormattedObjects } from '../../../helpers/utils' | ||
13 | import { UserNotificationModel } from '../../../models/account/user-notification' | ||
14 | import { meRouter } from './me' | ||
15 | import { | 16 | import { |
16 | listUserNotificationsValidator, | 17 | listUserNotificationsValidator, |
17 | markAsReadUserNotificationsValidator, | 18 | markAsReadUserNotificationsValidator, |
18 | updateNotificationSettingsValidator | 19 | updateNotificationSettingsValidator |
19 | } from '../../../middlewares/validators/user-notifications' | 20 | } from '../../../middlewares/validators/user-notifications' |
20 | import { UserNotificationSetting } from '../../../../shared/models/users' | 21 | import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' |
21 | import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting' | 22 | import { meRouter } from './me' |
22 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
23 | 23 | ||
24 | const myNotificationsRouter = express.Router() | 24 | const myNotificationsRouter = express.Router() |
25 | 25 | ||
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index 56b93276f..46a73d49e 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -27,7 +27,7 @@ import { | |||
27 | userSubscriptionsSortValidator, | 27 | userSubscriptionsSortValidator, |
28 | videosSortValidator | 28 | videosSortValidator |
29 | } from '../../../middlewares/validators' | 29 | } from '../../../middlewares/validators' |
30 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 30 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
31 | import { VideoModel } from '../../../models/video/video' | 31 | import { VideoModel } from '../../../models/video/video' |
32 | 32 | ||
33 | const mySubscriptionsRouter = express.Router() | 33 | const mySubscriptionsRouter = express.Router() |
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index 694bb0a92..b405ddbf4 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as RateLimit from 'express-rate-limit' | 2 | import * as RateLimit from 'express-rate-limit' |
3 | import { v4 as uuidv4 } from 'uuid' | ||
4 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | import { buildUUID } from '@server/helpers/uuid' | ||
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' | 6 | import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' |
7 | import { handleOAuthToken } from '@server/lib/auth/oauth' | 7 | import { handleOAuthToken } from '@server/lib/auth/oauth' |
8 | import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' | 8 | import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' |
9 | import { Hooks } from '@server/lib/plugins/hooks' | 9 | import { Hooks } from '@server/lib/plugins/hooks' |
10 | import { asyncMiddleware, authenticate } from '@server/middlewares' | 10 | import { asyncMiddleware, authenticate, openapiOperationDoc } from '@server/middlewares' |
11 | import { ScopedToken } from '@shared/models/users/user-scoped-token' | 11 | import { ScopedToken } from '@shared/models/users/user-scoped-token' |
12 | 12 | ||
13 | const tokensRouter = express.Router() | 13 | const tokensRouter = express.Router() |
@@ -19,10 +19,12 @@ const loginRateLimiter = RateLimit({ | |||
19 | 19 | ||
20 | tokensRouter.post('/token', | 20 | tokensRouter.post('/token', |
21 | loginRateLimiter, | 21 | loginRateLimiter, |
22 | openapiOperationDoc({ operationId: 'getOAuthToken' }), | ||
22 | asyncMiddleware(handleToken) | 23 | asyncMiddleware(handleToken) |
23 | ) | 24 | ) |
24 | 25 | ||
25 | tokensRouter.post('/revoke-token', | 26 | tokensRouter.post('/revoke-token', |
27 | openapiOperationDoc({ operationId: 'revokeOAuthToken' }), | ||
26 | authenticate, | 28 | authenticate, |
27 | asyncMiddleware(handleTokenRevocation) | 29 | asyncMiddleware(handleTokenRevocation) |
28 | ) | 30 | ) |
@@ -78,9 +80,10 @@ async function handleToken (req: express.Request, res: express.Response, next: e | |||
78 | } catch (err) { | 80 | } catch (err) { |
79 | logger.warn('Login error', { err }) | 81 | logger.warn('Login error', { err }) |
80 | 82 | ||
81 | return res.status(err.code || 400).json({ | 83 | return res.fail({ |
82 | code: err.name, | 84 | status: err.code, |
83 | error: err.message | 85 | message: err.message, |
86 | type: err.name | ||
84 | }) | 87 | }) |
85 | } | 88 | } |
86 | } | 89 | } |
@@ -104,7 +107,7 @@ function getScopedTokens (req: express.Request, res: express.Response) { | |||
104 | async function renewScopedTokens (req: express.Request, res: express.Response) { | 107 | async function renewScopedTokens (req: express.Request, res: express.Response) { |
105 | const user = res.locals.oauth.token.user | 108 | const user = res.locals.oauth.token.user |
106 | 109 | ||
107 | user.feedToken = uuidv4() | 110 | user.feedToken = buildUUID() |
108 | await user.save() | 111 | await user.save() |
109 | 112 | ||
110 | return res.json({ | 113 | return res.json({ |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index a755d7e57..bc8d203b0 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -13,8 +13,8 @@ import { CONFIG } from '../../initializers/config' | |||
13 | import { MIMETYPES } from '../../initializers/constants' | 13 | import { MIMETYPES } from '../../initializers/constants' |
14 | import { sequelizeTypescript } from '../../initializers/database' | 14 | import { sequelizeTypescript } from '../../initializers/database' |
15 | import { sendUpdateActor } from '../../lib/activitypub/send' | 15 | import { sendUpdateActor } from '../../lib/activitypub/send' |
16 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image' | ||
17 | import { JobQueue } from '../../lib/job-queue' | 16 | import { JobQueue } from '../../lib/job-queue' |
17 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/local-actor' | ||
18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
19 | import { | 19 | import { |
20 | asyncMiddleware, | 20 | asyncMiddleware, |
@@ -32,7 +32,7 @@ import { | |||
32 | videoChannelsUpdateValidator, | 32 | videoChannelsUpdateValidator, |
33 | videoPlaylistsSortValidator | 33 | videoPlaylistsSortValidator |
34 | } from '../../middlewares' | 34 | } from '../../middlewares' |
35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' | 35 | import { videoChannelsListValidator, videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' |
36 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' | 36 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' |
37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
38 | import { AccountModel } from '../../models/account/account' | 38 | import { AccountModel } from '../../models/account/account' |
@@ -51,7 +51,7 @@ videoChannelRouter.get('/', | |||
51 | videoChannelsSortValidator, | 51 | videoChannelsSortValidator, |
52 | setDefaultSort, | 52 | setDefaultSort, |
53 | setDefaultPagination, | 53 | setDefaultPagination, |
54 | videoChannelsOwnSearchValidator, | 54 | videoChannelsListValidator, |
55 | asyncMiddleware(listVideoChannels) | 55 | asyncMiddleware(listVideoChannels) |
56 | ) | 56 | ) |
57 | 57 | ||
@@ -162,6 +162,7 @@ async function updateVideoChannelBanner (req: express.Request, res: express.Resp | |||
162 | 162 | ||
163 | return res.json({ banner: banner.toFormattedJSON() }) | 163 | return res.json({ banner: banner.toFormattedJSON() }) |
164 | } | 164 | } |
165 | |||
165 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | 166 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { |
166 | const avatarPhysicalFile = req.files['avatarfile'][0] | 167 | const avatarPhysicalFile = req.files['avatarfile'][0] |
167 | const videoChannel = res.locals.videoChannel | 168 | const videoChannel = res.locals.videoChannel |
@@ -179,7 +180,7 @@ async function deleteVideoChannelAvatar (req: express.Request, res: express.Resp | |||
179 | 180 | ||
180 | await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) | 181 | await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) |
181 | 182 | ||
182 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 183 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
183 | } | 184 | } |
184 | 185 | ||
185 | async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { | 186 | async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { |
@@ -187,7 +188,7 @@ async function deleteVideoChannelBanner (req: express.Request, res: express.Resp | |||
187 | 188 | ||
188 | await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) | 189 | await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) |
189 | 190 | ||
190 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 191 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
191 | } | 192 | } |
192 | 193 | ||
193 | async function addVideoChannel (req: express.Request, res: express.Response) { | 194 | async function addVideoChannel (req: express.Request, res: express.Response) { |
@@ -221,10 +222,6 @@ async function updateVideoChannel (req: express.Request, res: express.Response) | |||
221 | 222 | ||
222 | try { | 223 | try { |
223 | await sequelizeTypescript.transaction(async t => { | 224 | await sequelizeTypescript.transaction(async t => { |
224 | const sequelizeOptions = { | ||
225 | transaction: t | ||
226 | } | ||
227 | |||
228 | if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName | 225 | if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName |
229 | if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description | 226 | if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description |
230 | 227 | ||
@@ -238,7 +235,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response) | |||
238 | } | 235 | } |
239 | } | 236 | } |
240 | 237 | ||
241 | const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault | 238 | const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault |
242 | await sendUpdateActor(videoChannelInstanceUpdated, t) | 239 | await sendUpdateActor(videoChannelInstanceUpdated, t) |
243 | 240 | ||
244 | auditLogger.update( | 241 | auditLogger.update( |
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index aab16533d..87a6f6bbe 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -1,7 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { uuidToShort } from '@server/helpers/uuid' | ||
4 | import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
3 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
4 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' | 7 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' |
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' | 9 | import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' |
6 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' | 10 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' |
7 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' | 11 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' |
@@ -17,8 +21,7 @@ import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constant | |||
17 | import { sequelizeTypescript } from '../../initializers/database' | 21 | import { sequelizeTypescript } from '../../initializers/database' |
18 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' | 22 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' |
19 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' | 23 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' |
20 | import { JobQueue } from '../../lib/job-queue' | 24 | import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail' |
21 | import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail' | ||
22 | import { | 25 | import { |
23 | asyncMiddleware, | 26 | asyncMiddleware, |
24 | asyncRetryTransactionMiddleware, | 27 | asyncRetryTransactionMiddleware, |
@@ -42,7 +45,6 @@ import { | |||
42 | import { AccountModel } from '../../models/account/account' | 45 | import { AccountModel } from '../../models/account/account' |
43 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 46 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
44 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | 47 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' |
45 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
46 | 48 | ||
47 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) | 49 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) |
48 | 50 | ||
@@ -144,9 +146,7 @@ async function listVideoPlaylists (req: express.Request, res: express.Response) | |||
144 | function getVideoPlaylist (req: express.Request, res: express.Response) { | 146 | function getVideoPlaylist (req: express.Request, res: express.Response) { |
145 | const videoPlaylist = res.locals.videoPlaylistSummary | 147 | const videoPlaylist = res.locals.videoPlaylistSummary |
146 | 148 | ||
147 | if (videoPlaylist.isOutdated()) { | 149 | scheduleRefreshIfNeeded(videoPlaylist) |
148 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } }) | ||
149 | } | ||
150 | 150 | ||
151 | return res.json(videoPlaylist.toFormattedJSON()) | 151 | return res.json(videoPlaylist.toFormattedJSON()) |
152 | } | 152 | } |
@@ -173,7 +173,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
173 | 173 | ||
174 | const thumbnailField = req.files['thumbnailfile'] | 174 | const thumbnailField = req.files['thumbnailfile'] |
175 | const thumbnailModel = thumbnailField | 175 | const thumbnailModel = thumbnailField |
176 | ? await createPlaylistMiniatureFromExisting({ | 176 | ? await updatePlaylistMiniatureFromExisting({ |
177 | inputPath: thumbnailField[0].path, | 177 | inputPath: thumbnailField[0].path, |
178 | playlist: videoPlaylist, | 178 | playlist: videoPlaylist, |
179 | automaticallyGenerated: false | 179 | automaticallyGenerated: false |
@@ -200,9 +200,10 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
200 | return res.json({ | 200 | return res.json({ |
201 | videoPlaylist: { | 201 | videoPlaylist: { |
202 | id: videoPlaylistCreated.id, | 202 | id: videoPlaylistCreated.id, |
203 | shortUUID: uuidToShort(videoPlaylistCreated.uuid), | ||
203 | uuid: videoPlaylistCreated.uuid | 204 | uuid: videoPlaylistCreated.uuid |
204 | } | 205 | } |
205 | }).end() | 206 | }) |
206 | } | 207 | } |
207 | 208 | ||
208 | async function updateVideoPlaylist (req: express.Request, res: express.Response) { | 209 | async function updateVideoPlaylist (req: express.Request, res: express.Response) { |
@@ -215,7 +216,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
215 | 216 | ||
216 | const thumbnailField = req.files['thumbnailfile'] | 217 | const thumbnailField = req.files['thumbnailfile'] |
217 | const thumbnailModel = thumbnailField | 218 | const thumbnailModel = thumbnailField |
218 | ? await createPlaylistMiniatureFromExisting({ | 219 | ? await updatePlaylistMiniatureFromExisting({ |
219 | inputPath: thumbnailField[0].path, | 220 | inputPath: thumbnailField[0].path, |
220 | playlist: videoPlaylistInstance, | 221 | playlist: videoPlaylistInstance, |
221 | automaticallyGenerated: false | 222 | automaticallyGenerated: false |
@@ -332,6 +333,8 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response) | |||
332 | 333 | ||
333 | logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) | 334 | logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) |
334 | 335 | ||
336 | Hooks.runAction('action:api.video-playlist-element.created', { playlistElement }) | ||
337 | |||
335 | return res.json({ | 338 | return res.json({ |
336 | videoPlaylistElement: { | 339 | videoPlaylistElement: { |
337 | id: playlistElement.id | 340 | id: playlistElement.id |
@@ -482,7 +485,7 @@ async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbn | |||
482 | } | 485 | } |
483 | 486 | ||
484 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) | 487 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename) |
485 | const thumbnailModel = await createPlaylistMiniatureFromExisting({ | 488 | const thumbnailModel = await updatePlaylistMiniatureFromExisting({ |
486 | inputPath, | 489 | inputPath, |
487 | playlist: videoPlaylist, | 490 | playlist: videoPlaylist, |
488 | automaticallyGenerated: true, | 491 | automaticallyGenerated: true, |
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts index fa8448c86..530e17965 100644 --- a/server/controllers/api/videos/blacklist.ts +++ b/server/controllers/api/videos/blacklist.ts | |||
@@ -9,6 +9,7 @@ import { | |||
9 | authenticate, | 9 | authenticate, |
10 | blacklistSortValidator, | 10 | blacklistSortValidator, |
11 | ensureUserHasRight, | 11 | ensureUserHasRight, |
12 | openapiOperationDoc, | ||
12 | paginationValidator, | 13 | paginationValidator, |
13 | setBlacklistSort, | 14 | setBlacklistSort, |
14 | setDefaultPagination, | 15 | setDefaultPagination, |
@@ -23,6 +24,7 @@ import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-c | |||
23 | const blacklistRouter = express.Router() | 24 | const blacklistRouter = express.Router() |
24 | 25 | ||
25 | blacklistRouter.post('/:videoId/blacklist', | 26 | blacklistRouter.post('/:videoId/blacklist', |
27 | openapiOperationDoc({ operationId: 'addVideoBlock' }), | ||
26 | authenticate, | 28 | authenticate, |
27 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | 29 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), |
28 | asyncMiddleware(videosBlacklistAddValidator), | 30 | asyncMiddleware(videosBlacklistAddValidator), |
@@ -30,6 +32,7 @@ blacklistRouter.post('/:videoId/blacklist', | |||
30 | ) | 32 | ) |
31 | 33 | ||
32 | blacklistRouter.get('/blacklist', | 34 | blacklistRouter.get('/blacklist', |
35 | openapiOperationDoc({ operationId: 'getVideoBlocks' }), | ||
33 | authenticate, | 36 | authenticate, |
34 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | 37 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), |
35 | paginationValidator, | 38 | paginationValidator, |
@@ -48,6 +51,7 @@ blacklistRouter.put('/:videoId/blacklist', | |||
48 | ) | 51 | ) |
49 | 52 | ||
50 | blacklistRouter.delete('/:videoId/blacklist', | 53 | blacklistRouter.delete('/:videoId/blacklist', |
54 | openapiOperationDoc({ operationId: 'delVideoBlock' }), | ||
51 | authenticate, | 55 | authenticate, |
52 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), | 56 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), |
53 | asyncMiddleware(videosBlacklistRemoveValidator), | 57 | asyncMiddleware(videosBlacklistRemoveValidator), |
@@ -70,7 +74,7 @@ async function addVideoToBlacklistController (req: express.Request, res: express | |||
70 | 74 | ||
71 | logger.info('Video %s blacklisted.', videoInstance.uuid) | 75 | logger.info('Video %s blacklisted.', videoInstance.uuid) |
72 | 76 | ||
73 | return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) | 77 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() |
74 | } | 78 | } |
75 | 79 | ||
76 | async function updateVideoBlacklistController (req: express.Request, res: express.Response) { | 80 | async function updateVideoBlacklistController (req: express.Request, res: express.Response) { |
@@ -82,7 +86,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres | |||
82 | return videoBlacklist.save({ transaction: t }) | 86 | return videoBlacklist.save({ transaction: t }) |
83 | }) | 87 | }) |
84 | 88 | ||
85 | return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) | 89 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() |
86 | } | 90 | } |
87 | 91 | ||
88 | async function listBlacklist (req: express.Request, res: express.Response) { | 92 | async function listBlacklist (req: express.Request, res: express.Response) { |
@@ -105,5 +109,5 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex | |||
105 | 109 | ||
106 | logger.info('Video %s removed from blacklist.', video.uuid) | 110 | logger.info('Video %s removed from blacklist.', video.uuid) |
107 | 111 | ||
108 | return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) | 112 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() |
109 | } | 113 | } |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index f1f53d354..e6f28c1cb 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 2 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
3 | import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models' | 3 | import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models' |
4 | import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' | 4 | import { VideoCommentCreate } from '../../../../shared/models/videos/comment/video-comment.model' |
5 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | 5 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' |
6 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { getFormattedObjects } from '../../../helpers/utils' |
7 | import { sequelizeTypescript } from '../../../initializers/database' | 7 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -166,7 +166,10 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo | |||
166 | } | 166 | } |
167 | 167 | ||
168 | if (resultList.data.length === 0) { | 168 | if (resultList.data.length === 0) { |
169 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 169 | return res.fail({ |
170 | status: HttpStatusCode.NOT_FOUND_404, | ||
171 | message: 'No comments were found' | ||
172 | }) | ||
170 | } | 173 | } |
171 | 174 | ||
172 | return res.json(buildFormattedCommentTree(resultList)) | 175 | return res.json(buildFormattedCommentTree(resultList)) |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 3b9b887e2..de9a5308a 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -3,7 +3,9 @@ import { move, readFile } from 'fs-extra' | |||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as parseTorrent from 'parse-torrent' | 4 | import * as parseTorrent from 'parse-torrent' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { ServerConfigManager } from '@server/lib/server-config-manager' | ||
6 | import { setVideoTags } from '@server/lib/video' | 7 | import { setVideoTags } from '@server/lib/video' |
8 | import { FilteredModelAttributes } from '@server/types' | ||
7 | import { | 9 | import { |
8 | MChannelAccountDefault, | 10 | MChannelAccountDefault, |
9 | MThumbnail, | 11 | MThumbnail, |
@@ -14,23 +16,22 @@ import { | |||
14 | MVideoThumbnail, | 16 | MVideoThumbnail, |
15 | MVideoWithBlacklistLight | 17 | MVideoWithBlacklistLight |
16 | } from '@server/types/models' | 18 | } from '@server/types/models' |
17 | import { MVideoImport, MVideoImportFormattable } from '@server/types/models/video/video-import' | 19 | import { MVideoImportFormattable } from '@server/types/models/video/video-import' |
18 | import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' | 20 | import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' |
19 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
20 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
21 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' | 22 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' |
22 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' | 23 | import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' |
23 | import { isArray } from '../../../helpers/custom-validators/misc' | 24 | import { isArray } from '../../../helpers/custom-validators/misc' |
24 | import { createReqFiles } from '../../../helpers/express-utils' | 25 | import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils' |
25 | import { logger } from '../../../helpers/logger' | 26 | import { logger } from '../../../helpers/logger' |
26 | import { getSecureTorrentName } from '../../../helpers/utils' | 27 | import { getSecureTorrentName } from '../../../helpers/utils' |
27 | import { getYoutubeDLInfo, getYoutubeDLSubs, YoutubeDLInfo } from '../../../helpers/youtube-dl' | 28 | import { YoutubeDL, YoutubeDLInfo } from '../../../helpers/youtube-dl' |
28 | import { CONFIG } from '../../../initializers/config' | 29 | import { CONFIG } from '../../../initializers/config' |
29 | import { MIMETYPES } from '../../../initializers/constants' | 30 | import { MIMETYPES } from '../../../initializers/constants' |
30 | import { sequelizeTypescript } from '../../../initializers/database' | 31 | import { sequelizeTypescript } from '../../../initializers/database' |
31 | import { getLocalVideoActivityPubUrl } from '../../../lib/activitypub/url' | 32 | import { getLocalVideoActivityPubUrl } from '../../../lib/activitypub/url' |
32 | import { JobQueue } from '../../../lib/job-queue/job-queue' | 33 | import { JobQueue } from '../../../lib/job-queue/job-queue' |
33 | import { createVideoMiniatureFromExisting, createVideoMiniatureFromUrl } from '../../../lib/thumbnail' | 34 | import { updateVideoMiniatureFromExisting, updateVideoMiniatureFromUrl } from '../../../lib/thumbnail' |
34 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 35 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
35 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' | 36 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' |
36 | import { VideoModel } from '../../../models/video/video' | 37 | import { VideoModel } from '../../../models/video/video' |
@@ -81,22 +82,15 @@ async function addTorrentImport (req: express.Request, res: express.Response, to | |||
81 | let magnetUri: string | 82 | let magnetUri: string |
82 | 83 | ||
83 | if (torrentfile) { | 84 | if (torrentfile) { |
84 | torrentName = torrentfile.originalname | 85 | const result = await processTorrentOrAbortRequest(req, res, torrentfile) |
86 | if (!result) return | ||
85 | 87 | ||
86 | // Rename the torrent to a secured name | 88 | videoName = result.name |
87 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) | 89 | torrentName = result.torrentName |
88 | await move(torrentfile.path, newTorrentPath) | ||
89 | torrentfile.path = newTorrentPath | ||
90 | |||
91 | const buf = await readFile(torrentfile.path) | ||
92 | const parsedTorrent = parseTorrent(buf) | ||
93 | |||
94 | videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[0] : parsedTorrent.name as string | ||
95 | } else { | 90 | } else { |
96 | magnetUri = body.magnetUri | 91 | const result = processMagnetURI(body) |
97 | 92 | magnetUri = result.magnetUri | |
98 | const parsed = magnetUtil.decode(magnetUri) | 93 | videoName = result.name |
99 | videoName = isArray(parsed.name) ? parsed.name[0] : parsed.name as string | ||
100 | } | 94 | } |
101 | 95 | ||
102 | const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) | 96 | const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) |
@@ -104,26 +98,26 @@ async function addTorrentImport (req: express.Request, res: express.Response, to | |||
104 | const thumbnailModel = await processThumbnail(req, video) | 98 | const thumbnailModel = await processThumbnail(req, video) |
105 | const previewModel = await processPreview(req, video) | 99 | const previewModel = await processPreview(req, video) |
106 | 100 | ||
107 | const tags = body.tags || undefined | ||
108 | const videoImportAttributes = { | ||
109 | magnetUri, | ||
110 | torrentName, | ||
111 | state: VideoImportState.PENDING, | ||
112 | userId: user.id | ||
113 | } | ||
114 | const videoImport = await insertIntoDB({ | 101 | const videoImport = await insertIntoDB({ |
115 | video, | 102 | video, |
116 | thumbnailModel, | 103 | thumbnailModel, |
117 | previewModel, | 104 | previewModel, |
118 | videoChannel: res.locals.videoChannel, | 105 | videoChannel: res.locals.videoChannel, |
119 | tags, | 106 | tags: body.tags || undefined, |
120 | videoImportAttributes, | 107 | user, |
121 | user | 108 | videoImportAttributes: { |
109 | magnetUri, | ||
110 | torrentName, | ||
111 | state: VideoImportState.PENDING, | ||
112 | userId: user.id | ||
113 | } | ||
122 | }) | 114 | }) |
123 | 115 | ||
124 | // Create job to import the video | 116 | // Create job to import the video |
125 | const payload = { | 117 | const payload = { |
126 | type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri', | 118 | type: torrentfile |
119 | ? 'torrent-file' as 'torrent-file' | ||
120 | : 'magnet-uri' as 'magnet-uri', | ||
127 | videoImportId: videoImport.id, | 121 | videoImportId: videoImport.id, |
128 | magnetUri | 122 | magnetUri |
129 | } | 123 | } |
@@ -139,17 +133,21 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
139 | const targetUrl = body.targetUrl | 133 | const targetUrl = body.targetUrl |
140 | const user = res.locals.oauth.token.User | 134 | const user = res.locals.oauth.token.User |
141 | 135 | ||
136 | const youtubeDL = new YoutubeDL(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) | ||
137 | |||
142 | // Get video infos | 138 | // Get video infos |
143 | let youtubeDLInfo: YoutubeDLInfo | 139 | let youtubeDLInfo: YoutubeDLInfo |
144 | try { | 140 | try { |
145 | youtubeDLInfo = await getYoutubeDLInfo(targetUrl) | 141 | youtubeDLInfo = await youtubeDL.getYoutubeDLInfo() |
146 | } catch (err) { | 142 | } catch (err) { |
147 | logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) | 143 | logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) |
148 | 144 | ||
149 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 145 | return res.fail({ |
150 | .json({ | 146 | message: 'Cannot fetch remote information of this URL.', |
151 | error: 'Cannot fetch remote information of this URL.' | 147 | data: { |
152 | }) | 148 | targetUrl |
149 | } | ||
150 | }) | ||
153 | } | 151 | } |
154 | 152 | ||
155 | const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) | 153 | const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) |
@@ -170,45 +168,22 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
170 | previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video) | 168 | previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video) |
171 | } | 169 | } |
172 | 170 | ||
173 | const tags = body.tags || youtubeDLInfo.tags | ||
174 | const videoImportAttributes = { | ||
175 | targetUrl, | ||
176 | state: VideoImportState.PENDING, | ||
177 | userId: user.id | ||
178 | } | ||
179 | const videoImport = await insertIntoDB({ | 171 | const videoImport = await insertIntoDB({ |
180 | video, | 172 | video, |
181 | thumbnailModel, | 173 | thumbnailModel, |
182 | previewModel, | 174 | previewModel, |
183 | videoChannel: res.locals.videoChannel, | 175 | videoChannel: res.locals.videoChannel, |
184 | tags, | 176 | tags: body.tags || youtubeDLInfo.tags, |
185 | videoImportAttributes, | 177 | user, |
186 | user | 178 | videoImportAttributes: { |
179 | targetUrl, | ||
180 | state: VideoImportState.PENDING, | ||
181 | userId: user.id | ||
182 | } | ||
187 | }) | 183 | }) |
188 | 184 | ||
189 | // Get video subtitles | 185 | // Get video subtitles |
190 | try { | 186 | await processYoutubeSubtitles(youtubeDL, targetUrl, video.id) |
191 | const subtitles = await getYoutubeDLSubs(targetUrl) | ||
192 | |||
193 | logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl) | ||
194 | |||
195 | for (const subtitle of subtitles) { | ||
196 | const videoCaption = new VideoCaptionModel({ | ||
197 | videoId: video.id, | ||
198 | language: subtitle.language, | ||
199 | filename: VideoCaptionModel.generateCaptionName(subtitle.language) | ||
200 | }) as MVideoCaption | ||
201 | |||
202 | // Move physical file | ||
203 | await moveAndProcessCaptionFile(subtitle, videoCaption) | ||
204 | |||
205 | await sequelizeTypescript.transaction(async t => { | ||
206 | await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t) | ||
207 | }) | ||
208 | } | ||
209 | } catch (err) { | ||
210 | logger.warn('Cannot get video subtitles.', { err }) | ||
211 | } | ||
212 | 187 | ||
213 | // Create job to import the video | 188 | // Create job to import the video |
214 | const payload = { | 189 | const payload = { |
@@ -240,7 +215,9 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You | |||
240 | privacy: body.privacy || VideoPrivacy.PRIVATE, | 215 | privacy: body.privacy || VideoPrivacy.PRIVATE, |
241 | duration: 0, // duration will be set by the import job | 216 | duration: 0, // duration will be set by the import job |
242 | channelId: channelId, | 217 | channelId: channelId, |
243 | originallyPublishedAt: body.originallyPublishedAt || importData.originallyPublishedAt | 218 | originallyPublishedAt: body.originallyPublishedAt |
219 | ? new Date(body.originallyPublishedAt) | ||
220 | : importData.originallyPublishedAt | ||
244 | } | 221 | } |
245 | const video = new VideoModel(videoData) | 222 | const video = new VideoModel(videoData) |
246 | video.url = getLocalVideoActivityPubUrl(video) | 223 | video.url = getLocalVideoActivityPubUrl(video) |
@@ -253,7 +230,7 @@ async function processThumbnail (req: express.Request, video: MVideoThumbnail) { | |||
253 | if (thumbnailField) { | 230 | if (thumbnailField) { |
254 | const thumbnailPhysicalFile = thumbnailField[0] | 231 | const thumbnailPhysicalFile = thumbnailField[0] |
255 | 232 | ||
256 | return createVideoMiniatureFromExisting({ | 233 | return updateVideoMiniatureFromExisting({ |
257 | inputPath: thumbnailPhysicalFile.path, | 234 | inputPath: thumbnailPhysicalFile.path, |
258 | video, | 235 | video, |
259 | type: ThumbnailType.MINIATURE, | 236 | type: ThumbnailType.MINIATURE, |
@@ -269,7 +246,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr | |||
269 | if (previewField) { | 246 | if (previewField) { |
270 | const previewPhysicalFile = previewField[0] | 247 | const previewPhysicalFile = previewField[0] |
271 | 248 | ||
272 | return createVideoMiniatureFromExisting({ | 249 | return updateVideoMiniatureFromExisting({ |
273 | inputPath: previewPhysicalFile.path, | 250 | inputPath: previewPhysicalFile.path, |
274 | video, | 251 | video, |
275 | type: ThumbnailType.PREVIEW, | 252 | type: ThumbnailType.PREVIEW, |
@@ -282,7 +259,7 @@ async function processPreview (req: express.Request, video: MVideoThumbnail): Pr | |||
282 | 259 | ||
283 | async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { | 260 | async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { |
284 | try { | 261 | try { |
285 | return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.MINIATURE }) | 262 | return updateVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.MINIATURE }) |
286 | } catch (err) { | 263 | } catch (err) { |
287 | logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err }) | 264 | logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err }) |
288 | return undefined | 265 | return undefined |
@@ -291,7 +268,7 @@ async function processThumbnailFromUrl (url: string, video: MVideoThumbnail) { | |||
291 | 268 | ||
292 | async function processPreviewFromUrl (url: string, video: MVideoThumbnail) { | 269 | async function processPreviewFromUrl (url: string, video: MVideoThumbnail) { |
293 | try { | 270 | try { |
294 | return createVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.PREVIEW }) | 271 | return updateVideoMiniatureFromUrl({ downloadUrl: url, video, type: ThumbnailType.PREVIEW }) |
295 | } catch (err) { | 272 | } catch (err) { |
296 | logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err }) | 273 | logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err }) |
297 | return undefined | 274 | return undefined |
@@ -304,7 +281,7 @@ async function insertIntoDB (parameters: { | |||
304 | previewModel: MThumbnail | 281 | previewModel: MThumbnail |
305 | videoChannel: MChannelAccountDefault | 282 | videoChannel: MChannelAccountDefault |
306 | tags: string[] | 283 | tags: string[] |
307 | videoImportAttributes: Partial<MVideoImport> | 284 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> |
308 | user: MUser | 285 | user: MUser |
309 | }): Promise<MVideoImportFormattable> { | 286 | }): Promise<MVideoImportFormattable> { |
310 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters | 287 | const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters |
@@ -342,3 +319,69 @@ async function insertIntoDB (parameters: { | |||
342 | 319 | ||
343 | return videoImport | 320 | return videoImport |
344 | } | 321 | } |
322 | |||
323 | async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { | ||
324 | const torrentName = torrentfile.originalname | ||
325 | |||
326 | // Rename the torrent to a secured name | ||
327 | const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) | ||
328 | await move(torrentfile.path, newTorrentPath, { overwrite: true }) | ||
329 | torrentfile.path = newTorrentPath | ||
330 | |||
331 | const buf = await readFile(torrentfile.path) | ||
332 | const parsedTorrent = parseTorrent(buf) as parseTorrent.Instance | ||
333 | |||
334 | if (parsedTorrent.files.length !== 1) { | ||
335 | cleanUpReqFiles(req) | ||
336 | |||
337 | res.fail({ | ||
338 | type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT, | ||
339 | message: 'Torrents with only 1 file are supported.' | ||
340 | }) | ||
341 | return undefined | ||
342 | } | ||
343 | |||
344 | return { | ||
345 | name: extractNameFromArray(parsedTorrent.name), | ||
346 | torrentName | ||
347 | } | ||
348 | } | ||
349 | |||
350 | function processMagnetURI (body: VideoImportCreate) { | ||
351 | const magnetUri = body.magnetUri | ||
352 | const parsed = magnetUtil.decode(magnetUri) | ||
353 | |||
354 | return { | ||
355 | name: extractNameFromArray(parsed.name), | ||
356 | magnetUri | ||
357 | } | ||
358 | } | ||
359 | |||
360 | function extractNameFromArray (name: string | string[]) { | ||
361 | return isArray(name) ? name[0] : name | ||
362 | } | ||
363 | |||
364 | async function processYoutubeSubtitles (youtubeDL: YoutubeDL, targetUrl: string, videoId: number) { | ||
365 | try { | ||
366 | const subtitles = await youtubeDL.getYoutubeDLSubs() | ||
367 | |||
368 | logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl) | ||
369 | |||
370 | for (const subtitle of subtitles) { | ||
371 | const videoCaption = new VideoCaptionModel({ | ||
372 | videoId, | ||
373 | language: subtitle.language, | ||
374 | filename: VideoCaptionModel.generateCaptionName(subtitle.language) | ||
375 | }) as MVideoCaption | ||
376 | |||
377 | // Move physical file | ||
378 | await moveAndProcessCaptionFile(subtitle, videoCaption) | ||
379 | |||
380 | await sequelizeTypescript.transaction(async t => { | ||
381 | await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t) | ||
382 | }) | ||
383 | } | ||
384 | } catch (err) { | ||
385 | logger.warn('Cannot get video subtitles.', { err }) | ||
386 | } | ||
387 | } | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index c32626d30..74b100e59 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -1,43 +1,22 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { move } from 'fs-extra' | ||
3 | import { extname } from 'path' | ||
4 | import toInt from 'validator/lib/toInt' | 2 | import toInt from 'validator/lib/toInt' |
5 | import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 4 | import { LiveManager } from '@server/lib/live' |
7 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | 5 | import { openapiOperationDoc } from '@server/middlewares/doc' |
8 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
9 | import { LiveManager } from '@server/lib/live-manager' | ||
10 | import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
11 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
12 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
13 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 7 | import { MVideoAccountLight } from '@server/types/models' |
14 | import { uploadx } from '@uploadx/core' | 8 | import { VideosCommonQuery } from '../../../../shared' |
15 | import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared' | ||
16 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' | 9 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' |
17 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 10 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
18 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 11 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
19 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 12 | import { logger } from '../../../helpers/logger' |
20 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | ||
21 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
22 | import { getFormattedObjects } from '../../../helpers/utils' | 13 | import { getFormattedObjects } from '../../../helpers/utils' |
23 | import { CONFIG } from '../../../initializers/config' | 14 | import { REMOTE_SCHEME, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' |
24 | import { | ||
25 | DEFAULT_AUDIO_RESOLUTION, | ||
26 | MIMETYPES, | ||
27 | VIDEO_CATEGORIES, | ||
28 | VIDEO_LANGUAGES, | ||
29 | VIDEO_LICENCES, | ||
30 | VIDEO_PRIVACIES | ||
31 | } from '../../../initializers/constants' | ||
32 | import { sequelizeTypescript } from '../../../initializers/database' | 15 | import { sequelizeTypescript } from '../../../initializers/database' |
33 | import { sendView } from '../../../lib/activitypub/send/send-view' | 16 | import { sendView } from '../../../lib/activitypub/send/send-view' |
34 | import { federateVideoIfNeeded, fetchRemoteVideoDescription } from '../../../lib/activitypub/videos' | ||
35 | import { JobQueue } from '../../../lib/job-queue' | 17 | import { JobQueue } from '../../../lib/job-queue' |
36 | import { Notifier } from '../../../lib/notifier' | ||
37 | import { Hooks } from '../../../lib/plugins/hooks' | 18 | import { Hooks } from '../../../lib/plugins/hooks' |
38 | import { Redis } from '../../../lib/redis' | 19 | import { Redis } from '../../../lib/redis' |
39 | import { generateVideoMiniature } from '../../../lib/thumbnail' | ||
40 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
41 | import { | 20 | import { |
42 | asyncMiddleware, | 21 | asyncMiddleware, |
43 | asyncRetryTransactionMiddleware, | 22 | asyncRetryTransactionMiddleware, |
@@ -49,16 +28,11 @@ import { | |||
49 | setDefaultPagination, | 28 | setDefaultPagination, |
50 | setDefaultVideosSort, | 29 | setDefaultVideosSort, |
51 | videoFileMetadataGetValidator, | 30 | videoFileMetadataGetValidator, |
52 | videosAddLegacyValidator, | ||
53 | videosAddResumableInitValidator, | ||
54 | videosAddResumableValidator, | ||
55 | videosCustomGetValidator, | 31 | videosCustomGetValidator, |
56 | videosGetValidator, | 32 | videosGetValidator, |
57 | videosRemoveValidator, | 33 | videosRemoveValidator, |
58 | videosSortValidator, | 34 | videosSortValidator |
59 | videosUpdateValidator | ||
60 | } from '../../../middlewares' | 35 | } from '../../../middlewares' |
61 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
62 | import { VideoModel } from '../../../models/video/video' | 36 | import { VideoModel } from '../../../models/video/video' |
63 | import { VideoFileModel } from '../../../models/video/video-file' | 37 | import { VideoFileModel } from '../../../models/video/video-file' |
64 | import { blacklistRouter } from './blacklist' | 38 | import { blacklistRouter } from './blacklist' |
@@ -68,40 +42,12 @@ import { videoImportsRouter } from './import' | |||
68 | import { liveRouter } from './live' | 42 | import { liveRouter } from './live' |
69 | import { ownershipVideoRouter } from './ownership' | 43 | import { ownershipVideoRouter } from './ownership' |
70 | import { rateVideoRouter } from './rate' | 44 | import { rateVideoRouter } from './rate' |
45 | import { updateRouter } from './update' | ||
46 | import { uploadRouter } from './upload' | ||
71 | import { watchingRouter } from './watching' | 47 | import { watchingRouter } from './watching' |
72 | 48 | ||
73 | const lTags = loggerTagsFactory('api', 'video') | ||
74 | const auditLogger = auditLoggerFactory('videos') | 49 | const auditLogger = auditLoggerFactory('videos') |
75 | const videosRouter = express.Router() | 50 | const videosRouter = express.Router() |
76 | const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() }) | ||
77 | |||
78 | const reqVideoFileAdd = createReqFiles( | ||
79 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | ||
80 | Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), | ||
81 | { | ||
82 | videofile: CONFIG.STORAGE.TMP_DIR, | ||
83 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
84 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
85 | } | ||
86 | ) | ||
87 | |||
88 | const reqVideoFileAddResumable = createReqFiles( | ||
89 | [ 'thumbnailfile', 'previewfile' ], | ||
90 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
91 | { | ||
92 | thumbnailfile: getResumableUploadPath(), | ||
93 | previewfile: getResumableUploadPath() | ||
94 | } | ||
95 | ) | ||
96 | |||
97 | const reqVideoFileUpdate = createReqFiles( | ||
98 | [ 'thumbnailfile', 'previewfile' ], | ||
99 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
100 | { | ||
101 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
102 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
103 | } | ||
104 | ) | ||
105 | 51 | ||
106 | videosRouter.use('/', blacklistRouter) | 52 | videosRouter.use('/', blacklistRouter) |
107 | videosRouter.use('/', rateVideoRouter) | 53 | videosRouter.use('/', rateVideoRouter) |
@@ -111,13 +57,28 @@ videosRouter.use('/', videoImportsRouter) | |||
111 | videosRouter.use('/', ownershipVideoRouter) | 57 | videosRouter.use('/', ownershipVideoRouter) |
112 | videosRouter.use('/', watchingRouter) | 58 | videosRouter.use('/', watchingRouter) |
113 | videosRouter.use('/', liveRouter) | 59 | videosRouter.use('/', liveRouter) |
60 | videosRouter.use('/', uploadRouter) | ||
61 | videosRouter.use('/', updateRouter) | ||
114 | 62 | ||
115 | videosRouter.get('/categories', listVideoCategories) | 63 | videosRouter.get('/categories', |
116 | videosRouter.get('/licences', listVideoLicences) | 64 | openapiOperationDoc({ operationId: 'getCategories' }), |
117 | videosRouter.get('/languages', listVideoLanguages) | 65 | listVideoCategories |
118 | videosRouter.get('/privacies', listVideoPrivacies) | 66 | ) |
67 | videosRouter.get('/licences', | ||
68 | openapiOperationDoc({ operationId: 'getLicences' }), | ||
69 | listVideoLicences | ||
70 | ) | ||
71 | videosRouter.get('/languages', | ||
72 | openapiOperationDoc({ operationId: 'getLanguages' }), | ||
73 | listVideoLanguages | ||
74 | ) | ||
75 | videosRouter.get('/privacies', | ||
76 | openapiOperationDoc({ operationId: 'getPrivacies' }), | ||
77 | listVideoPrivacies | ||
78 | ) | ||
119 | 79 | ||
120 | videosRouter.get('/', | 80 | videosRouter.get('/', |
81 | openapiOperationDoc({ operationId: 'getVideos' }), | ||
121 | paginationValidator, | 82 | paginationValidator, |
122 | videosSortValidator, | 83 | videosSortValidator, |
123 | setDefaultVideosSort, | 84 | setDefaultVideosSort, |
@@ -127,40 +88,8 @@ videosRouter.get('/', | |||
127 | asyncMiddleware(listVideos) | 88 | asyncMiddleware(listVideos) |
128 | ) | 89 | ) |
129 | 90 | ||
130 | videosRouter.post('/upload', | ||
131 | authenticate, | ||
132 | reqVideoFileAdd, | ||
133 | asyncMiddleware(videosAddLegacyValidator), | ||
134 | asyncRetryTransactionMiddleware(addVideoLegacy) | ||
135 | ) | ||
136 | |||
137 | videosRouter.post('/upload-resumable', | ||
138 | authenticate, | ||
139 | reqVideoFileAddResumable, | ||
140 | asyncMiddleware(videosAddResumableInitValidator), | ||
141 | uploadxMiddleware | ||
142 | ) | ||
143 | |||
144 | videosRouter.delete('/upload-resumable', | ||
145 | authenticate, | ||
146 | uploadxMiddleware | ||
147 | ) | ||
148 | |||
149 | videosRouter.put('/upload-resumable', | ||
150 | authenticate, | ||
151 | uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes | ||
152 | asyncMiddleware(videosAddResumableValidator), | ||
153 | asyncMiddleware(addVideoResumable) | ||
154 | ) | ||
155 | |||
156 | videosRouter.put('/:id', | ||
157 | authenticate, | ||
158 | reqVideoFileUpdate, | ||
159 | asyncMiddleware(videosUpdateValidator), | ||
160 | asyncRetryTransactionMiddleware(updateVideo) | ||
161 | ) | ||
162 | |||
163 | videosRouter.get('/:id/description', | 91 | videosRouter.get('/:id/description', |
92 | openapiOperationDoc({ operationId: 'getVideoDesc' }), | ||
164 | asyncMiddleware(videosGetValidator), | 93 | asyncMiddleware(videosGetValidator), |
165 | asyncMiddleware(getVideoDescription) | 94 | asyncMiddleware(getVideoDescription) |
166 | ) | 95 | ) |
@@ -169,17 +98,20 @@ videosRouter.get('/:id/metadata/:videoFileId', | |||
169 | asyncMiddleware(getVideoFileMetadata) | 98 | asyncMiddleware(getVideoFileMetadata) |
170 | ) | 99 | ) |
171 | videosRouter.get('/:id', | 100 | videosRouter.get('/:id', |
101 | openapiOperationDoc({ operationId: 'getVideo' }), | ||
172 | optionalAuthenticate, | 102 | optionalAuthenticate, |
173 | asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), | 103 | asyncMiddleware(videosCustomGetValidator('for-api')), |
174 | asyncMiddleware(checkVideoFollowConstraints), | 104 | asyncMiddleware(checkVideoFollowConstraints), |
175 | asyncMiddleware(getVideo) | 105 | asyncMiddleware(getVideo) |
176 | ) | 106 | ) |
177 | videosRouter.post('/:id/views', | 107 | videosRouter.post('/:id/views', |
108 | openapiOperationDoc({ operationId: 'addView' }), | ||
178 | asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), | 109 | asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), |
179 | asyncMiddleware(viewVideo) | 110 | asyncMiddleware(viewVideo) |
180 | ) | 111 | ) |
181 | 112 | ||
182 | videosRouter.delete('/:id', | 113 | videosRouter.delete('/:id', |
114 | openapiOperationDoc({ operationId: 'delVideo' }), | ||
183 | authenticate, | 115 | authenticate, |
184 | asyncMiddleware(videosRemoveValidator), | 116 | asyncMiddleware(videosRemoveValidator), |
185 | asyncRetryTransactionMiddleware(removeVideo) | 117 | asyncRetryTransactionMiddleware(removeVideo) |
@@ -209,287 +141,8 @@ function listVideoPrivacies (_req: express.Request, res: express.Response) { | |||
209 | res.json(VIDEO_PRIVACIES) | 141 | res.json(VIDEO_PRIVACIES) |
210 | } | 142 | } |
211 | 143 | ||
212 | async function addVideoLegacy (req: express.Request, res: express.Response) { | 144 | async function getVideo (_req: express.Request, res: express.Response) { |
213 | // Uploading the video could be long | 145 | const video = res.locals.videoAPI |
214 | // Set timeout to 10 minutes, as Express's default is 2 minutes | ||
215 | req.setTimeout(1000 * 60 * 10, () => { | ||
216 | logger.error('Upload video has timed out.') | ||
217 | return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408) | ||
218 | }) | ||
219 | |||
220 | const videoPhysicalFile = req.files['videofile'][0] | ||
221 | const videoInfo: VideoCreate = req.body | ||
222 | const files = req.files | ||
223 | |||
224 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
225 | } | ||
226 | |||
227 | async function addVideoResumable (_req: express.Request, res: express.Response) { | ||
228 | const videoPhysicalFile = res.locals.videoFileResumable | ||
229 | const videoInfo = videoPhysicalFile.metadata | ||
230 | const files = { previewfile: videoInfo.previewfile } | ||
231 | |||
232 | // Don't need the meta file anymore | ||
233 | await deleteResumableUploadMetaFile(videoPhysicalFile.path) | ||
234 | |||
235 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
236 | } | ||
237 | |||
238 | async function addVideo (options: { | ||
239 | res: express.Response | ||
240 | videoPhysicalFile: express.VideoUploadFile | ||
241 | videoInfo: VideoCreate | ||
242 | files: express.UploadFiles | ||
243 | }) { | ||
244 | const { res, videoPhysicalFile, videoInfo, files } = options | ||
245 | const videoChannel = res.locals.videoChannel | ||
246 | const user = res.locals.oauth.token.User | ||
247 | |||
248 | const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) | ||
249 | |||
250 | videoData.state = CONFIG.TRANSCODING.ENABLED | ||
251 | ? VideoState.TO_TRANSCODE | ||
252 | : VideoState.PUBLISHED | ||
253 | |||
254 | videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware | ||
255 | |||
256 | const video = new VideoModel(videoData) as MVideoFullLight | ||
257 | video.VideoChannel = videoChannel | ||
258 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | ||
259 | |||
260 | const videoFile = new VideoFileModel({ | ||
261 | extname: extname(videoPhysicalFile.filename), | ||
262 | size: videoPhysicalFile.size, | ||
263 | videoStreamingPlaylistId: null, | ||
264 | metadata: await getMetadataFromFile(videoPhysicalFile.path) | ||
265 | }) | ||
266 | |||
267 | if (videoFile.isAudio()) { | ||
268 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION | ||
269 | } else { | ||
270 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
271 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | ||
272 | } | ||
273 | |||
274 | videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname) | ||
275 | |||
276 | // Move physical file | ||
277 | const destination = getVideoFilePath(video, videoFile) | ||
278 | await move(videoPhysicalFile.path, destination) | ||
279 | // This is important in case if there is another attempt in the retry process | ||
280 | videoPhysicalFile.filename = getVideoFilePath(video, videoFile) | ||
281 | videoPhysicalFile.path = destination | ||
282 | |||
283 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
284 | video, | ||
285 | files, | ||
286 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | ||
287 | }) | ||
288 | |||
289 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
290 | const sequelizeOptions = { transaction: t } | ||
291 | |||
292 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | ||
293 | |||
294 | await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
295 | await videoCreated.addAndSaveThumbnail(previewModel, t) | ||
296 | |||
297 | // Do not forget to add video channel information to the created video | ||
298 | videoCreated.VideoChannel = res.locals.videoChannel | ||
299 | |||
300 | videoFile.videoId = video.id | ||
301 | await videoFile.save(sequelizeOptions) | ||
302 | |||
303 | video.VideoFiles = [ videoFile ] | ||
304 | |||
305 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) | ||
306 | |||
307 | // Schedule an update in the future? | ||
308 | if (videoInfo.scheduleUpdate) { | ||
309 | await ScheduleVideoUpdateModel.create({ | ||
310 | videoId: video.id, | ||
311 | updateAt: videoInfo.scheduleUpdate.updateAt, | ||
312 | privacy: videoInfo.scheduleUpdate.privacy || null | ||
313 | }, { transaction: t }) | ||
314 | } | ||
315 | |||
316 | // Channel has a new content, set as updated | ||
317 | await videoCreated.VideoChannel.setAsUpdated(t) | ||
318 | |||
319 | await autoBlacklistVideoIfNeeded({ | ||
320 | video, | ||
321 | user, | ||
322 | isRemote: false, | ||
323 | isNew: true, | ||
324 | transaction: t | ||
325 | }) | ||
326 | |||
327 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | ||
328 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) | ||
329 | |||
330 | return { videoCreated } | ||
331 | }) | ||
332 | |||
333 | // Create the torrent file in async way because it could be long | ||
334 | createTorrentAndSetInfoHashAsync(video, videoFile) | ||
335 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) | ||
336 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) | ||
337 | .then(refreshedVideo => { | ||
338 | if (!refreshedVideo) return | ||
339 | |||
340 | // Only federate and notify after the torrent creation | ||
341 | Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo) | ||
342 | |||
343 | return retryTransactionWrapper(() => { | ||
344 | return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) | ||
345 | }) | ||
346 | }) | ||
347 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) | ||
348 | |||
349 | if (video.state === VideoState.TO_TRANSCODE) { | ||
350 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) | ||
351 | } | ||
352 | |||
353 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) | ||
354 | |||
355 | return res.json({ | ||
356 | video: { | ||
357 | id: videoCreated.id, | ||
358 | uuid: videoCreated.uuid | ||
359 | } | ||
360 | }) | ||
361 | } | ||
362 | |||
363 | async function updateVideo (req: express.Request, res: express.Response) { | ||
364 | const videoInstance = res.locals.videoAll | ||
365 | const videoFieldsSave = videoInstance.toJSON() | ||
366 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | ||
367 | const videoInfoToUpdate: VideoUpdate = req.body | ||
368 | |||
369 | const wasConfidentialVideo = videoInstance.isConfidential() | ||
370 | const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation() | ||
371 | |||
372 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
373 | video: videoInstance, | ||
374 | files: req.files, | ||
375 | fallback: () => Promise.resolve(undefined), | ||
376 | automaticallyGenerated: false | ||
377 | }) | ||
378 | |||
379 | try { | ||
380 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { | ||
381 | const sequelizeOptions = { transaction: t } | ||
382 | const oldVideoChannel = videoInstance.VideoChannel | ||
383 | |||
384 | if (videoInfoToUpdate.name !== undefined) videoInstance.name = videoInfoToUpdate.name | ||
385 | if (videoInfoToUpdate.category !== undefined) videoInstance.category = videoInfoToUpdate.category | ||
386 | if (videoInfoToUpdate.licence !== undefined) videoInstance.licence = videoInfoToUpdate.licence | ||
387 | if (videoInfoToUpdate.language !== undefined) videoInstance.language = videoInfoToUpdate.language | ||
388 | if (videoInfoToUpdate.nsfw !== undefined) videoInstance.nsfw = videoInfoToUpdate.nsfw | ||
389 | if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.waitTranscoding = videoInfoToUpdate.waitTranscoding | ||
390 | if (videoInfoToUpdate.support !== undefined) videoInstance.support = videoInfoToUpdate.support | ||
391 | if (videoInfoToUpdate.description !== undefined) videoInstance.description = videoInfoToUpdate.description | ||
392 | if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.commentsEnabled = videoInfoToUpdate.commentsEnabled | ||
393 | if (videoInfoToUpdate.downloadEnabled !== undefined) videoInstance.downloadEnabled = videoInfoToUpdate.downloadEnabled | ||
394 | |||
395 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { | ||
396 | videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) | ||
397 | } | ||
398 | |||
399 | let isNewVideo = false | ||
400 | if (videoInfoToUpdate.privacy !== undefined) { | ||
401 | isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) | ||
402 | |||
403 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) | ||
404 | videoInstance.setPrivacy(newPrivacy) | ||
405 | |||
406 | // Unfederate the video if the new privacy is not compatible with federation | ||
407 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | ||
408 | await VideoModel.sendDelete(videoInstance, { transaction: t }) | ||
409 | } | ||
410 | } | ||
411 | |||
412 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight | ||
413 | |||
414 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
415 | if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) | ||
416 | |||
417 | // Video tags update? | ||
418 | if (videoInfoToUpdate.tags !== undefined) { | ||
419 | await setVideoTags({ | ||
420 | video: videoInstanceUpdated, | ||
421 | tags: videoInfoToUpdate.tags, | ||
422 | transaction: t | ||
423 | }) | ||
424 | } | ||
425 | |||
426 | // Video channel update? | ||
427 | if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { | ||
428 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | ||
429 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | ||
430 | |||
431 | if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | ||
432 | } | ||
433 | |||
434 | // Schedule an update in the future? | ||
435 | if (videoInfoToUpdate.scheduleUpdate) { | ||
436 | await ScheduleVideoUpdateModel.upsert({ | ||
437 | videoId: videoInstanceUpdated.id, | ||
438 | updateAt: videoInfoToUpdate.scheduleUpdate.updateAt, | ||
439 | privacy: videoInfoToUpdate.scheduleUpdate.privacy || null | ||
440 | }, { transaction: t }) | ||
441 | } else if (videoInfoToUpdate.scheduleUpdate === null) { | ||
442 | await ScheduleVideoUpdateModel.deleteByVideoId(videoInstanceUpdated.id, t) | ||
443 | } | ||
444 | |||
445 | await autoBlacklistVideoIfNeeded({ | ||
446 | video: videoInstanceUpdated, | ||
447 | user: res.locals.oauth.token.User, | ||
448 | isRemote: false, | ||
449 | isNew: false, | ||
450 | transaction: t | ||
451 | }) | ||
452 | |||
453 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | ||
454 | |||
455 | auditLogger.update( | ||
456 | getAuditIdFromRes(res), | ||
457 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | ||
458 | oldVideoAuditView | ||
459 | ) | ||
460 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid)) | ||
461 | |||
462 | return videoInstanceUpdated | ||
463 | }) | ||
464 | |||
465 | if (wasConfidentialVideo) { | ||
466 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) | ||
467 | } | ||
468 | |||
469 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body }) | ||
470 | } catch (err) { | ||
471 | // Force fields we want to update | ||
472 | // If the transaction is retried, sequelize will think the object has not changed | ||
473 | // So it will skip the SQL request, even if the last one was ROLLBACKed! | ||
474 | resetSequelizeInstance(videoInstance, videoFieldsSave) | ||
475 | |||
476 | throw err | ||
477 | } | ||
478 | |||
479 | return res.type('json') | ||
480 | .status(HttpStatusCode.NO_CONTENT_204) | ||
481 | .end() | ||
482 | } | ||
483 | |||
484 | async function getVideo (req: express.Request, res: express.Response) { | ||
485 | // We need more attributes | ||
486 | const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null | ||
487 | |||
488 | const video = await Hooks.wrapPromiseFun( | ||
489 | VideoModel.loadForGetAPI, | ||
490 | { id: res.locals.onlyVideoWithRights.id, userId }, | ||
491 | 'filter:api.video.get.result' | ||
492 | ) | ||
493 | 146 | ||
494 | if (video.isOutdated()) { | 147 | if (video.isOutdated()) { |
495 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) | 148 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) |
@@ -505,7 +158,7 @@ async function viewVideo (req: express.Request, res: express.Response) { | |||
505 | const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid) | 158 | const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid) |
506 | if (exists) { | 159 | if (exists) { |
507 | logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid) | 160 | logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid) |
508 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 161 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
509 | } | 162 | } |
510 | 163 | ||
511 | const video = await VideoModel.load(immutableVideoAttrs.id) | 164 | const video = await VideoModel.load(immutableVideoAttrs.id) |
@@ -538,18 +191,15 @@ async function viewVideo (req: express.Request, res: express.Response) { | |||
538 | 191 | ||
539 | Hooks.runAction('action:api.video.viewed', { video, ip }) | 192 | Hooks.runAction('action:api.video.viewed', { video, ip }) |
540 | 193 | ||
541 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 194 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
542 | } | 195 | } |
543 | 196 | ||
544 | async function getVideoDescription (req: express.Request, res: express.Response) { | 197 | async function getVideoDescription (req: express.Request, res: express.Response) { |
545 | const videoInstance = res.locals.videoAll | 198 | const videoInstance = res.locals.videoAll |
546 | let description = '' | ||
547 | 199 | ||
548 | if (videoInstance.isOwned()) { | 200 | const description = videoInstance.isOwned() |
549 | description = videoInstance.description | 201 | ? videoInstance.description |
550 | } else { | 202 | : await fetchRemoteVideoDescription(videoInstance) |
551 | description = await fetchRemoteVideoDescription(videoInstance) | ||
552 | } | ||
553 | 203 | ||
554 | return res.json({ description }) | 204 | return res.json({ description }) |
555 | } | 205 | } |
@@ -591,7 +241,7 @@ async function listVideos (req: express.Request, res: express.Response) { | |||
591 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 241 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
592 | } | 242 | } |
593 | 243 | ||
594 | async function removeVideo (req: express.Request, res: express.Response) { | 244 | async function removeVideo (_req: express.Request, res: express.Response) { |
595 | const videoInstance = res.locals.videoAll | 245 | const videoInstance = res.locals.videoAll |
596 | 246 | ||
597 | await sequelizeTypescript.transaction(async t => { | 247 | await sequelizeTypescript.transaction(async t => { |
@@ -608,16 +258,14 @@ async function removeVideo (req: express.Request, res: express.Response) { | |||
608 | .end() | 258 | .end() |
609 | } | 259 | } |
610 | 260 | ||
611 | async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) { | 261 | // --------------------------------------------------------------------------- |
612 | await createTorrentAndSetInfoHash(video, fileArg) | ||
613 | |||
614 | // Refresh videoFile because the createTorrentAndSetInfoHash could be long | ||
615 | const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id) | ||
616 | // File does not exist anymore, remove the generated torrent | ||
617 | if (!refreshedFile) return fileArg.removeTorrent() | ||
618 | 262 | ||
619 | refreshedFile.infoHash = fileArg.infoHash | 263 | // FIXME: Should not exist, we rely on specific API |
620 | refreshedFile.torrentFilename = fileArg.torrentFilename | 264 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { |
265 | const host = video.VideoChannel.Account.Actor.Server.host | ||
266 | const path = video.getDescriptionAPIPath() | ||
267 | const url = REMOTE_SCHEME.HTTP + '://' + host + path | ||
621 | 268 | ||
622 | return refreshedFile.save() | 269 | const { body } = await doJSONRequest<any>(url) |
270 | return body.description || '' | ||
623 | } | 271 | } |
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index 04d2494ce..d8c51c2d4 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { v4 as uuidv4 } from 'uuid' | ||
3 | import { createReqFiles } from '@server/helpers/express-utils' | 2 | import { createReqFiles } from '@server/helpers/express-utils' |
3 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' | ||
4 | import { CONFIG } from '@server/initializers/config' | 4 | import { CONFIG } from '@server/initializers/config' |
5 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' | 5 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' |
6 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | 6 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' |
@@ -11,12 +11,12 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator | |||
11 | import { VideoLiveModel } from '@server/models/video/video-live' | 11 | import { VideoLiveModel } from '@server/models/video/video-live' |
12 | import { MVideoDetails, MVideoFullLight } from '@server/types/models' | 12 | import { MVideoDetails, MVideoFullLight } from '@server/types/models' |
13 | import { LiveVideoCreate, LiveVideoUpdate, VideoState } from '../../../../shared' | 13 | import { LiveVideoCreate, LiveVideoUpdate, VideoState } from '../../../../shared' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
14 | import { logger } from '../../../helpers/logger' | 15 | import { logger } from '../../../helpers/logger' |
15 | import { sequelizeTypescript } from '../../../initializers/database' | 16 | import { sequelizeTypescript } from '../../../initializers/database' |
16 | import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail' | 17 | import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' |
17 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' | 18 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' |
18 | import { VideoModel } from '../../../models/video/video' | 19 | import { VideoModel } from '../../../models/video/video' |
19 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
20 | 20 | ||
21 | const liveRouter = express.Router() | 21 | const liveRouter = express.Router() |
22 | 22 | ||
@@ -76,7 +76,7 @@ async function updateLiveVideo (req: express.Request, res: express.Response) { | |||
76 | 76 | ||
77 | await federateVideoIfNeeded(video, false) | 77 | await federateVideoIfNeeded(video, false) |
78 | 78 | ||
79 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 79 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
80 | } | 80 | } |
81 | 81 | ||
82 | async function addLiveVideo (req: express.Request, res: express.Response) { | 82 | async function addLiveVideo (req: express.Request, res: express.Response) { |
@@ -94,13 +94,13 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
94 | const videoLive = new VideoLiveModel() | 94 | const videoLive = new VideoLiveModel() |
95 | videoLive.saveReplay = videoInfo.saveReplay || false | 95 | videoLive.saveReplay = videoInfo.saveReplay || false |
96 | videoLive.permanentLive = videoInfo.permanentLive || false | 96 | videoLive.permanentLive = videoInfo.permanentLive || false |
97 | videoLive.streamKey = uuidv4() | 97 | videoLive.streamKey = buildUUID() |
98 | 98 | ||
99 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | 99 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ |
100 | video, | 100 | video, |
101 | files: req.files, | 101 | files: req.files, |
102 | fallback: type => { | 102 | fallback: type => { |
103 | return createVideoMiniatureFromExisting({ | 103 | return updateVideoMiniatureFromExisting({ |
104 | inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, | 104 | inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, |
105 | video, | 105 | video, |
106 | type, | 106 | type, |
@@ -138,6 +138,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
138 | return res.json({ | 138 | return res.json({ |
139 | video: { | 139 | video: { |
140 | id: videoCreated.id, | 140 | id: videoCreated.id, |
141 | shortUUID: uuidToShort(videoCreated.uuid), | ||
141 | uuid: videoCreated.uuid | 142 | uuid: videoCreated.uuid |
142 | } | 143 | } |
143 | }) | 144 | }) |
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts index a85d7c30b..1bb96e046 100644 --- a/server/controllers/api/videos/ownership.ts +++ b/server/controllers/api/videos/ownership.ts | |||
@@ -99,15 +99,15 @@ async function listVideoOwnership (req: express.Request, res: express.Response) | |||
99 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 99 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
100 | } | 100 | } |
101 | 101 | ||
102 | async function acceptOwnership (req: express.Request, res: express.Response) { | 102 | function acceptOwnership (req: express.Request, res: express.Response) { |
103 | return sequelizeTypescript.transaction(async t => { | 103 | return sequelizeTypescript.transaction(async t => { |
104 | const videoChangeOwnership = res.locals.videoChangeOwnership | 104 | const videoChangeOwnership = res.locals.videoChangeOwnership |
105 | const channel = res.locals.videoChannel | 105 | const channel = res.locals.videoChannel |
106 | 106 | ||
107 | // We need more attributes for federation | 107 | // We need more attributes for federation |
108 | const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) | 108 | const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id, t) |
109 | 109 | ||
110 | const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId) | 110 | const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId, t) |
111 | 111 | ||
112 | targetVideo.channelId = channel.id | 112 | targetVideo.channelId = channel.id |
113 | 113 | ||
@@ -122,17 +122,17 @@ async function acceptOwnership (req: express.Request, res: express.Response) { | |||
122 | videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED | 122 | videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED |
123 | await videoChangeOwnership.save({ transaction: t }) | 123 | await videoChangeOwnership.save({ transaction: t }) |
124 | 124 | ||
125 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 125 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
126 | }) | 126 | }) |
127 | } | 127 | } |
128 | 128 | ||
129 | async function refuseOwnership (req: express.Request, res: express.Response) { | 129 | function refuseOwnership (req: express.Request, res: express.Response) { |
130 | return sequelizeTypescript.transaction(async t => { | 130 | return sequelizeTypescript.transaction(async t => { |
131 | const videoChangeOwnership = res.locals.videoChangeOwnership | 131 | const videoChangeOwnership = res.locals.videoChangeOwnership |
132 | 132 | ||
133 | videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED | 133 | videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED |
134 | await videoChangeOwnership.save({ transaction: t }) | 134 | await videoChangeOwnership.save({ transaction: t }) |
135 | 135 | ||
136 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 136 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
137 | }) | 137 | }) |
138 | } | 138 | } |
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts new file mode 100644 index 000000000..8affe71c6 --- /dev/null +++ b/server/controllers/api/videos/update.ts | |||
@@ -0,0 +1,193 @@ | |||
1 | import * as express from 'express' | ||
2 | import { Transaction } from 'sequelize/types' | ||
3 | import { changeVideoChannelShare } from '@server/lib/activitypub/share' | ||
4 | import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
5 | import { FilteredModelAttributes } from '@server/types' | ||
6 | import { MVideoFullLight } from '@server/types/models' | ||
7 | import { VideoUpdate } from '../../../../shared' | ||
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' | ||
9 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
10 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | ||
11 | import { createReqFiles } from '../../../helpers/express-utils' | ||
12 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
13 | import { CONFIG } from '../../../initializers/config' | ||
14 | import { MIMETYPES } from '../../../initializers/constants' | ||
15 | import { sequelizeTypescript } from '../../../initializers/database' | ||
16 | import { federateVideoIfNeeded } from '../../../lib/activitypub/videos' | ||
17 | import { Notifier } from '../../../lib/notifier' | ||
18 | import { Hooks } from '../../../lib/plugins/hooks' | ||
19 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
20 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' | ||
21 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
22 | import { VideoModel } from '../../../models/video/video' | ||
23 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
24 | |||
25 | const lTags = loggerTagsFactory('api', 'video') | ||
26 | const auditLogger = auditLoggerFactory('videos') | ||
27 | const updateRouter = express.Router() | ||
28 | |||
29 | const reqVideoFileUpdate = createReqFiles( | ||
30 | [ 'thumbnailfile', 'previewfile' ], | ||
31 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
32 | { | ||
33 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
34 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
35 | } | ||
36 | ) | ||
37 | |||
38 | updateRouter.put('/:id', | ||
39 | openapiOperationDoc({ operationId: 'putVideo' }), | ||
40 | authenticate, | ||
41 | reqVideoFileUpdate, | ||
42 | asyncMiddleware(videosUpdateValidator), | ||
43 | asyncRetryTransactionMiddleware(updateVideo) | ||
44 | ) | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | export { | ||
49 | updateRouter | ||
50 | } | ||
51 | |||
52 | // --------------------------------------------------------------------------- | ||
53 | |||
54 | export async function updateVideo (req: express.Request, res: express.Response) { | ||
55 | const videoInstance = res.locals.videoAll | ||
56 | const videoFieldsSave = videoInstance.toJSON() | ||
57 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | ||
58 | const videoInfoToUpdate: VideoUpdate = req.body | ||
59 | |||
60 | const wasConfidentialVideo = videoInstance.isConfidential() | ||
61 | const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation() | ||
62 | |||
63 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
64 | video: videoInstance, | ||
65 | files: req.files, | ||
66 | fallback: () => Promise.resolve(undefined), | ||
67 | automaticallyGenerated: false | ||
68 | }) | ||
69 | |||
70 | try { | ||
71 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { | ||
72 | const sequelizeOptions = { transaction: t } | ||
73 | const oldVideoChannel = videoInstance.VideoChannel | ||
74 | |||
75 | const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [ | ||
76 | 'name', | ||
77 | 'category', | ||
78 | 'licence', | ||
79 | 'language', | ||
80 | 'nsfw', | ||
81 | 'waitTranscoding', | ||
82 | 'support', | ||
83 | 'description', | ||
84 | 'commentsEnabled', | ||
85 | 'downloadEnabled' | ||
86 | ] | ||
87 | |||
88 | for (const key of keysToUpdate) { | ||
89 | if (videoInfoToUpdate[key] !== undefined) videoInstance.set(key, videoInfoToUpdate[key]) | ||
90 | } | ||
91 | |||
92 | if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { | ||
93 | videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) | ||
94 | } | ||
95 | |||
96 | // Privacy update? | ||
97 | let isNewVideo = false | ||
98 | if (videoInfoToUpdate.privacy !== undefined) { | ||
99 | isNewVideo = await updateVideoPrivacy({ videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction: t }) | ||
100 | } | ||
101 | |||
102 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight | ||
103 | |||
104 | // Thumbnail & preview updates? | ||
105 | if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
106 | if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) | ||
107 | |||
108 | // Video tags update? | ||
109 | if (videoInfoToUpdate.tags !== undefined) { | ||
110 | await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t }) | ||
111 | } | ||
112 | |||
113 | // Video channel update? | ||
114 | if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { | ||
115 | await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) | ||
116 | videoInstanceUpdated.VideoChannel = res.locals.videoChannel | ||
117 | |||
118 | if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) | ||
119 | } | ||
120 | |||
121 | // Schedule an update in the future? | ||
122 | await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t) | ||
123 | |||
124 | await autoBlacklistVideoIfNeeded({ | ||
125 | video: videoInstanceUpdated, | ||
126 | user: res.locals.oauth.token.User, | ||
127 | isRemote: false, | ||
128 | isNew: false, | ||
129 | transaction: t | ||
130 | }) | ||
131 | |||
132 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | ||
133 | |||
134 | auditLogger.update( | ||
135 | getAuditIdFromRes(res), | ||
136 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | ||
137 | oldVideoAuditView | ||
138 | ) | ||
139 | logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid)) | ||
140 | |||
141 | return videoInstanceUpdated | ||
142 | }) | ||
143 | |||
144 | if (wasConfidentialVideo) { | ||
145 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) | ||
146 | } | ||
147 | |||
148 | Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body }) | ||
149 | } catch (err) { | ||
150 | // Force fields we want to update | ||
151 | // If the transaction is retried, sequelize will think the object has not changed | ||
152 | // So it will skip the SQL request, even if the last one was ROLLBACKed! | ||
153 | resetSequelizeInstance(videoInstance, videoFieldsSave) | ||
154 | |||
155 | throw err | ||
156 | } | ||
157 | |||
158 | return res.type('json') | ||
159 | .status(HttpStatusCode.NO_CONTENT_204) | ||
160 | .end() | ||
161 | } | ||
162 | |||
163 | async function updateVideoPrivacy (options: { | ||
164 | videoInstance: MVideoFullLight | ||
165 | videoInfoToUpdate: VideoUpdate | ||
166 | hadPrivacyForFederation: boolean | ||
167 | transaction: Transaction | ||
168 | }) { | ||
169 | const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options | ||
170 | const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) | ||
171 | |||
172 | const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) | ||
173 | videoInstance.setPrivacy(newPrivacy) | ||
174 | |||
175 | // Unfederate the video if the new privacy is not compatible with federation | ||
176 | if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { | ||
177 | await VideoModel.sendDelete(videoInstance, { transaction }) | ||
178 | } | ||
179 | |||
180 | return isNewVideo | ||
181 | } | ||
182 | |||
183 | function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) { | ||
184 | if (videoInfoToUpdate.scheduleUpdate) { | ||
185 | return ScheduleVideoUpdateModel.upsert({ | ||
186 | videoId: videoInstance.id, | ||
187 | updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt), | ||
188 | privacy: videoInfoToUpdate.scheduleUpdate.privacy || null | ||
189 | }, { transaction }) | ||
190 | } else if (videoInfoToUpdate.scheduleUpdate === null) { | ||
191 | return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) | ||
192 | } | ||
193 | } | ||
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts new file mode 100644 index 000000000..bcd21ac99 --- /dev/null +++ b/server/controllers/api/videos/upload.ts | |||
@@ -0,0 +1,278 @@ | |||
1 | import * as express from 'express' | ||
2 | import { move } from 'fs-extra' | ||
3 | import { getLowercaseExtension } from '@server/helpers/core-utils' | ||
4 | import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' | ||
5 | import { uuidToShort } from '@server/helpers/uuid' | ||
6 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | ||
7 | import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' | ||
8 | import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | ||
9 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
10 | import { openapiOperationDoc } from '@server/middlewares/doc' | ||
11 | import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | ||
12 | import { uploadx } from '@uploadx/core' | ||
13 | import { VideoCreate, VideoState } from '../../../../shared' | ||
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs' | ||
15 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | ||
16 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
17 | import { createReqFiles } from '../../../helpers/express-utils' | ||
18 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | ||
19 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
20 | import { CONFIG } from '../../../initializers/config' | ||
21 | import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants' | ||
22 | import { sequelizeTypescript } from '../../../initializers/database' | ||
23 | import { federateVideoIfNeeded } from '../../../lib/activitypub/videos' | ||
24 | import { Notifier } from '../../../lib/notifier' | ||
25 | import { Hooks } from '../../../lib/plugins/hooks' | ||
26 | import { generateVideoMiniature } from '../../../lib/thumbnail' | ||
27 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | ||
28 | import { | ||
29 | asyncMiddleware, | ||
30 | asyncRetryTransactionMiddleware, | ||
31 | authenticate, | ||
32 | videosAddLegacyValidator, | ||
33 | videosAddResumableInitValidator, | ||
34 | videosAddResumableValidator | ||
35 | } from '../../../middlewares' | ||
36 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | ||
37 | import { VideoModel } from '../../../models/video/video' | ||
38 | import { VideoFileModel } from '../../../models/video/video-file' | ||
39 | |||
40 | const lTags = loggerTagsFactory('api', 'video') | ||
41 | const auditLogger = auditLoggerFactory('videos') | ||
42 | const uploadRouter = express.Router() | ||
43 | const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() }) | ||
44 | |||
45 | const reqVideoFileAdd = createReqFiles( | ||
46 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | ||
47 | Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), | ||
48 | { | ||
49 | videofile: CONFIG.STORAGE.TMP_DIR, | ||
50 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, | ||
51 | previewfile: CONFIG.STORAGE.TMP_DIR | ||
52 | } | ||
53 | ) | ||
54 | |||
55 | const reqVideoFileAddResumable = createReqFiles( | ||
56 | [ 'thumbnailfile', 'previewfile' ], | ||
57 | MIMETYPES.IMAGE.MIMETYPE_EXT, | ||
58 | { | ||
59 | thumbnailfile: getResumableUploadPath(), | ||
60 | previewfile: getResumableUploadPath() | ||
61 | } | ||
62 | ) | ||
63 | |||
64 | uploadRouter.post('/upload', | ||
65 | openapiOperationDoc({ operationId: 'uploadLegacy' }), | ||
66 | authenticate, | ||
67 | reqVideoFileAdd, | ||
68 | asyncMiddleware(videosAddLegacyValidator), | ||
69 | asyncRetryTransactionMiddleware(addVideoLegacy) | ||
70 | ) | ||
71 | |||
72 | uploadRouter.post('/upload-resumable', | ||
73 | openapiOperationDoc({ operationId: 'uploadResumableInit' }), | ||
74 | authenticate, | ||
75 | reqVideoFileAddResumable, | ||
76 | asyncMiddleware(videosAddResumableInitValidator), | ||
77 | uploadxMiddleware | ||
78 | ) | ||
79 | |||
80 | uploadRouter.delete('/upload-resumable', | ||
81 | authenticate, | ||
82 | uploadxMiddleware | ||
83 | ) | ||
84 | |||
85 | uploadRouter.put('/upload-resumable', | ||
86 | openapiOperationDoc({ operationId: 'uploadResumable' }), | ||
87 | authenticate, | ||
88 | uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes | ||
89 | asyncMiddleware(videosAddResumableValidator), | ||
90 | asyncMiddleware(addVideoResumable) | ||
91 | ) | ||
92 | |||
93 | // --------------------------------------------------------------------------- | ||
94 | |||
95 | export { | ||
96 | uploadRouter | ||
97 | } | ||
98 | |||
99 | // --------------------------------------------------------------------------- | ||
100 | |||
101 | export async function addVideoLegacy (req: express.Request, res: express.Response) { | ||
102 | // Uploading the video could be long | ||
103 | // Set timeout to 10 minutes, as Express's default is 2 minutes | ||
104 | req.setTimeout(1000 * 60 * 10, () => { | ||
105 | logger.error('Video upload has timed out.') | ||
106 | return res.fail({ | ||
107 | status: HttpStatusCode.REQUEST_TIMEOUT_408, | ||
108 | message: 'Video upload has timed out.' | ||
109 | }) | ||
110 | }) | ||
111 | |||
112 | const videoPhysicalFile = req.files['videofile'][0] | ||
113 | const videoInfo: VideoCreate = req.body | ||
114 | const files = req.files | ||
115 | |||
116 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
117 | } | ||
118 | |||
119 | export async function addVideoResumable (_req: express.Request, res: express.Response) { | ||
120 | const videoPhysicalFile = res.locals.videoFileResumable | ||
121 | const videoInfo = videoPhysicalFile.metadata | ||
122 | const files = { previewfile: videoInfo.previewfile } | ||
123 | |||
124 | // Don't need the meta file anymore | ||
125 | await deleteResumableUploadMetaFile(videoPhysicalFile.path) | ||
126 | |||
127 | return addVideo({ res, videoPhysicalFile, videoInfo, files }) | ||
128 | } | ||
129 | |||
130 | async function addVideo (options: { | ||
131 | res: express.Response | ||
132 | videoPhysicalFile: express.VideoUploadFile | ||
133 | videoInfo: VideoCreate | ||
134 | files: express.UploadFiles | ||
135 | }) { | ||
136 | const { res, videoPhysicalFile, videoInfo, files } = options | ||
137 | const videoChannel = res.locals.videoChannel | ||
138 | const user = res.locals.oauth.token.User | ||
139 | |||
140 | const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) | ||
141 | |||
142 | videoData.state = CONFIG.TRANSCODING.ENABLED | ||
143 | ? VideoState.TO_TRANSCODE | ||
144 | : VideoState.PUBLISHED | ||
145 | |||
146 | videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware | ||
147 | |||
148 | const video = new VideoModel(videoData) as MVideoFullLight | ||
149 | video.VideoChannel = videoChannel | ||
150 | video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object | ||
151 | |||
152 | const videoFile = await buildNewFile(video, videoPhysicalFile) | ||
153 | |||
154 | // Move physical file | ||
155 | const destination = getVideoFilePath(video, videoFile) | ||
156 | await move(videoPhysicalFile.path, destination) | ||
157 | // This is important in case if there is another attempt in the retry process | ||
158 | videoPhysicalFile.filename = getVideoFilePath(video, videoFile) | ||
159 | videoPhysicalFile.path = destination | ||
160 | |||
161 | const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ | ||
162 | video, | ||
163 | files, | ||
164 | fallback: type => generateVideoMiniature({ video, videoFile, type }) | ||
165 | }) | ||
166 | |||
167 | const { videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
168 | const sequelizeOptions = { transaction: t } | ||
169 | |||
170 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | ||
171 | |||
172 | await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
173 | await videoCreated.addAndSaveThumbnail(previewModel, t) | ||
174 | |||
175 | // Do not forget to add video channel information to the created video | ||
176 | videoCreated.VideoChannel = res.locals.videoChannel | ||
177 | |||
178 | videoFile.videoId = video.id | ||
179 | await videoFile.save(sequelizeOptions) | ||
180 | |||
181 | video.VideoFiles = [ videoFile ] | ||
182 | |||
183 | await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) | ||
184 | |||
185 | // Schedule an update in the future? | ||
186 | if (videoInfo.scheduleUpdate) { | ||
187 | await ScheduleVideoUpdateModel.create({ | ||
188 | videoId: video.id, | ||
189 | updateAt: new Date(videoInfo.scheduleUpdate.updateAt), | ||
190 | privacy: videoInfo.scheduleUpdate.privacy || null | ||
191 | }, sequelizeOptions) | ||
192 | } | ||
193 | |||
194 | // Channel has a new content, set as updated | ||
195 | await videoCreated.VideoChannel.setAsUpdated(t) | ||
196 | |||
197 | await autoBlacklistVideoIfNeeded({ | ||
198 | video, | ||
199 | user, | ||
200 | isRemote: false, | ||
201 | isNew: true, | ||
202 | transaction: t | ||
203 | }) | ||
204 | |||
205 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | ||
206 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) | ||
207 | |||
208 | return { videoCreated } | ||
209 | }) | ||
210 | |||
211 | createTorrentFederate(video, videoFile) | ||
212 | |||
213 | if (video.state === VideoState.TO_TRANSCODE) { | ||
214 | await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user) | ||
215 | } | ||
216 | |||
217 | Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) | ||
218 | |||
219 | return res.json({ | ||
220 | video: { | ||
221 | id: videoCreated.id, | ||
222 | shortUUID: uuidToShort(videoCreated.uuid), | ||
223 | uuid: videoCreated.uuid | ||
224 | } | ||
225 | }) | ||
226 | } | ||
227 | |||
228 | async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) { | ||
229 | const videoFile = new VideoFileModel({ | ||
230 | extname: getLowercaseExtension(videoPhysicalFile.filename), | ||
231 | size: videoPhysicalFile.size, | ||
232 | videoStreamingPlaylistId: null, | ||
233 | metadata: await getMetadataFromFile(videoPhysicalFile.path) | ||
234 | }) | ||
235 | |||
236 | if (videoFile.isAudio()) { | ||
237 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION | ||
238 | } else { | ||
239 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) | ||
240 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | ||
241 | } | ||
242 | |||
243 | videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname) | ||
244 | |||
245 | return videoFile | ||
246 | } | ||
247 | |||
248 | async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) { | ||
249 | await createTorrentAndSetInfoHash(video, fileArg) | ||
250 | |||
251 | // Refresh videoFile because the createTorrentAndSetInfoHash could be long | ||
252 | const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id) | ||
253 | // File does not exist anymore, remove the generated torrent | ||
254 | if (!refreshedFile) return fileArg.removeTorrent() | ||
255 | |||
256 | refreshedFile.infoHash = fileArg.infoHash | ||
257 | refreshedFile.torrentFilename = fileArg.torrentFilename | ||
258 | |||
259 | return refreshedFile.save() | ||
260 | } | ||
261 | |||
262 | function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void { | ||
263 | // Create the torrent file in async way because it could be long | ||
264 | createTorrentAndSetInfoHashAsync(video, videoFile) | ||
265 | .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) })) | ||
266 | .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) | ||
267 | .then(refreshedVideo => { | ||
268 | if (!refreshedVideo) return | ||
269 | |||
270 | // Only federate and notify after the torrent creation | ||
271 | Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo) | ||
272 | |||
273 | return retryTransactionWrapper(() => { | ||
274 | return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) | ||
275 | }) | ||
276 | }) | ||
277 | .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) })) | ||
278 | } | ||
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts index 627f12aa9..8b15525aa 100644 --- a/server/controllers/api/videos/watching.ts +++ b/server/controllers/api/videos/watching.ts | |||
@@ -1,12 +1,19 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { UserWatchingVideo } from '../../../../shared' | 2 | import { UserWatchingVideo } from '../../../../shared' |
3 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' | 3 | import { |
4 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | 4 | asyncMiddleware, |
5 | asyncRetryTransactionMiddleware, | ||
6 | authenticate, | ||
7 | openapiOperationDoc, | ||
8 | videoWatchingValidator | ||
9 | } from '../../../middlewares' | ||
10 | import { UserVideoHistoryModel } from '../../../models/user/user-video-history' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 11 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
6 | 12 | ||
7 | const watchingRouter = express.Router() | 13 | const watchingRouter = express.Router() |
8 | 14 | ||
9 | watchingRouter.put('/:videoId/watching', | 15 | watchingRouter.put('/:videoId/watching', |
16 | openapiOperationDoc({ operationId: 'setProgress' }), | ||
10 | authenticate, | 17 | authenticate, |
11 | asyncMiddleware(videoWatchingValidator), | 18 | asyncMiddleware(videoWatchingValidator), |
12 | asyncRetryTransactionMiddleware(userWatchVideo) | 19 | asyncRetryTransactionMiddleware(userWatchVideo) |
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts index 8d1fa72f3..9e92063d4 100644 --- a/server/controllers/bots.ts +++ b/server/controllers/bots.ts | |||
@@ -75,7 +75,7 @@ async function getSitemapLocalVideoUrls () { | |||
75 | }) | 75 | }) |
76 | 76 | ||
77 | return data.map(v => ({ | 77 | return data.map(v => ({ |
78 | url: WEBSERVER.URL + '/videos/watch/' + v.uuid, | 78 | url: WEBSERVER.URL + '/w/' + v.uuid, |
79 | video: [ | 79 | video: [ |
80 | { | 80 | { |
81 | title: v.name, | 81 | title: v.name, |
diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 022a17ff4..eb1ee6cbd 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts | |||
@@ -19,10 +19,11 @@ const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html') | |||
19 | 19 | ||
20 | // Special route that add OpenGraph and oEmbed tags | 20 | // Special route that add OpenGraph and oEmbed tags |
21 | // Do not use a template engine for a so little thing | 21 | // Do not use a template engine for a so little thing |
22 | clientsRouter.use('/videos/watch/playlist/:id', asyncMiddleware(generateWatchPlaylistHtmlPage)) | 22 | clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ], asyncMiddleware(generateWatchPlaylistHtmlPage)) |
23 | clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) | 23 | clientsRouter.use([ '/w/:id', '/videos/watch/:id' ], asyncMiddleware(generateWatchHtmlPage)) |
24 | clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage)) | 24 | clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], asyncMiddleware(generateAccountHtmlPage)) |
25 | clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage)) | 25 | clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], asyncMiddleware(generateVideoChannelHtmlPage)) |
26 | clientsRouter.use('/@:nameWithHost', asyncMiddleware(generateActorHtmlPage)) | ||
26 | 27 | ||
27 | const embedMiddlewares = [ | 28 | const embedMiddlewares = [ |
28 | CONFIG.CSP.ENABLED | 29 | CONFIG.CSP.ENABLED |
@@ -77,7 +78,7 @@ clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.C | |||
77 | 78 | ||
78 | // 404 for static files not found | 79 | // 404 for static files not found |
79 | clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => { | 80 | clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => { |
80 | res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 81 | res.status(HttpStatusCode.NOT_FOUND_404).end() |
81 | }) | 82 | }) |
82 | 83 | ||
83 | // Always serve index client page (the client is a single page application, let it handle routing) | 84 | // Always serve index client page (the client is a single page application, let it handle routing) |
@@ -104,7 +105,7 @@ function serveServerTranslations (req: express.Request, res: express.Response) { | |||
104 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) | 105 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) |
105 | } | 106 | } |
106 | 107 | ||
107 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 108 | return res.status(HttpStatusCode.NOT_FOUND_404).end() |
108 | } | 109 | } |
109 | 110 | ||
110 | async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { | 111 | async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { |
@@ -155,6 +156,12 @@ async function generateVideoChannelHtmlPage (req: express.Request, res: express. | |||
155 | return sendHTML(html, res) | 156 | return sendHTML(html, res) |
156 | } | 157 | } |
157 | 158 | ||
159 | async function generateActorHtmlPage (req: express.Request, res: express.Response) { | ||
160 | const html = await ClientHtml.getActorHTMLPage(req.params.nameWithHost, req, res) | ||
161 | |||
162 | return sendHTML(html, res) | ||
163 | } | ||
164 | |||
158 | async function generateManifest (req: express.Request, res: express.Response) { | 165 | async function generateManifest (req: express.Request, res: express.Response) { |
159 | const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest') | 166 | const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest') |
160 | const manifestJson = await readFile(manifestPhysicalPath, 'utf8') | 167 | const manifestJson = await readFile(manifestPhysicalPath, 'utf8') |
diff --git a/server/controllers/download.ts b/server/controllers/download.ts index 9a8194c5c..4293a32e2 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts | |||
@@ -41,7 +41,12 @@ export { | |||
41 | 41 | ||
42 | async function downloadTorrent (req: express.Request, res: express.Response) { | 42 | async function downloadTorrent (req: express.Request, res: express.Response) { |
43 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 43 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) |
44 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 44 | if (!result) { |
45 | return res.fail({ | ||
46 | status: HttpStatusCode.NOT_FOUND_404, | ||
47 | message: 'Torrent file not found' | ||
48 | }) | ||
49 | } | ||
45 | 50 | ||
46 | const allowParameters = { torrentPath: result.path, downloadName: result.downloadName } | 51 | const allowParameters = { torrentPath: result.path, downloadName: result.downloadName } |
47 | 52 | ||
@@ -60,7 +65,12 @@ async function downloadVideoFile (req: express.Request, res: express.Response) { | |||
60 | const video = res.locals.videoAll | 65 | const video = res.locals.videoAll |
61 | 66 | ||
62 | const videoFile = getVideoFile(req, video.VideoFiles) | 67 | const videoFile = getVideoFile(req, video.VideoFiles) |
63 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 68 | if (!videoFile) { |
69 | return res.fail({ | ||
70 | status: HttpStatusCode.NOT_FOUND_404, | ||
71 | message: 'Video file not found' | ||
72 | }) | ||
73 | } | ||
64 | 74 | ||
65 | const allowParameters = { video, videoFile } | 75 | const allowParameters = { video, videoFile } |
66 | 76 | ||
@@ -81,7 +91,12 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response | |||
81 | if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end | 91 | if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end |
82 | 92 | ||
83 | const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) | 93 | const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) |
84 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 94 | if (!videoFile) { |
95 | return res.fail({ | ||
96 | status: HttpStatusCode.NOT_FOUND_404, | ||
97 | message: 'Video file not found' | ||
98 | }) | ||
99 | } | ||
85 | 100 | ||
86 | const allowParameters = { video, streamingPlaylist, videoFile } | 101 | const allowParameters = { video, streamingPlaylist, videoFile } |
87 | 102 | ||
@@ -131,9 +146,11 @@ function isVideoDownloadAllowed (_object: { | |||
131 | function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { | 146 | function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { |
132 | if (!result || result.allowed !== true) { | 147 | if (!result || result.allowed !== true) { |
133 | logger.info('Download is not allowed.', { result, allowParameters }) | 148 | logger.info('Download is not allowed.', { result, allowParameters }) |
134 | res.status(HttpStatusCode.FORBIDDEN_403) | ||
135 | .json({ error: result?.errorMessage || 'Refused download' }) | ||
136 | 149 | ||
150 | res.fail({ | ||
151 | status: HttpStatusCode.FORBIDDEN_403, | ||
152 | message: result?.errorMessage || 'Refused download' | ||
153 | }) | ||
137 | return false | 154 | return false |
138 | } | 155 | } |
139 | 156 | ||
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index f0717bbbc..435b12193 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as Feed from 'pfeed' | 2 | import * as Feed from 'pfeed' |
3 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' | ||
3 | import { VideoFilter } from '../../shared/models/videos/video-query.type' | 4 | import { VideoFilter } from '../../shared/models/videos/video-query.type' |
4 | import { buildNSFWFilter } from '../helpers/express-utils' | 5 | import { buildNSFWFilter } from '../helpers/express-utils' |
5 | import { CONFIG } from '../initializers/config' | 6 | import { CONFIG } from '../initializers/config' |
@@ -286,14 +287,14 @@ function addVideosToFeed (feed, videos: VideoModel[]) { | |||
286 | if (video.category) { | 287 | if (video.category) { |
287 | categories.push({ | 288 | categories.push({ |
288 | value: video.category, | 289 | value: video.category, |
289 | label: VideoModel.getCategoryLabel(video.category) | 290 | label: getCategoryLabel(video.category) |
290 | }) | 291 | }) |
291 | } | 292 | } |
292 | 293 | ||
293 | feed.addItem({ | 294 | feed.addItem({ |
294 | title: video.name, | 295 | title: video.name, |
295 | id: video.url, | 296 | id: video.url, |
296 | link: WEBSERVER.URL + '/videos/watch/' + video.uuid, | 297 | link: WEBSERVER.URL + '/w/' + video.uuid, |
297 | description: video.getTruncatedDescription(), | 298 | description: video.getTruncatedDescription(), |
298 | content: video.description, | 299 | content: video.description, |
299 | author: [ | 300 | author: [ |
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts index 6f71fdb16..9a7dacba0 100644 --- a/server/controllers/lazy-static.ts +++ b/server/controllers/lazy-static.ts | |||
@@ -4,10 +4,10 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache | |||
4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' | 6 | import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' |
7 | import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image' | ||
8 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' | 7 | import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' |
8 | import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/local-actor' | ||
9 | import { asyncMiddleware } from '../middlewares' | 9 | import { asyncMiddleware } from '../middlewares' |
10 | import { ActorImageModel } from '../models/account/actor-image' | 10 | import { ActorImageModel } from '../models/actor/actor-image' |
11 | 11 | ||
12 | const lazyStaticRouter = express.Router() | 12 | const lazyStaticRouter = express.Router() |
13 | 13 | ||
@@ -48,7 +48,7 @@ export { | |||
48 | 48 | ||
49 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
50 | 50 | ||
51 | async function getActorImage (req: express.Request, res: express.Response) { | 51 | async function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { |
52 | const filename = req.params.filename | 52 | const filename = req.params.filename |
53 | 53 | ||
54 | if (actorImagePathUnsafeCache.has(filename)) { | 54 | if (actorImagePathUnsafeCache.has(filename)) { |
@@ -56,10 +56,10 @@ async function getActorImage (req: express.Request, res: express.Response) { | |||
56 | } | 56 | } |
57 | 57 | ||
58 | const image = await ActorImageModel.loadByName(filename) | 58 | const image = await ActorImageModel.loadByName(filename) |
59 | if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 59 | if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
60 | 60 | ||
61 | if (image.onDisk === false) { | 61 | if (image.onDisk === false) { |
62 | if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 62 | if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
63 | 63 | ||
64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) | 64 | logger.info('Lazy serve remote actor image %s.', image.fileUrl) |
65 | 65 | ||
@@ -67,7 +67,7 @@ async function getActorImage (req: express.Request, res: express.Response) { | |||
67 | await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) | 67 | await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) |
68 | } catch (err) { | 68 | } catch (err) { |
69 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) | 69 | logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) |
70 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 70 | return res.status(HttpStatusCode.NOT_FOUND_404).end() |
71 | } | 71 | } |
72 | 72 | ||
73 | image.onDisk = true | 73 | image.onDisk = true |
@@ -78,26 +78,42 @@ async function getActorImage (req: express.Request, res: express.Response) { | |||
78 | const path = image.getPath() | 78 | const path = image.getPath() |
79 | 79 | ||
80 | actorImagePathUnsafeCache.set(filename, path) | 80 | actorImagePathUnsafeCache.set(filename, path) |
81 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 81 | |
82 | return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }, (err: any) => { | ||
83 | if (!err) return | ||
84 | |||
85 | // It seems this actor image is not on the disk anymore | ||
86 | if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { | ||
87 | logger.error('Cannot lazy serve actor image %s.', filename, { err }) | ||
88 | |||
89 | actorImagePathUnsafeCache.del(filename) | ||
90 | |||
91 | image.onDisk = false | ||
92 | image.save() | ||
93 | .catch(err => logger.error('Cannot save new actor image disk state.', { err })) | ||
94 | } | ||
95 | |||
96 | return next(err) | ||
97 | }) | ||
82 | } | 98 | } |
83 | 99 | ||
84 | async function getPreview (req: express.Request, res: express.Response) { | 100 | async function getPreview (req: express.Request, res: express.Response) { |
85 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) | 101 | const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) |
86 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 102 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
87 | 103 | ||
88 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 104 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
89 | } | 105 | } |
90 | 106 | ||
91 | async function getVideoCaption (req: express.Request, res: express.Response) { | 107 | async function getVideoCaption (req: express.Request, res: express.Response) { |
92 | const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) | 108 | const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) |
93 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 109 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
94 | 110 | ||
95 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) | 111 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) |
96 | } | 112 | } |
97 | 113 | ||
98 | async function getTorrent (req: express.Request, res: express.Response) { | 114 | async function getTorrent (req: express.Request, res: express.Response) { |
99 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 115 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) |
100 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 116 | if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
101 | 117 | ||
102 | // Torrents still use the old naming convention (video uuid + .torrent) | 118 | // Torrents still use the old naming convention (video uuid + .torrent) |
103 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) | 119 | return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) |
diff --git a/server/controllers/live.ts b/server/controllers/live.ts index ff48b0e21..f2686fb23 100644 --- a/server/controllers/live.ts +++ b/server/controllers/live.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { mapToJSON } from '@server/helpers/core-utils' | 3 | import { mapToJSON } from '@server/helpers/core-utils' |
4 | import { LiveManager } from '@server/lib/live-manager' | 4 | import { LiveSegmentShaStore } from '@server/lib/live' |
5 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 5 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
6 | 6 | ||
7 | const liveRouter = express.Router() | 7 | const liveRouter = express.Router() |
@@ -22,10 +22,10 @@ export { | |||
22 | function getSegmentsSha256 (req: express.Request, res: express.Response) { | 22 | function getSegmentsSha256 (req: express.Request, res: express.Response) { |
23 | const videoUUID = req.params.videoUUID | 23 | const videoUUID = req.params.videoUUID |
24 | 24 | ||
25 | const result = LiveManager.Instance.getSegmentsSha256(videoUUID) | 25 | const result = LiveSegmentShaStore.Instance.getSegmentsSha256(videoUUID) |
26 | 26 | ||
27 | if (!result) { | 27 | if (!result) { |
28 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 28 | return res.status(HttpStatusCode.NOT_FOUND_404).end() |
29 | } | 29 | } |
30 | 30 | ||
31 | return res.json(mapToJSON(result)) | 31 | return res.json(mapToJSON(result)) |
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts index 105f51518..7213e3f15 100644 --- a/server/controllers/plugins.ts +++ b/server/controllers/plugins.ts | |||
@@ -100,7 +100,7 @@ function getPluginTranslations (req: express.Request, res: express.Response) { | |||
100 | return res.json(json) | 100 | return res.json(json) |
101 | } | 101 | } |
102 | 102 | ||
103 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 103 | return res.status(HttpStatusCode.NOT_FOUND_404).end() |
104 | } | 104 | } |
105 | 105 | ||
106 | function servePluginStaticDirectory (req: express.Request, res: express.Response) { | 106 | function servePluginStaticDirectory (req: express.Request, res: express.Response) { |
@@ -110,7 +110,7 @@ function servePluginStaticDirectory (req: express.Request, res: express.Response | |||
110 | const [ directory, ...file ] = staticEndpoint.split('/') | 110 | const [ directory, ...file ] = staticEndpoint.split('/') |
111 | 111 | ||
112 | const staticPath = plugin.staticDirs[directory] | 112 | const staticPath = plugin.staticDirs[directory] |
113 | if (!staticPath) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 113 | if (!staticPath) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
114 | 114 | ||
115 | const filepath = file.join('/') | 115 | const filepath = file.join('/') |
116 | return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions) | 116 | return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions) |
@@ -120,7 +120,7 @@ function servePluginCustomRoutes (req: express.Request, res: express.Response, n | |||
120 | const plugin: RegisteredPlugin = res.locals.registeredPlugin | 120 | const plugin: RegisteredPlugin = res.locals.registeredPlugin |
121 | const router = PluginManager.Instance.getRouter(plugin.npmName) | 121 | const router = PluginManager.Instance.getRouter(plugin.npmName) |
122 | 122 | ||
123 | if (!router) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 123 | if (!router) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
124 | 124 | ||
125 | return router(req, res, next) | 125 | return router(req, res, next) |
126 | } | 126 | } |
@@ -130,7 +130,7 @@ function servePluginClientScripts (req: express.Request, res: express.Response) | |||
130 | const staticEndpoint = req.params.staticEndpoint | 130 | const staticEndpoint = req.params.staticEndpoint |
131 | 131 | ||
132 | const file = plugin.clientScripts[staticEndpoint] | 132 | const file = plugin.clientScripts[staticEndpoint] |
133 | if (!file) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 133 | if (!file) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
134 | 134 | ||
135 | return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) | 135 | return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) |
136 | } | 136 | } |
@@ -140,7 +140,7 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) { | |||
140 | const staticEndpoint = req.params.staticEndpoint | 140 | const staticEndpoint = req.params.staticEndpoint |
141 | 141 | ||
142 | if (plugin.css.includes(staticEndpoint) === false) { | 142 | if (plugin.css.includes(staticEndpoint) === false) { |
143 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 143 | return res.status(HttpStatusCode.NOT_FOUND_404).end() |
144 | } | 144 | } |
145 | 145 | ||
146 | return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) | 146 | return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) |
diff --git a/server/controllers/services.ts b/server/controllers/services.ts index 189e1651b..8c0af9ff7 100644 --- a/server/controllers/services.ts +++ b/server/controllers/services.ts | |||
@@ -78,17 +78,18 @@ function buildOEmbed (options: { | |||
78 | const maxWidth = parseInt(req.query.maxwidth, 10) | 78 | const maxWidth = parseInt(req.query.maxwidth, 10) |
79 | 79 | ||
80 | const embedUrl = webserverUrl + embedPath | 80 | const embedUrl = webserverUrl + embedPath |
81 | let embedWidth = EMBED_SIZE.width | ||
82 | let embedHeight = EMBED_SIZE.height | ||
83 | const embedTitle = escapeHTML(title) | 81 | const embedTitle = escapeHTML(title) |
84 | 82 | ||
85 | let thumbnailUrl = previewPath | 83 | let thumbnailUrl = previewPath |
86 | ? webserverUrl + previewPath | 84 | ? webserverUrl + previewPath |
87 | : undefined | 85 | : undefined |
88 | 86 | ||
89 | if (maxHeight < embedHeight) embedHeight = maxHeight | 87 | let embedWidth = EMBED_SIZE.width |
90 | if (maxWidth < embedWidth) embedWidth = maxWidth | 88 | if (maxWidth < embedWidth) embedWidth = maxWidth |
91 | 89 | ||
90 | let embedHeight = EMBED_SIZE.height | ||
91 | if (maxHeight < embedHeight) embedHeight = maxHeight | ||
92 | |||
92 | // Our thumbnail is too big for the consumer | 93 | // Our thumbnail is too big for the consumer |
93 | if ( | 94 | if ( |
94 | (maxHeight !== undefined && maxHeight < previewSize.height) || | 95 | (maxHeight !== undefined && maxHeight < previewSize.height) || |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 8d9003a3e..35e024dda 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -2,9 +2,9 @@ import * as cors from 'cors' | |||
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { serveIndexHTML } from '@server/lib/client-html' | 4 | import { serveIndexHTML } from '@server/lib/client-html' |
5 | import { getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config' | 5 | import { ServerConfigManager } from '@server/lib/server-config-manager' |
6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
7 | import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' | 7 | import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model' |
8 | import { root } from '../helpers/core-utils' | 8 | import { root } from '../helpers/core-utils' |
9 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 9 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
10 | import { | 10 | import { |
@@ -18,10 +18,9 @@ import { | |||
18 | WEBSERVER | 18 | WEBSERVER |
19 | } from '../initializers/constants' | 19 | } from '../initializers/constants' |
20 | import { getThemeOrDefault } from '../lib/plugins/theme-utils' | 20 | import { getThemeOrDefault } from '../lib/plugins/theme-utils' |
21 | import { getEnabledResolutions } from '../lib/video-transcoding' | ||
22 | import { asyncMiddleware } from '../middlewares' | 21 | import { asyncMiddleware } from '../middlewares' |
23 | import { cacheRoute } from '../middlewares/cache' | 22 | import { cacheRoute } from '../middlewares/cache' |
24 | import { UserModel } from '../models/account/user' | 23 | import { UserModel } from '../models/user/user' |
25 | import { VideoModel } from '../models/video/video' | 24 | import { VideoModel } from '../models/video/video' |
26 | import { VideoCommentModel } from '../models/video/video-comment' | 25 | import { VideoCommentModel } from '../models/video/video-comment' |
27 | 26 | ||
@@ -161,143 +160,145 @@ async function generateNodeinfo (req: express.Request, res: express.Response) { | |||
161 | const { totalVideos } = await VideoModel.getStats() | 160 | const { totalVideos } = await VideoModel.getStats() |
162 | const { totalLocalVideoComments } = await VideoCommentModel.getStats() | 161 | const { totalLocalVideoComments } = await VideoCommentModel.getStats() |
163 | const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats() | 162 | const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats() |
164 | let json = {} | ||
165 | 163 | ||
166 | if (req.params.version && (req.params.version === '2.0')) { | 164 | if (!req.params.version || req.params.version !== '2.0') { |
167 | json = { | 165 | return res.fail({ |
168 | version: '2.0', | 166 | status: HttpStatusCode.NOT_FOUND_404, |
169 | software: { | 167 | message: 'Nodeinfo schema version not handled' |
170 | name: 'peertube', | 168 | }) |
171 | version: PEERTUBE_VERSION | 169 | } |
170 | |||
171 | const json = { | ||
172 | version: '2.0', | ||
173 | software: { | ||
174 | name: 'peertube', | ||
175 | version: PEERTUBE_VERSION | ||
176 | }, | ||
177 | protocols: [ | ||
178 | 'activitypub' | ||
179 | ], | ||
180 | services: { | ||
181 | inbound: [], | ||
182 | outbound: [ | ||
183 | 'atom1.0', | ||
184 | 'rss2.0' | ||
185 | ] | ||
186 | }, | ||
187 | openRegistrations: CONFIG.SIGNUP.ENABLED, | ||
188 | usage: { | ||
189 | users: { | ||
190 | total: totalUsers, | ||
191 | activeMonth: totalMonthlyActiveUsers, | ||
192 | activeHalfyear: totalHalfYearActiveUsers | ||
172 | }, | 193 | }, |
173 | protocols: [ | 194 | localPosts: totalVideos, |
174 | 'activitypub' | 195 | localComments: totalLocalVideoComments |
175 | ], | 196 | }, |
176 | services: { | 197 | metadata: { |
177 | inbound: [], | 198 | taxonomy: { |
178 | outbound: [ | 199 | postsName: 'Videos' |
179 | 'atom1.0', | ||
180 | 'rss2.0' | ||
181 | ] | ||
182 | }, | 200 | }, |
183 | openRegistrations: CONFIG.SIGNUP.ENABLED, | 201 | nodeName: CONFIG.INSTANCE.NAME, |
184 | usage: { | 202 | nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, |
185 | users: { | 203 | nodeConfig: { |
186 | total: totalUsers, | 204 | search: { |
187 | activeMonth: totalMonthlyActiveUsers, | 205 | remoteUri: { |
188 | activeHalfyear: totalHalfYearActiveUsers | 206 | users: CONFIG.SEARCH.REMOTE_URI.USERS, |
207 | anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS | ||
208 | } | ||
189 | }, | 209 | }, |
190 | localPosts: totalVideos, | 210 | plugin: { |
191 | localComments: totalLocalVideoComments | 211 | registered: ServerConfigManager.Instance.getRegisteredPlugins() |
192 | }, | ||
193 | metadata: { | ||
194 | taxonomy: { | ||
195 | postsName: 'Videos' | ||
196 | }, | 212 | }, |
197 | nodeName: CONFIG.INSTANCE.NAME, | 213 | theme: { |
198 | nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, | 214 | registered: ServerConfigManager.Instance.getRegisteredThemes(), |
199 | nodeConfig: { | 215 | default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) |
200 | search: { | 216 | }, |
201 | remoteUri: { | 217 | email: { |
202 | users: CONFIG.SEARCH.REMOTE_URI.USERS, | 218 | enabled: isEmailEnabled() |
203 | anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS | 219 | }, |
204 | } | 220 | contactForm: { |
205 | }, | 221 | enabled: CONFIG.CONTACT_FORM.ENABLED |
206 | plugin: { | 222 | }, |
207 | registered: getRegisteredPlugins() | 223 | transcoding: { |
208 | }, | 224 | hls: { |
209 | theme: { | 225 | enabled: CONFIG.TRANSCODING.HLS.ENABLED |
210 | registered: getRegisteredThemes(), | ||
211 | default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) | ||
212 | }, | ||
213 | email: { | ||
214 | enabled: isEmailEnabled() | ||
215 | }, | 226 | }, |
216 | contactForm: { | 227 | webtorrent: { |
217 | enabled: CONFIG.CONTACT_FORM.ENABLED | 228 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED |
218 | }, | 229 | }, |
230 | enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod') | ||
231 | }, | ||
232 | live: { | ||
233 | enabled: CONFIG.LIVE.ENABLED, | ||
219 | transcoding: { | 234 | transcoding: { |
220 | hls: { | 235 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, |
221 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | 236 | enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('live') |
222 | }, | 237 | } |
223 | webtorrent: { | 238 | }, |
224 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | 239 | import: { |
240 | videos: { | ||
241 | http: { | ||
242 | enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED | ||
225 | }, | 243 | }, |
226 | enabledResolutions: getEnabledResolutions('vod') | 244 | torrent: { |
227 | }, | 245 | enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED |
228 | live: { | ||
229 | enabled: CONFIG.LIVE.ENABLED, | ||
230 | transcoding: { | ||
231 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, | ||
232 | enabledResolutions: getEnabledResolutions('live') | ||
233 | } | ||
234 | }, | ||
235 | import: { | ||
236 | videos: { | ||
237 | http: { | ||
238 | enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED | ||
239 | }, | ||
240 | torrent: { | ||
241 | enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED | ||
242 | } | ||
243 | } | ||
244 | }, | ||
245 | autoBlacklist: { | ||
246 | videos: { | ||
247 | ofUsers: { | ||
248 | enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED | ||
249 | } | ||
250 | } | 246 | } |
251 | }, | 247 | } |
252 | avatar: { | 248 | }, |
253 | file: { | 249 | autoBlacklist: { |
254 | size: { | 250 | videos: { |
255 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | 251 | ofUsers: { |
256 | }, | 252 | enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED |
257 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
258 | } | 253 | } |
259 | }, | 254 | } |
260 | video: { | 255 | }, |
261 | image: { | 256 | avatar: { |
262 | extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, | 257 | file: { |
263 | size: { | 258 | size: { |
264 | max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max | 259 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max |
265 | } | ||
266 | }, | 260 | }, |
267 | file: { | 261 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME |
268 | extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME | 262 | } |
269 | } | 263 | }, |
270 | }, | 264 | video: { |
271 | videoCaption: { | 265 | image: { |
272 | file: { | 266 | extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, |
273 | size: { | 267 | size: { |
274 | max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max | 268 | max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max |
275 | }, | ||
276 | extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME | ||
277 | } | ||
278 | }, | ||
279 | user: { | ||
280 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
281 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | ||
282 | }, | ||
283 | trending: { | ||
284 | videos: { | ||
285 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | ||
286 | } | 269 | } |
287 | }, | 270 | }, |
288 | tracker: { | 271 | file: { |
289 | enabled: CONFIG.TRACKER.ENABLED | 272 | extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME |
273 | } | ||
274 | }, | ||
275 | videoCaption: { | ||
276 | file: { | ||
277 | size: { | ||
278 | max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max | ||
279 | }, | ||
280 | extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME | ||
290 | } | 281 | } |
282 | }, | ||
283 | user: { | ||
284 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
285 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | ||
286 | }, | ||
287 | trending: { | ||
288 | videos: { | ||
289 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS | ||
290 | } | ||
291 | }, | ||
292 | tracker: { | ||
293 | enabled: CONFIG.TRACKER.ENABLED | ||
291 | } | 294 | } |
292 | } | 295 | } |
293 | } as HttpNodeinfoDiasporaSoftwareNsSchema20 | 296 | } |
294 | res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"') | 297 | } as HttpNodeinfoDiasporaSoftwareNsSchema20 |
295 | } else { | ||
296 | json = { error: 'Nodeinfo schema version not handled' } | ||
297 | res.status(HttpStatusCode.NOT_FOUND_404) | ||
298 | } | ||
299 | 298 | ||
300 | return res.send(json).end() | 299 | res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"') |
300 | .send(json) | ||
301 | .end() | ||
301 | } | 302 | } |
302 | 303 | ||
303 | function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { | 304 | function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { |
diff --git a/server/helpers/actor.ts b/server/helpers/actor.ts deleted file mode 100644 index a60d3ed5d..000000000 --- a/server/helpers/actor.ts +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | |||
2 | import { ActorModel } from '../models/activitypub/actor' | ||
3 | import { MActorAccountChannelId, MActorFull } from '../types/models' | ||
4 | |||
5 | type ActorFetchByUrlType = 'all' | 'association-ids' | ||
6 | |||
7 | function fetchActorByUrl (url: string, fetchType: ActorFetchByUrlType): Promise<MActorFull | MActorAccountChannelId> { | ||
8 | if (fetchType === 'all') return ActorModel.loadByUrlAndPopulateAccountAndChannel(url) | ||
9 | |||
10 | if (fetchType === 'association-ids') return ActorModel.loadByUrl(url) | ||
11 | } | ||
12 | |||
13 | export { | ||
14 | ActorFetchByUrlType, | ||
15 | fetchActorByUrl | ||
16 | } | ||
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index 6aae5e821..884bd187d 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts | |||
@@ -7,7 +7,7 @@ import * as winston from 'winston' | |||
7 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' | 7 | import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' |
8 | import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared' | 8 | import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared' |
9 | import { CustomConfig } from '../../shared/models/server/custom-config.model' | 9 | import { CustomConfig } from '../../shared/models/server/custom-config.model' |
10 | import { VideoComment } from '../../shared/models/videos/video-comment.model' | 10 | import { VideoComment } from '../../shared/models/videos/comment/video-comment.model' |
11 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
12 | import { jsonLoggerFormat, labelFormatter } from './logger' | 12 | import { jsonLoggerFormat, labelFormatter } from './logger' |
13 | 13 | ||
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index b93868c12..9abc532d2 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -8,7 +8,7 @@ | |||
8 | import { exec, ExecOptions } from 'child_process' | 8 | import { exec, ExecOptions } from 'child_process' |
9 | import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto' | 9 | import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto' |
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import { basename, isAbsolute, join, resolve } from 'path' | 11 | import { basename, extname, isAbsolute, join, resolve } from 'path' |
12 | import * as pem from 'pem' | 12 | import * as pem from 'pem' |
13 | import { pipeline } from 'stream' | 13 | import { pipeline } from 'stream' |
14 | import { URL } from 'url' | 14 | import { URL } from 'url' |
@@ -32,6 +32,18 @@ const objectConverter = (oldObject: any, keyConverter: (e: string) => string, va | |||
32 | return newObject | 32 | return newObject |
33 | } | 33 | } |
34 | 34 | ||
35 | function mapToJSON (map: Map<any, any>) { | ||
36 | const obj: any = {} | ||
37 | |||
38 | for (const [ k, v ] of map) { | ||
39 | obj[k] = v | ||
40 | } | ||
41 | |||
42 | return obj | ||
43 | } | ||
44 | |||
45 | // --------------------------------------------------------------------------- | ||
46 | |||
35 | const timeTable = { | 47 | const timeTable = { |
36 | ms: 1, | 48 | ms: 1, |
37 | second: 1000, | 49 | second: 1000, |
@@ -110,6 +122,8 @@ export function parseBytes (value: string | number): number { | |||
110 | } | 122 | } |
111 | } | 123 | } |
112 | 124 | ||
125 | // --------------------------------------------------------------------------- | ||
126 | |||
113 | function sanitizeUrl (url: string) { | 127 | function sanitizeUrl (url: string) { |
114 | const urlObject = new URL(url) | 128 | const urlObject = new URL(url) |
115 | 129 | ||
@@ -129,6 +143,8 @@ function sanitizeHost (host: string, remoteScheme: string) { | |||
129 | return host.replace(new RegExp(`:${toRemove}$`), '') | 143 | return host.replace(new RegExp(`:${toRemove}$`), '') |
130 | } | 144 | } |
131 | 145 | ||
146 | // --------------------------------------------------------------------------- | ||
147 | |||
132 | function isTestInstance () { | 148 | function isTestInstance () { |
133 | return process.env.NODE_ENV === 'test' | 149 | return process.env.NODE_ENV === 'test' |
134 | } | 150 | } |
@@ -141,6 +157,8 @@ function getAppNumber () { | |||
141 | return process.env.NODE_APP_INSTANCE | 157 | return process.env.NODE_APP_INSTANCE |
142 | } | 158 | } |
143 | 159 | ||
160 | // --------------------------------------------------------------------------- | ||
161 | |||
144 | let rootPath: string | 162 | let rootPath: string |
145 | 163 | ||
146 | function root () { | 164 | function root () { |
@@ -154,27 +172,19 @@ function root () { | |||
154 | return rootPath | 172 | return rootPath |
155 | } | 173 | } |
156 | 174 | ||
157 | function pageToStartAndCount (page: number, itemsPerPage: number) { | 175 | function buildPath (path: string) { |
158 | const start = (page - 1) * itemsPerPage | 176 | if (isAbsolute(path)) return path |
159 | 177 | ||
160 | return { start, count: itemsPerPage } | 178 | return join(root(), path) |
161 | } | 179 | } |
162 | 180 | ||
163 | function mapToJSON (map: Map<any, any>) { | 181 | function getLowercaseExtension (filename: string) { |
164 | const obj: any = {} | 182 | const ext = extname(filename) || '' |
165 | 183 | ||
166 | for (const [ k, v ] of map) { | 184 | return ext.toLowerCase() |
167 | obj[k] = v | ||
168 | } | ||
169 | |||
170 | return obj | ||
171 | } | 185 | } |
172 | 186 | ||
173 | function buildPath (path: string) { | 187 | // --------------------------------------------------------------------------- |
174 | if (isAbsolute(path)) return path | ||
175 | |||
176 | return join(root(), path) | ||
177 | } | ||
178 | 188 | ||
179 | // Consistent with .length, lodash truncate function is not | 189 | // Consistent with .length, lodash truncate function is not |
180 | function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) { | 190 | function peertubeTruncate (str: string, options: { length: number, separator?: RegExp, omission?: string }) { |
@@ -189,6 +199,27 @@ function peertubeTruncate (str: string, options: { length: number, separator?: R | |||
189 | return truncate(str, options) | 199 | return truncate(str, options) |
190 | } | 200 | } |
191 | 201 | ||
202 | function pageToStartAndCount (page: number, itemsPerPage: number) { | ||
203 | const start = (page - 1) * itemsPerPage | ||
204 | |||
205 | return { start, count: itemsPerPage } | ||
206 | } | ||
207 | |||
208 | // --------------------------------------------------------------------------- | ||
209 | |||
210 | type SemVersion = { major: number, minor: number, patch: number } | ||
211 | function parseSemVersion (s: string) { | ||
212 | const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i) | ||
213 | |||
214 | return { | ||
215 | major: parseInt(parsed[1]), | ||
216 | minor: parseInt(parsed[2]), | ||
217 | patch: parseInt(parsed[3]) | ||
218 | } as SemVersion | ||
219 | } | ||
220 | |||
221 | // --------------------------------------------------------------------------- | ||
222 | |||
192 | function sha256 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') { | 223 | function sha256 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') { |
193 | return createHash('sha256').update(str).digest(encoding) | 224 | return createHash('sha256').update(str).digest(encoding) |
194 | } | 225 | } |
@@ -197,6 +228,8 @@ function sha1 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') { | |||
197 | return createHash('sha1').update(str).digest(encoding) | 228 | return createHash('sha1').update(str).digest(encoding) |
198 | } | 229 | } |
199 | 230 | ||
231 | // --------------------------------------------------------------------------- | ||
232 | |||
200 | function execShell (command: string, options?: ExecOptions) { | 233 | function execShell (command: string, options?: ExecOptions) { |
201 | return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => { | 234 | return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => { |
202 | exec(command, options, (err, stdout, stderr) => { | 235 | exec(command, options, (err, stdout, stderr) => { |
@@ -208,6 +241,20 @@ function execShell (command: string, options?: ExecOptions) { | |||
208 | }) | 241 | }) |
209 | } | 242 | } |
210 | 243 | ||
244 | // --------------------------------------------------------------------------- | ||
245 | |||
246 | function isOdd (num: number) { | ||
247 | return (num % 2) !== 0 | ||
248 | } | ||
249 | |||
250 | function toEven (num: number) { | ||
251 | if (isOdd(num)) return num + 1 | ||
252 | |||
253 | return num | ||
254 | } | ||
255 | |||
256 | // --------------------------------------------------------------------------- | ||
257 | |||
211 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | 258 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { |
212 | return function promisified (): Promise<A> { | 259 | return function promisified (): Promise<A> { |
213 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | 260 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { |
@@ -233,17 +280,6 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) | |||
233 | } | 280 | } |
234 | } | 281 | } |
235 | 282 | ||
236 | type SemVersion = { major: number, minor: number, patch: number } | ||
237 | function parseSemVersion (s: string) { | ||
238 | const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i) | ||
239 | |||
240 | return { | ||
241 | major: parseInt(parsed[1]), | ||
242 | minor: parseInt(parsed[2]), | ||
243 | patch: parseInt(parsed[3]) | ||
244 | } as SemVersion | ||
245 | } | ||
246 | |||
247 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) | 283 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) |
248 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) | 284 | const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) |
249 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) | 285 | const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) |
@@ -259,17 +295,21 @@ export { | |||
259 | getAppNumber, | 295 | getAppNumber, |
260 | 296 | ||
261 | objectConverter, | 297 | objectConverter, |
298 | mapToJSON, | ||
299 | |||
262 | root, | 300 | root, |
263 | pageToStartAndCount, | 301 | buildPath, |
302 | getLowercaseExtension, | ||
264 | sanitizeUrl, | 303 | sanitizeUrl, |
265 | sanitizeHost, | 304 | sanitizeHost, |
266 | buildPath, | 305 | |
267 | execShell, | 306 | execShell, |
307 | |||
308 | pageToStartAndCount, | ||
268 | peertubeTruncate, | 309 | peertubeTruncate, |
269 | 310 | ||
270 | sha256, | 311 | sha256, |
271 | sha1, | 312 | sha1, |
272 | mapToJSON, | ||
273 | 313 | ||
274 | promisify0, | 314 | promisify0, |
275 | promisify1, | 315 | promisify1, |
@@ -282,5 +322,8 @@ export { | |||
282 | execPromise, | 322 | execPromise, |
283 | pipelinePromise, | 323 | pipelinePromise, |
284 | 324 | ||
285 | parseSemVersion | 325 | parseSemVersion, |
326 | |||
327 | isOdd, | ||
328 | toEven | ||
286 | } | 329 | } |
diff --git a/server/helpers/custom-validators/activitypub/playlist.ts b/server/helpers/custom-validators/activitypub/playlist.ts index bd0d16a4a..72c5b80e9 100644 --- a/server/helpers/custom-validators/activitypub/playlist.ts +++ b/server/helpers/custom-validators/activitypub/playlist.ts | |||
@@ -1,13 +1,16 @@ | |||
1 | import { exists, isDateValid } from '../misc' | ||
2 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
3 | import validator from 'validator' | 1 | import validator from 'validator' |
4 | import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object' | 2 | import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object' |
3 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
4 | import { exists, isDateValid, isUUIDValid } from '../misc' | ||
5 | import { isVideoPlaylistNameValid } from '../video-playlists' | ||
5 | import { isActivityPubUrlValid } from './misc' | 6 | import { isActivityPubUrlValid } from './misc' |
6 | 7 | ||
7 | function isPlaylistObjectValid (object: PlaylistObject) { | 8 | function isPlaylistObjectValid (object: PlaylistObject) { |
8 | return exists(object) && | 9 | return exists(object) && |
9 | object.type === 'Playlist' && | 10 | object.type === 'Playlist' && |
10 | validator.isInt(object.totalItems + '') && | 11 | validator.isInt(object.totalItems + '') && |
12 | isVideoPlaylistNameValid(object.name) && | ||
13 | isUUIDValid(object.uuid) && | ||
11 | isDateValid(object.published) && | 14 | isDateValid(object.published) && |
12 | isDateValid(object.updated) | 15 | isDateValid(object.updated) |
13 | } | 16 | } |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index fd3b45804..528bfcfb8 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -2,6 +2,7 @@ import 'multer' | |||
2 | import { UploadFilesForCheck } from 'express' | 2 | import { UploadFilesForCheck } from 'express' |
3 | import { sep } from 'path' | 3 | import { sep } from 'path' |
4 | import validator from 'validator' | 4 | import validator from 'validator' |
5 | import { isShortUUID, shortToUUID } from '../uuid' | ||
5 | 6 | ||
6 | function exists (value: any) { | 7 | function exists (value: any) { |
7 | return value !== undefined && value !== null | 8 | return value !== undefined && value !== null |
@@ -14,7 +15,7 @@ function isSafePath (p: string) { | |||
14 | }) | 15 | }) |
15 | } | 16 | } |
16 | 17 | ||
17 | function isArray (value: any) { | 18 | function isArray (value: any): value is any[] { |
18 | return Array.isArray(value) | 19 | return Array.isArray(value) |
19 | } | 20 | } |
20 | 21 | ||
@@ -50,42 +51,7 @@ function isIntOrNull (value: any) { | |||
50 | return value === null || validator.isInt('' + value) | 51 | return value === null || validator.isInt('' + value) |
51 | } | 52 | } |
52 | 53 | ||
53 | function toIntOrNull (value: string) { | 54 | // --------------------------------------------------------------------------- |
54 | const v = toValueOrNull(value) | ||
55 | |||
56 | if (v === null || v === undefined) return v | ||
57 | if (typeof v === 'number') return v | ||
58 | |||
59 | return validator.toInt('' + v) | ||
60 | } | ||
61 | |||
62 | function toBooleanOrNull (value: any) { | ||
63 | const v = toValueOrNull(value) | ||
64 | |||
65 | if (v === null || v === undefined) return v | ||
66 | if (typeof v === 'boolean') return v | ||
67 | |||
68 | return validator.toBoolean('' + v) | ||
69 | } | ||
70 | |||
71 | function toValueOrNull (value: string) { | ||
72 | if (value === 'null') return null | ||
73 | |||
74 | return value | ||
75 | } | ||
76 | |||
77 | function toArray (value: any) { | ||
78 | if (value && isArray(value) === false) return [ value ] | ||
79 | |||
80 | return value | ||
81 | } | ||
82 | |||
83 | function toIntArray (value: any) { | ||
84 | if (!value) return [] | ||
85 | if (isArray(value) === false) return [ validator.toInt(value) ] | ||
86 | |||
87 | return value.map(v => validator.toInt(v)) | ||
88 | } | ||
89 | 55 | ||
90 | function isFileFieldValid ( | 56 | function isFileFieldValid ( |
91 | files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], | 57 | files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], |
@@ -160,6 +126,51 @@ function isFileValid ( | |||
160 | 126 | ||
161 | // --------------------------------------------------------------------------- | 127 | // --------------------------------------------------------------------------- |
162 | 128 | ||
129 | function toCompleteUUID (value: string) { | ||
130 | if (isShortUUID(value)) return shortToUUID(value) | ||
131 | |||
132 | return value | ||
133 | } | ||
134 | |||
135 | function toIntOrNull (value: string) { | ||
136 | const v = toValueOrNull(value) | ||
137 | |||
138 | if (v === null || v === undefined) return v | ||
139 | if (typeof v === 'number') return v | ||
140 | |||
141 | return validator.toInt('' + v) | ||
142 | } | ||
143 | |||
144 | function toBooleanOrNull (value: any) { | ||
145 | const v = toValueOrNull(value) | ||
146 | |||
147 | if (v === null || v === undefined) return v | ||
148 | if (typeof v === 'boolean') return v | ||
149 | |||
150 | return validator.toBoolean('' + v) | ||
151 | } | ||
152 | |||
153 | function toValueOrNull (value: string) { | ||
154 | if (value === 'null') return null | ||
155 | |||
156 | return value | ||
157 | } | ||
158 | |||
159 | function toArray (value: any) { | ||
160 | if (value && isArray(value) === false) return [ value ] | ||
161 | |||
162 | return value | ||
163 | } | ||
164 | |||
165 | function toIntArray (value: any) { | ||
166 | if (!value) return [] | ||
167 | if (isArray(value) === false) return [ validator.toInt(value) ] | ||
168 | |||
169 | return value.map(v => validator.toInt(v)) | ||
170 | } | ||
171 | |||
172 | // --------------------------------------------------------------------------- | ||
173 | |||
163 | export { | 174 | export { |
164 | exists, | 175 | exists, |
165 | isArrayOf, | 176 | isArrayOf, |
@@ -169,6 +180,7 @@ export { | |||
169 | isIdValid, | 180 | isIdValid, |
170 | isSafePath, | 181 | isSafePath, |
171 | isUUIDValid, | 182 | isUUIDValid, |
183 | toCompleteUUID, | ||
172 | isIdOrUUIDValid, | 184 | isIdOrUUIDValid, |
173 | isDateValid, | 185 | isDateValid, |
174 | toValueOrNull, | 186 | toValueOrNull, |
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts index 8d3ce580e..94bdf237a 100644 --- a/server/helpers/custom-validators/video-comments.ts +++ b/server/helpers/custom-validators/video-comments.ts | |||
@@ -1,9 +1,5 @@ | |||
1 | import * as express from 'express' | ||
2 | import validator from 'validator' | 1 | import validator from 'validator' |
3 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 2 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
5 | import { MVideoId } from '@server/types/models' | ||
6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
7 | 3 | ||
8 | const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS | 4 | const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS |
9 | 5 | ||
@@ -11,83 +7,8 @@ function isValidVideoCommentText (value: string) { | |||
11 | return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) | 7 | return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) |
12 | } | 8 | } |
13 | 9 | ||
14 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
15 | const id = parseInt(idArg + '', 10) | ||
16 | const videoComment = await VideoCommentModel.loadById(id) | ||
17 | |||
18 | if (!videoComment) { | ||
19 | res.status(HttpStatusCode.NOT_FOUND_404) | ||
20 | .json({ error: 'Video comment thread not found' }) | ||
21 | .end() | ||
22 | |||
23 | return false | ||
24 | } | ||
25 | |||
26 | if (videoComment.videoId !== video.id) { | ||
27 | res.status(HttpStatusCode.BAD_REQUEST_400) | ||
28 | .json({ error: 'Video comment is not associated to this video.' }) | ||
29 | .end() | ||
30 | |||
31 | return false | ||
32 | } | ||
33 | |||
34 | if (videoComment.inReplyToCommentId !== null) { | ||
35 | res.status(HttpStatusCode.BAD_REQUEST_400) | ||
36 | .json({ error: 'Video comment is not a thread.' }) | ||
37 | .end() | ||
38 | |||
39 | return false | ||
40 | } | ||
41 | |||
42 | res.locals.videoCommentThread = videoComment | ||
43 | return true | ||
44 | } | ||
45 | |||
46 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
47 | const id = parseInt(idArg + '', 10) | ||
48 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
49 | |||
50 | if (!videoComment) { | ||
51 | res.status(HttpStatusCode.NOT_FOUND_404) | ||
52 | .json({ error: 'Video comment thread not found' }) | ||
53 | .end() | ||
54 | |||
55 | return false | ||
56 | } | ||
57 | |||
58 | if (videoComment.videoId !== video.id) { | ||
59 | res.status(HttpStatusCode.BAD_REQUEST_400) | ||
60 | .json({ error: 'Video comment is not associated to this video.' }) | ||
61 | .end() | ||
62 | |||
63 | return false | ||
64 | } | ||
65 | |||
66 | res.locals.videoCommentFull = videoComment | ||
67 | return true | ||
68 | } | ||
69 | |||
70 | async function doesCommentIdExist (idArg: number | string, res: express.Response) { | ||
71 | const id = parseInt(idArg + '', 10) | ||
72 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
73 | |||
74 | if (!videoComment) { | ||
75 | res.status(HttpStatusCode.NOT_FOUND_404) | ||
76 | .json({ error: 'Video comment thread not found' }) | ||
77 | |||
78 | return false | ||
79 | } | ||
80 | |||
81 | res.locals.videoCommentFull = videoComment | ||
82 | |||
83 | return true | ||
84 | } | ||
85 | |||
86 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
87 | 11 | ||
88 | export { | 12 | export { |
89 | isValidVideoCommentText, | 13 | isValidVideoCommentText |
90 | doesVideoCommentThreadExist, | ||
91 | doesVideoCommentExist, | ||
92 | doesCommentIdExist | ||
93 | } | 14 | } |
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts index 0063d3337..dbf6a3504 100644 --- a/server/helpers/custom-validators/video-imports.ts +++ b/server/helpers/custom-validators/video-imports.ts | |||
@@ -2,9 +2,6 @@ import 'multer' | |||
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers/constants' | 3 | import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers/constants' |
4 | import { exists, isFileValid } from './misc' | 4 | import { exists, isFileValid } from './misc' |
5 | import * as express from 'express' | ||
6 | import { VideoImportModel } from '../../models/video/video-import' | ||
7 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
8 | 5 | ||
9 | function isVideoImportTargetUrlValid (url: string) { | 6 | function isVideoImportTargetUrlValid (url: string) { |
10 | const isURLOptions = { | 7 | const isURLOptions = { |
@@ -32,26 +29,10 @@ function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multe | |||
32 | return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) | 29 | return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) |
33 | } | 30 | } |
34 | 31 | ||
35 | async function doesVideoImportExist (id: number, res: express.Response) { | ||
36 | const videoImport = await VideoImportModel.loadAndPopulateVideo(id) | ||
37 | |||
38 | if (!videoImport) { | ||
39 | res.status(HttpStatusCode.NOT_FOUND_404) | ||
40 | .json({ error: 'Video import not found' }) | ||
41 | .end() | ||
42 | |||
43 | return false | ||
44 | } | ||
45 | |||
46 | res.locals.videoImport = videoImport | ||
47 | return true | ||
48 | } | ||
49 | |||
50 | // --------------------------------------------------------------------------- | 32 | // --------------------------------------------------------------------------- |
51 | 33 | ||
52 | export { | 34 | export { |
53 | isVideoImportStateValid, | 35 | isVideoImportStateValid, |
54 | isVideoImportTargetUrlValid, | 36 | isVideoImportTargetUrlValid, |
55 | doesVideoImportExist, | ||
56 | isVideoImportTorrentFile | 37 | isVideoImportTorrentFile |
57 | } | 38 | } |
diff --git a/server/helpers/custom-validators/video-ownership.ts b/server/helpers/custom-validators/video-ownership.ts index ee3cebe10..0e1c63bad 100644 --- a/server/helpers/custom-validators/video-ownership.ts +++ b/server/helpers/custom-validators/video-ownership.ts | |||
@@ -1,32 +1,20 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' | ||
3 | import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership' | ||
4 | import { MUserId } from '@server/types/models' | 2 | import { MUserId } from '@server/types/models' |
3 | import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership' | ||
5 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
6 | 5 | ||
7 | export async function doesChangeVideoOwnershipExist (idArg: number | string, res: Response) { | 6 | function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) { |
8 | const id = parseInt(idArg + '', 10) | ||
9 | const videoChangeOwnership = await VideoChangeOwnershipModel.load(id) | ||
10 | |||
11 | if (!videoChangeOwnership) { | ||
12 | res.status(HttpStatusCode.NOT_FOUND_404) | ||
13 | .json({ error: 'Video change ownership not found' }) | ||
14 | .end() | ||
15 | |||
16 | return false | ||
17 | } | ||
18 | |||
19 | res.locals.videoChangeOwnership = videoChangeOwnership | ||
20 | return true | ||
21 | } | ||
22 | |||
23 | export function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) { | ||
24 | if (videoChangeOwnership.NextOwner.userId === user.id) { | 7 | if (videoChangeOwnership.NextOwner.userId === user.id) { |
25 | return true | 8 | return true |
26 | } | 9 | } |
27 | 10 | ||
28 | res.status(HttpStatusCode.FORBIDDEN_403) | 11 | res.fail({ |
29 | .json({ error: 'Cannot terminate an ownership change of another user' }) | 12 | status: HttpStatusCode.FORBIDDEN_403, |
30 | .end() | 13 | message: 'Cannot terminate an ownership change of another user' |
14 | }) | ||
31 | return false | 15 | return false |
32 | } | 16 | } |
17 | |||
18 | export { | ||
19 | checkUserCanTerminateOwnershipChange | ||
20 | } | ||
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts index f9cb33aca..b5dc70c17 100644 --- a/server/helpers/database-utils.ts +++ b/server/helpers/database-utils.ts | |||
@@ -58,7 +58,7 @@ function transactionRetryer <T> (func: (err: any, data: T) => any) { | |||
58 | 58 | ||
59 | errorFilter: err => { | 59 | errorFilter: err => { |
60 | const willRetry = (err.name === 'SequelizeDatabaseError') | 60 | const willRetry = (err.name === 'SequelizeDatabaseError') |
61 | logger.debug('Maybe retrying the transaction function.', { willRetry, err }) | 61 | logger.debug('Maybe retrying the transaction function.', { willRetry, err, tags: [ 'sql', 'retry' ] }) |
62 | return willRetry | 62 | return willRetry |
63 | } | 63 | } |
64 | }, | 64 | }, |
@@ -68,7 +68,9 @@ function transactionRetryer <T> (func: (err: any, data: T) => any) { | |||
68 | }) | 68 | }) |
69 | } | 69 | } |
70 | 70 | ||
71 | function updateInstanceWithAnother <T extends Model<T>> (instanceToUpdate: Model<T>, baseInstance: Model<T>) { | 71 | // --------------------------------------------------------------------------- |
72 | |||
73 | function updateInstanceWithAnother <M, T extends U, U extends Model<M>> (instanceToUpdate: T, baseInstance: U) { | ||
72 | const obj = baseInstance.toJSON() | 74 | const obj = baseInstance.toJSON() |
73 | 75 | ||
74 | for (const key of Object.keys(obj)) { | 76 | for (const key of Object.keys(obj)) { |
@@ -82,13 +84,7 @@ function resetSequelizeInstance (instance: Model<any>, savedFields: object) { | |||
82 | }) | 84 | }) |
83 | } | 85 | } |
84 | 86 | ||
85 | function afterCommitIfTransaction (t: Transaction, fn: Function) { | 87 | function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Pick<Model, 'destroy'>> ( |
86 | if (t) return t.afterCommit(() => fn()) | ||
87 | |||
88 | return fn() | ||
89 | } | ||
90 | |||
91 | function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Model<T>> ( | ||
92 | fromDatabase: T[], | 88 | fromDatabase: T[], |
93 | newModels: T[], | 89 | newModels: T[], |
94 | t: Transaction | 90 | t: Transaction |
@@ -111,6 +107,20 @@ function setAsUpdated (table: string, id: number, transaction?: Transaction) { | |||
111 | 107 | ||
112 | // --------------------------------------------------------------------------- | 108 | // --------------------------------------------------------------------------- |
113 | 109 | ||
110 | function runInReadCommittedTransaction <T> (fn: (t: Transaction) => Promise<T>) { | ||
111 | const options = { isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED } | ||
112 | |||
113 | return sequelizeTypescript.transaction(options, t => fn(t)) | ||
114 | } | ||
115 | |||
116 | function afterCommitIfTransaction (t: Transaction, fn: Function) { | ||
117 | if (t) return t.afterCommit(() => fn()) | ||
118 | |||
119 | return fn() | ||
120 | } | ||
121 | |||
122 | // --------------------------------------------------------------------------- | ||
123 | |||
114 | export { | 124 | export { |
115 | resetSequelizeInstance, | 125 | resetSequelizeInstance, |
116 | retryTransactionWrapper, | 126 | retryTransactionWrapper, |
@@ -118,5 +128,6 @@ export { | |||
118 | updateInstanceWithAnother, | 128 | updateInstanceWithAnother, |
119 | afterCommitIfTransaction, | 129 | afterCommitIfTransaction, |
120 | deleteNonExistingModels, | 130 | deleteNonExistingModels, |
121 | setAsUpdated | 131 | setAsUpdated, |
132 | runInReadCommittedTransaction | ||
122 | } | 133 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index ede22a3cc..0ff113274 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
4 | import { CONFIG } from '../initializers/config' | ||
3 | import { REMOTE_SCHEME } from '../initializers/constants' | 5 | import { REMOTE_SCHEME } from '../initializers/constants' |
6 | import { getLowercaseExtension } from './core-utils' | ||
7 | import { isArray } from './custom-validators/misc' | ||
4 | import { logger } from './logger' | 8 | import { logger } from './logger' |
5 | import { deleteFileAndCatch, generateRandomString } from './utils' | 9 | import { deleteFileAndCatch, generateRandomString } from './utils' |
6 | import { extname } from 'path' | ||
7 | import { isArray } from './custom-validators/misc' | ||
8 | import { CONFIG } from '../initializers/config' | ||
9 | import { getExtFromMimetype } from './video' | 10 | import { getExtFromMimetype } from './video' |
10 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
11 | 11 | ||
12 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { | 12 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { |
13 | if (paramNSFW === 'true') return true | 13 | if (paramNSFW === 'true') return true |
@@ -30,21 +30,19 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { | |||
30 | return null | 30 | return null |
31 | } | 31 | } |
32 | 32 | ||
33 | function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] }) { | 33 | function cleanUpReqFiles (req: express.Request) { |
34 | const files = req.files | 34 | const filesObject = req.files |
35 | 35 | if (!filesObject) return | |
36 | if (!files) return | ||
37 | 36 | ||
38 | if (isArray(files)) { | 37 | if (isArray(filesObject)) { |
39 | (files as Express.Multer.File[]).forEach(f => deleteFileAndCatch(f.path)) | 38 | filesObject.forEach(f => deleteFileAndCatch(f.path)) |
40 | return | 39 | return |
41 | } | 40 | } |
42 | 41 | ||
43 | for (const key of Object.keys(files)) { | 42 | for (const key of Object.keys(filesObject)) { |
44 | const file = files[key] | 43 | const files = filesObject[key] |
45 | 44 | ||
46 | if (isArray(file)) file.forEach(f => deleteFileAndCatch(f.path)) | 45 | files.forEach(f => deleteFileAndCatch(f.path)) |
47 | else deleteFileAndCatch(file.path) | ||
48 | } | 46 | } |
49 | } | 47 | } |
50 | 48 | ||
@@ -79,7 +77,7 @@ function createReqFiles ( | |||
79 | 77 | ||
80 | filename: async (req, file, cb) => { | 78 | filename: async (req, file, cb) => { |
81 | let extension: string | 79 | let extension: string |
82 | const fileExtension = extname(file.originalname) | 80 | const fileExtension = getLowercaseExtension(file.originalname) |
83 | const extensionFromMimetype = getExtFromMimetype(mimeTypes, file.mimetype) | 81 | const extensionFromMimetype = getExtFromMimetype(mimeTypes, file.mimetype) |
84 | 82 | ||
85 | // Take the file extension if we don't understand the mime type | 83 | // Take the file extension if we don't understand the mime type |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index e328c49ac..6f5a71b4a 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -3,13 +3,12 @@ import * as ffmpeg from 'fluent-ffmpeg' | |||
3 | import { readFile, remove, writeFile } from 'fs-extra' | 3 | import { readFile, remove, writeFile } from 'fs-extra' |
4 | import { dirname, join } from 'path' | 4 | import { dirname, join } from 'path' |
5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' |
6 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptions, EncoderProfile, VideoResolution } from '../../shared/models/videos' | 6 | import { AvailableEncoders, EncoderOptions, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' |
7 | import { CONFIG } from '../initializers/config' | 7 | import { CONFIG } from '../initializers/config' |
8 | import { execPromise, promisify0 } from './core-utils' | 8 | import { execPromise, promisify0 } from './core-utils' |
9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' | 9 | import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' |
10 | import { processImage } from './image-utils' | 10 | import { processImage } from './image-utils' |
11 | import { logger } from './logger' | 11 | import { logger } from './logger' |
12 | import { FilterSpecification } from 'fluent-ffmpeg' | ||
13 | 12 | ||
14 | /** | 13 | /** |
15 | * | 14 | * |
@@ -133,7 +132,7 @@ interface BaseTranscodeOptions { | |||
133 | availableEncoders: AvailableEncoders | 132 | availableEncoders: AvailableEncoders |
134 | profile: string | 133 | profile: string |
135 | 134 | ||
136 | resolution: VideoResolution | 135 | resolution: number |
137 | 136 | ||
138 | isPortraitMode?: boolean | 137 | isPortraitMode?: boolean |
139 | 138 | ||
@@ -227,7 +226,7 @@ async function getLiveTranscodingCommand (options: { | |||
227 | 226 | ||
228 | const varStreamMap: string[] = [] | 227 | const varStreamMap: string[] = [] |
229 | 228 | ||
230 | const complexFilter: FilterSpecification[] = [ | 229 | const complexFilter: ffmpeg.FilterSpecification[] = [ |
231 | { | 230 | { |
232 | inputs: '[v:0]', | 231 | inputs: '[v:0]', |
233 | filter: 'split', | 232 | filter: 'split', |
@@ -407,8 +406,7 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran | |||
407 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { | 406 | async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { |
408 | command = command.loop(undefined) | 407 | command = command.loop(undefined) |
409 | 408 | ||
410 | // Avoid "height not divisible by 2" error | 409 | const scaleFilterValue = getScaleCleanerValue() |
411 | const scaleFilterValue = 'trunc(iw/2)*2:trunc(ih/2)*2' | ||
412 | command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue }) | 410 | command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue }) |
413 | 411 | ||
414 | command.outputOption('-preset:v veryfast') | 412 | command.outputOption('-preset:v veryfast') |
@@ -542,7 +540,7 @@ async function getEncoderBuilderResult (options: { | |||
542 | } | 540 | } |
543 | } | 541 | } |
544 | 542 | ||
545 | const result = await builder({ input, resolution: resolution, fps, streamNum }) | 543 | const result = await builder({ input, resolution, fps, streamNum }) |
546 | 544 | ||
547 | return { | 545 | return { |
548 | result, | 546 | result, |
@@ -727,6 +725,11 @@ async function runCommand (options: { | |||
727 | }) | 725 | }) |
728 | } | 726 | } |
729 | 727 | ||
728 | // Avoid "height not divisible by 2" error | ||
729 | function getScaleCleanerValue () { | ||
730 | return 'trunc(iw/2)*2:trunc(ih/2)*2' | ||
731 | } | ||
732 | |||
730 | // --------------------------------------------------------------------------- | 733 | // --------------------------------------------------------------------------- |
731 | 734 | ||
732 | export { | 735 | export { |
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index 40eaafd57..ef2aa3f89 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/server/helpers/ffprobe-utils.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' | 2 | import { getMaxBitrate, VideoFileMetadata, VideoResolution } from '../../shared/models/videos' |
3 | import { getMaxBitrate, VideoResolution } from '../../shared/models/videos' | ||
4 | import { CONFIG } from '../initializers/config' | 3 | import { CONFIG } from '../initializers/config' |
5 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 4 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
6 | import { logger } from './logger' | 5 | import { logger } from './logger' |
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 6f6f8d4da..c76ed545b 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { copy, readFile, remove, rename } from 'fs-extra' | 1 | import { copy, readFile, remove, rename } from 'fs-extra' |
2 | import * as Jimp from 'jimp' | 2 | import * as Jimp from 'jimp' |
3 | import { extname } from 'path' | 3 | import { getLowercaseExtension } from './core-utils' |
4 | import { v4 as uuidv4 } from 'uuid' | ||
5 | import { convertWebPToJPG, processGIF } from './ffmpeg-utils' | 4 | import { convertWebPToJPG, processGIF } from './ffmpeg-utils' |
6 | import { logger } from './logger' | 5 | import { logger } from './logger' |
6 | import { buildUUID } from './uuid' | ||
7 | 7 | ||
8 | function generateImageFilename (extension = '.jpg') { | 8 | function generateImageFilename (extension = '.jpg') { |
9 | return uuidv4() + extension | 9 | return buildUUID() + extension |
10 | } | 10 | } |
11 | 11 | ||
12 | async function processImage ( | 12 | async function processImage ( |
@@ -15,7 +15,7 @@ async function processImage ( | |||
15 | newSize: { width: number, height: number }, | 15 | newSize: { width: number, height: number }, |
16 | keepOriginal = false | 16 | keepOriginal = false |
17 | ) { | 17 | ) { |
18 | const extension = extname(path) | 18 | const extension = getLowercaseExtension(path) |
19 | 19 | ||
20 | if (path === destination) { | 20 | if (path === destination) { |
21 | throw new Error('Jimp/FFmpeg needs an input path different that the output path.') | 21 | throw new Error('Jimp/FFmpeg needs an input path different that the output path.') |
@@ -61,7 +61,8 @@ async function jimpProcessor (path: string, destination: string, newSize: { widt | |||
61 | await remove(destination) | 61 | await remove(destination) |
62 | 62 | ||
63 | // Optimization if the source file has the appropriate size | 63 | // Optimization if the source file has the appropriate size |
64 | if (await skipProcessing({ jimpInstance, newSize, imageBytes: inputBuffer.byteLength, inputExt, outputExt: extname(destination) })) { | 64 | const outputExt = getLowercaseExtension(destination) |
65 | if (skipProcessing({ jimpInstance, newSize, imageBytes: inputBuffer.byteLength, inputExt, outputExt })) { | ||
65 | return copy(path, destination) | 66 | return copy(path, destination) |
66 | } | 67 | } |
67 | 68 | ||
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts index a112fd300..29e06860d 100644 --- a/server/helpers/logger.ts +++ b/server/helpers/logger.ts | |||
@@ -151,7 +151,8 @@ const bunyanLogger = { | |||
151 | fatal: bunyanLogFactory('error') | 151 | fatal: bunyanLogFactory('error') |
152 | } | 152 | } |
153 | 153 | ||
154 | function loggerTagsFactory (...defaultTags: string[]) { | 154 | type LoggerTagsFn = (...tags: string[]) => { tags: string[] } |
155 | function loggerTagsFactory (...defaultTags: string[]): LoggerTagsFn { | ||
155 | return (...tags: string[]) => { | 156 | return (...tags: string[]) => { |
156 | return { tags: defaultTags.concat(tags) } | 157 | return { tags: defaultTags.concat(tags) } |
157 | } | 158 | } |
@@ -160,6 +161,8 @@ function loggerTagsFactory (...defaultTags: string[]) { | |||
160 | // --------------------------------------------------------------------------- | 161 | // --------------------------------------------------------------------------- |
161 | 162 | ||
162 | export { | 163 | export { |
164 | LoggerTagsFn, | ||
165 | |||
163 | buildLogger, | 166 | buildLogger, |
164 | timestampFormatter, | 167 | timestampFormatter, |
165 | labelFormatter, | 168 | labelFormatter, |
diff --git a/server/helpers/markdown.ts b/server/helpers/markdown.ts index 2126bb752..41e57d857 100644 --- a/server/helpers/markdown.ts +++ b/server/helpers/markdown.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' | 1 | import { getSanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils' |
2 | |||
3 | const sanitizeOptions = getSanitizeOptions() | ||
2 | 4 | ||
3 | const sanitizeHtml = require('sanitize-html') | 5 | const sanitizeHtml = require('sanitize-html') |
4 | const markdownItEmoji = require('markdown-it-emoji/light') | 6 | const markdownItEmoji = require('markdown-it-emoji/light') |
@@ -18,7 +20,7 @@ const toSafeHtml = text => { | |||
18 | const html = markdownIt.render(textWithLineFeed) | 20 | const html = markdownIt.render(textWithLineFeed) |
19 | 21 | ||
20 | // Convert to safe Html | 22 | // Convert to safe Html |
21 | return sanitizeHtml(html, SANITIZE_OPTIONS) | 23 | return sanitizeHtml(html, sanitizeOptions) |
22 | } | 24 | } |
23 | 25 | ||
24 | const mdToPlainText = text => { | 26 | const mdToPlainText = text => { |
@@ -28,7 +30,7 @@ const mdToPlainText = text => { | |||
28 | const html = markdownIt.render(text) | 30 | const html = markdownIt.render(text) |
29 | 31 | ||
30 | // Convert to safe Html | 32 | // Convert to safe Html |
31 | const safeHtml = sanitizeHtml(html, SANITIZE_OPTIONS) | 33 | const safeHtml = sanitizeHtml(html, sanitizeOptions) |
32 | 34 | ||
33 | return safeHtml.replace(/<[^>]+>/g, '') | 35 | return safeHtml.replace(/<[^>]+>/g, '') |
34 | .replace(/\n$/, '') | 36 | .replace(/\n$/, '') |
diff --git a/server/helpers/promise-cache.ts b/server/helpers/promise-cache.ts new file mode 100644 index 000000000..07e8a9962 --- /dev/null +++ b/server/helpers/promise-cache.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | export class PromiseCache <A, R> { | ||
2 | private readonly running = new Map<string, Promise<R>>() | ||
3 | |||
4 | constructor ( | ||
5 | private readonly fn: (arg: A) => Promise<R>, | ||
6 | private readonly keyBuilder: (arg: A) => string | ||
7 | ) { | ||
8 | } | ||
9 | |||
10 | run (arg: A) { | ||
11 | const key = this.keyBuilder(arg) | ||
12 | |||
13 | if (this.running.has(key)) return this.running.get(key) | ||
14 | |||
15 | const p = this.fn(arg) | ||
16 | |||
17 | this.running.set(key, p) | ||
18 | |||
19 | return p.finally(() => this.running.delete(key)) | ||
20 | } | ||
21 | } | ||
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index fd2a56f30..36e69458e 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -2,7 +2,7 @@ import { createWriteStream, remove } from 'fs-extra' | |||
2 | import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' | 2 | import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { CONFIG } from '../initializers/config' | 4 | import { CONFIG } from '../initializers/config' |
5 | import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' | 5 | import { ACTIVITY_PUB, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants' |
6 | import { pipelinePromise } from './core-utils' | 6 | import { pipelinePromise } from './core-utils' |
7 | import { processImage } from './image-utils' | 7 | import { processImage } from './image-utils' |
8 | import { logger } from './logger' | 8 | import { logger } from './logger' |
@@ -24,6 +24,7 @@ type PeerTubeRequestOptions = { | |||
24 | key: string | 24 | key: string |
25 | headers: string[] | 25 | headers: string[] |
26 | } | 26 | } |
27 | timeout?: number | ||
27 | jsonResponse?: boolean | 28 | jsonResponse?: boolean |
28 | } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> | 29 | } & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'> |
29 | 30 | ||
@@ -92,6 +93,10 @@ const peertubeGot = got.extend({ | |||
92 | path | 93 | path |
93 | }, httpSignatureOptions) | 94 | }, httpSignatureOptions) |
94 | } | 95 | } |
96 | }, | ||
97 | |||
98 | (options: GotOptions) => { | ||
99 | options.timeout = REQUEST_TIMEOUT | ||
95 | } | 100 | } |
96 | ] | 101 | ] |
97 | } | 102 | } |
@@ -180,8 +185,10 @@ function buildGotOptions (options: PeerTubeRequestOptions) { | |||
180 | 185 | ||
181 | return { | 186 | return { |
182 | method: options.method, | 187 | method: options.method, |
188 | dnsCache: true, | ||
183 | json: options.json, | 189 | json: options.json, |
184 | searchParams: options.searchParams, | 190 | searchParams: options.searchParams, |
191 | timeout: options.timeout ?? REQUEST_TIMEOUT, | ||
185 | headers, | 192 | headers, |
186 | context | 193 | context |
187 | } | 194 | } |
diff --git a/server/helpers/uuid.ts b/server/helpers/uuid.ts new file mode 100644 index 000000000..3eb06c773 --- /dev/null +++ b/server/helpers/uuid.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import * as short from 'short-uuid' | ||
2 | |||
3 | const translator = short() | ||
4 | |||
5 | function buildUUID () { | ||
6 | return short.uuid() | ||
7 | } | ||
8 | |||
9 | function uuidToShort (uuid: string) { | ||
10 | if (!uuid) return uuid | ||
11 | |||
12 | return translator.fromUUID(uuid) | ||
13 | } | ||
14 | |||
15 | function shortToUUID (shortUUID: string) { | ||
16 | if (!shortUUID) return shortUUID | ||
17 | |||
18 | return translator.toUUID(shortUUID) | ||
19 | } | ||
20 | |||
21 | function isShortUUID (value: string) { | ||
22 | if (!value) return false | ||
23 | |||
24 | return value.length === translator.maxLength | ||
25 | } | ||
26 | |||
27 | export { | ||
28 | buildUUID, | ||
29 | uuidToShort, | ||
30 | shortToUUID, | ||
31 | isShortUUID | ||
32 | } | ||
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index 7c510f474..f5f645d3e 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -1,69 +1,10 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { CONFIG } from '@server/initializers/config' | 2 | import { CONFIG } from '@server/initializers/config' |
3 | import { | 3 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models' |
4 | isStreamingPlaylist, | ||
5 | MStreamingPlaylistVideo, | ||
6 | MVideo, | ||
7 | MVideoAccountLightBlacklistAllFiles, | ||
8 | MVideoFullLight, | ||
9 | MVideoIdThumbnail, | ||
10 | MVideoImmutable, | ||
11 | MVideoThumbnail, | ||
12 | MVideoWithRights | ||
13 | } from '@server/types/models' | ||
14 | import { VideoPrivacy, VideoState } from '@shared/models' | 4 | import { VideoPrivacy, VideoState } from '@shared/models' |
15 | import { VideoModel } from '../models/video/video' | ||
16 | |||
17 | type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes' | ||
18 | |||
19 | function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Promise<MVideoFullLight> | ||
20 | function fetchVideo (id: number | string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable> | ||
21 | function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Promise<MVideoThumbnail> | ||
22 | function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Promise<MVideoWithRights> | ||
23 | function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Promise<MVideoIdThumbnail> | ||
24 | function fetchVideo ( | ||
25 | id: number | string, | ||
26 | fetchType: VideoFetchType, | ||
27 | userId?: number | ||
28 | ): Promise<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> | ||
29 | function fetchVideo ( | ||
30 | id: number | string, | ||
31 | fetchType: VideoFetchType, | ||
32 | userId?: number | ||
33 | ): Promise<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> { | ||
34 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) | ||
35 | |||
36 | if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id) | ||
37 | |||
38 | if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) | ||
39 | |||
40 | if (fetchType === 'only-video') return VideoModel.load(id) | ||
41 | |||
42 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) | ||
43 | } | ||
44 | |||
45 | type VideoFetchByUrlType = 'all' | 'only-video' | 'only-immutable-attributes' | ||
46 | |||
47 | function fetchVideoByUrl (url: string, fetchType: 'all'): Promise<MVideoAccountLightBlacklistAllFiles> | ||
48 | function fetchVideoByUrl (url: string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable> | ||
49 | function fetchVideoByUrl (url: string, fetchType: 'only-video'): Promise<MVideoThumbnail> | ||
50 | function fetchVideoByUrl ( | ||
51 | url: string, | ||
52 | fetchType: VideoFetchByUrlType | ||
53 | ): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> | ||
54 | function fetchVideoByUrl ( | ||
55 | url: string, | ||
56 | fetchType: VideoFetchByUrlType | ||
57 | ): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { | ||
58 | if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url) | ||
59 | |||
60 | if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url) | ||
61 | |||
62 | if (fetchType === 'only-video') return VideoModel.loadByUrl(url) | ||
63 | } | ||
64 | 5 | ||
65 | function getVideoWithAttributes (res: Response) { | 6 | function getVideoWithAttributes (res: Response) { |
66 | return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights | 7 | return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo |
67 | } | 8 | } |
68 | 9 | ||
69 | function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { | 10 | function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { |
@@ -100,11 +41,7 @@ function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mim | |||
100 | } | 41 | } |
101 | 42 | ||
102 | export { | 43 | export { |
103 | VideoFetchType, | ||
104 | VideoFetchByUrlType, | ||
105 | fetchVideo, | ||
106 | getVideoWithAttributes, | 44 | getVideoWithAttributes, |
107 | fetchVideoByUrl, | ||
108 | extractVideo, | 45 | extractVideo, |
109 | getExtFromMimetype, | 46 | getExtFromMimetype, |
110 | isStateForFederation, | 47 | isStateForFederation, |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index fac3da6ba..fdd361390 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -6,7 +6,6 @@ import { CONFIG } from '@server/initializers/config' | |||
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoResolution } from '../../shared/models/videos' | 7 | import { VideoResolution } from '../../shared/models/videos' |
8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' | 8 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' |
9 | import { getEnabledResolutions } from '../lib/video-transcoding' | ||
10 | import { peertubeTruncate, pipelinePromise, root } from './core-utils' | 9 | import { peertubeTruncate, pipelinePromise, root } from './core-utils' |
11 | import { isVideoFileExtnameValid } from './custom-validators/videos' | 10 | import { isVideoFileExtnameValid } from './custom-validators/videos' |
12 | import { logger } from './logger' | 11 | import { logger } from './logger' |
@@ -35,361 +34,359 @@ const processOptions = { | |||
35 | maxBuffer: 1024 * 1024 * 10 // 10MB | 34 | maxBuffer: 1024 * 1024 * 10 // 10MB |
36 | } | 35 | } |
37 | 36 | ||
38 | function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { | 37 | class YoutubeDL { |
39 | return new Promise<YoutubeDLInfo>((res, rej) => { | ||
40 | let args = opts || [ '-j', '--flat-playlist' ] | ||
41 | 38 | ||
42 | if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) { | 39 | constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) { |
43 | args.push('--force-ipv4') | ||
44 | } | ||
45 | 40 | ||
46 | args = wrapWithProxyOptions(args) | 41 | } |
47 | args = [ '-f', getYoutubeDLVideoFormat() ].concat(args) | ||
48 | 42 | ||
49 | safeGetYoutubeDL() | 43 | getYoutubeDLInfo (opts?: string[]): Promise<YoutubeDLInfo> { |
50 | .then(youtubeDL => { | 44 | return new Promise<YoutubeDLInfo>((res, rej) => { |
51 | youtubeDL.getInfo(url, args, processOptions, (err, info) => { | 45 | let args = opts || [] |
52 | if (err) return rej(err) | ||
53 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) | ||
54 | 46 | ||
55 | const obj = buildVideoInfo(normalizeObject(info)) | 47 | if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) { |
56 | if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' | 48 | args.push('--force-ipv4') |
49 | } | ||
57 | 50 | ||
58 | return res(obj) | 51 | args = this.wrapWithProxyOptions(args) |
59 | }) | 52 | args = [ '-f', this.getYoutubeDLVideoFormat() ].concat(args) |
60 | }) | ||
61 | .catch(err => rej(err)) | ||
62 | }) | ||
63 | } | ||
64 | 53 | ||
65 | function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> { | 54 | YoutubeDL.safeGetYoutubeDL() |
66 | return new Promise<YoutubeDLSubs>((res, rej) => { | 55 | .then(youtubeDL => { |
67 | const cwd = CONFIG.STORAGE.TMP_DIR | 56 | youtubeDL.getInfo(this.url, args, processOptions, (err, info) => { |
68 | const options = opts || { all: true, format: 'vtt', cwd } | 57 | if (err) return rej(err) |
69 | 58 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) | |
70 | safeGetYoutubeDL() | ||
71 | .then(youtubeDL => { | ||
72 | youtubeDL.getSubs(url, options, (err, files) => { | ||
73 | if (err) return rej(err) | ||
74 | if (!files) return [] | ||
75 | |||
76 | logger.debug('Get subtitles from youtube dl.', { url, files }) | ||
77 | |||
78 | const subtitles = files.reduce((acc, filename) => { | ||
79 | const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) | ||
80 | if (!matched || !matched[1]) return acc | ||
81 | |||
82 | return [ | ||
83 | ...acc, | ||
84 | { | ||
85 | language: matched[1], | ||
86 | path: join(cwd, filename), | ||
87 | filename | ||
88 | } | ||
89 | ] | ||
90 | }, []) | ||
91 | 59 | ||
92 | return res(subtitles) | 60 | const obj = this.buildVideoInfo(this.normalizeObject(info)) |
61 | if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' | ||
62 | |||
63 | return res(obj) | ||
64 | }) | ||
93 | }) | 65 | }) |
94 | }) | 66 | .catch(err => rej(err)) |
95 | .catch(err => rej(err)) | 67 | }) |
96 | }) | 68 | } |
97 | } | ||
98 | 69 | ||
99 | function getYoutubeDLVideoFormat () { | 70 | getYoutubeDLSubs (opts?: object): Promise<YoutubeDLSubs> { |
100 | /** | 71 | return new Promise<YoutubeDLSubs>((res, rej) => { |
101 | * list of format selectors in order or preference | 72 | const cwd = CONFIG.STORAGE.TMP_DIR |
102 | * see https://github.com/ytdl-org/youtube-dl#format-selection | 73 | const options = opts || { all: true, format: 'vtt', cwd } |
103 | * | 74 | |
104 | * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope | 75 | YoutubeDL.safeGetYoutubeDL() |
105 | * of being able to do a "quick-transcode" | 76 | .then(youtubeDL => { |
106 | * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9) | 77 | youtubeDL.getSubs(this.url, options, (err, files) => { |
107 | * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback | 78 | if (err) return rej(err) |
108 | * | 79 | if (!files) return [] |
109 | * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499 | 80 | |
110 | **/ | 81 | logger.debug('Get subtitles from youtube dl.', { url: this.url, files }) |
111 | const enabledResolutions = getEnabledResolutions('vod') | 82 | |
112 | const resolution = enabledResolutions.length === 0 | 83 | const subtitles = files.reduce((acc, filename) => { |
113 | ? VideoResolution.H_720P | 84 | const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) |
114 | : Math.max(...enabledResolutions) | 85 | if (!matched || !matched[1]) return acc |
115 | 86 | ||
116 | return [ | 87 | return [ |
117 | `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1 | 88 | ...acc, |
118 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2 | 89 | { |
119 | `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3 | 90 | language: matched[1], |
120 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`, | 91 | path: join(cwd, filename), |
121 | 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats | 92 | filename |
122 | 'best' // Ultimate fallback | 93 | } |
123 | ].join('/') | 94 | ] |
124 | } | 95 | }, []) |
96 | |||
97 | return res(subtitles) | ||
98 | }) | ||
99 | }) | ||
100 | .catch(err => rej(err)) | ||
101 | }) | ||
102 | } | ||
125 | 103 | ||
126 | function downloadYoutubeDLVideo (url: string, fileExt: string, timeout: number) { | 104 | getYoutubeDLVideoFormat () { |
127 | // Leave empty the extension, youtube-dl will add it | 105 | /** |
128 | const pathWithoutExtension = generateVideoImportTmpPath(url, '') | 106 | * list of format selectors in order or preference |
107 | * see https://github.com/ytdl-org/youtube-dl#format-selection | ||
108 | * | ||
109 | * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope | ||
110 | * of being able to do a "quick-transcode" | ||
111 | * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9) | ||
112 | * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback | ||
113 | * | ||
114 | * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499 | ||
115 | **/ | ||
116 | const resolution = this.enabledResolutions.length === 0 | ||
117 | ? VideoResolution.H_720P | ||
118 | : Math.max(...this.enabledResolutions) | ||
119 | |||
120 | return [ | ||
121 | `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1 | ||
122 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2 | ||
123 | `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3 | ||
124 | `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`, | ||
125 | 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats | ||
126 | 'best' // Ultimate fallback | ||
127 | ].join('/') | ||
128 | } | ||
129 | 129 | ||
130 | let timer | 130 | downloadYoutubeDLVideo (fileExt: string, timeout: number) { |
131 | // Leave empty the extension, youtube-dl will add it | ||
132 | const pathWithoutExtension = generateVideoImportTmpPath(this.url, '') | ||
131 | 133 | ||
132 | logger.info('Importing youtubeDL video %s to %s', url, pathWithoutExtension) | 134 | let timer |
133 | 135 | ||
134 | let options = [ '-f', getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ] | 136 | logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension) |
135 | options = wrapWithProxyOptions(options) | ||
136 | 137 | ||
137 | if (process.env.FFMPEG_PATH) { | 138 | let options = [ '-f', this.getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ] |
138 | options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ]) | 139 | options = this.wrapWithProxyOptions(options) |
139 | } | ||
140 | 140 | ||
141 | logger.debug('YoutubeDL options for %s.', url, { options }) | 141 | if (process.env.FFMPEG_PATH) { |
142 | options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ]) | ||
143 | } | ||
142 | 144 | ||
143 | return new Promise<string>((res, rej) => { | 145 | logger.debug('YoutubeDL options for %s.', this.url, { options }) |
144 | safeGetYoutubeDL() | ||
145 | .then(youtubeDL => { | ||
146 | youtubeDL.exec(url, options, processOptions, async err => { | ||
147 | clearTimeout(timer) | ||
148 | 146 | ||
149 | try { | 147 | return new Promise<string>((res, rej) => { |
150 | // If youtube-dl did not guess an extension for our file, just use .mp4 as default | 148 | YoutubeDL.safeGetYoutubeDL() |
151 | if (await pathExists(pathWithoutExtension)) { | 149 | .then(youtubeDL => { |
152 | await move(pathWithoutExtension, pathWithoutExtension + '.mp4') | 150 | youtubeDL.exec(this.url, options, processOptions, async err => { |
153 | } | 151 | clearTimeout(timer) |
152 | |||
153 | try { | ||
154 | // If youtube-dl did not guess an extension for our file, just use .mp4 as default | ||
155 | if (await pathExists(pathWithoutExtension)) { | ||
156 | await move(pathWithoutExtension, pathWithoutExtension + '.mp4') | ||
157 | } | ||
154 | 158 | ||
155 | const path = await guessVideoPathWithExtension(pathWithoutExtension, fileExt) | 159 | const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) |
156 | 160 | ||
157 | if (err) { | 161 | if (err) { |
158 | remove(path) | 162 | remove(path) |
159 | .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err })) | 163 | .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err })) |
160 | 164 | ||
165 | return rej(err) | ||
166 | } | ||
167 | |||
168 | return res(path) | ||
169 | } catch (err) { | ||
161 | return rej(err) | 170 | return rej(err) |
162 | } | 171 | } |
163 | 172 | }) | |
164 | return res(path) | 173 | |
165 | } catch (err) { | 174 | timer = setTimeout(() => { |
166 | return rej(err) | 175 | const err = new Error('YoutubeDL download timeout.') |
167 | } | 176 | |
177 | this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) | ||
178 | .then(path => remove(path)) | ||
179 | .finally(() => rej(err)) | ||
180 | .catch(err => { | ||
181 | logger.error('Cannot remove file in youtubeDL timeout.', { err }) | ||
182 | return rej(err) | ||
183 | }) | ||
184 | }, timeout) | ||
168 | }) | 185 | }) |
186 | .catch(err => rej(err)) | ||
187 | }) | ||
188 | } | ||
169 | 189 | ||
170 | timer = setTimeout(() => { | 190 | buildOriginallyPublishedAt (obj: any) { |
171 | const err = new Error('YoutubeDL download timeout.') | 191 | let originallyPublishedAt: Date = null |
172 | 192 | ||
173 | guessVideoPathWithExtension(pathWithoutExtension, fileExt) | 193 | const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date) |
174 | .then(path => remove(path)) | 194 | if (uploadDateMatcher) { |
175 | .finally(() => rej(err)) | 195 | originallyPublishedAt = new Date() |
176 | .catch(err => { | 196 | originallyPublishedAt.setHours(0, 0, 0, 0) |
177 | logger.error('Cannot remove file in youtubeDL timeout.', { err }) | ||
178 | return rej(err) | ||
179 | }) | ||
180 | }, timeout) | ||
181 | }) | ||
182 | .catch(err => rej(err)) | ||
183 | }) | ||
184 | } | ||
185 | |||
186 | // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js | ||
187 | // We rewrote it to avoid sync calls | ||
188 | async function updateYoutubeDLBinary () { | ||
189 | logger.info('Updating youtubeDL binary.') | ||
190 | 197 | ||
191 | const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin') | 198 | const year = parseInt(uploadDateMatcher[1], 10) |
192 | const bin = join(binDirectory, 'youtube-dl') | 199 | // Month starts from 0 |
193 | const detailsPath = join(binDirectory, 'details') | 200 | const month = parseInt(uploadDateMatcher[2], 10) - 1 |
194 | const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl' | 201 | const day = parseInt(uploadDateMatcher[3], 10) |
195 | 202 | ||
196 | await ensureDir(binDirectory) | 203 | originallyPublishedAt.setFullYear(year, month, day) |
204 | } | ||
197 | 205 | ||
198 | try { | 206 | return originallyPublishedAt |
199 | const result = await got(url, { followRedirect: false }) | 207 | } |
200 | 208 | ||
201 | if (result.statusCode !== HttpStatusCode.FOUND_302) { | 209 | private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) { |
202 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) | 210 | if (!isVideoFileExtnameValid(sourceExt)) { |
203 | return | 211 | throw new Error('Invalid video extension ' + sourceExt) |
204 | } | 212 | } |
205 | 213 | ||
206 | const newUrl = result.headers.location | 214 | const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ] |
207 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1] | ||
208 | 215 | ||
209 | const downloadFileStream = got.stream(newUrl) | 216 | for (const extension of extensions) { |
210 | const writeStream = createWriteStream(bin, { mode: 493 }) | 217 | const path = tmpPath + extension |
211 | 218 | ||
212 | await pipelinePromise( | 219 | if (await pathExists(path)) return path |
213 | downloadFileStream, | 220 | } |
214 | writeStream | ||
215 | ) | ||
216 | |||
217 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) | ||
218 | await writeFile(detailsPath, details, { encoding: 'utf8' }) | ||
219 | 221 | ||
220 | logger.info('youtube-dl updated to version %s.', newVersion) | 222 | throw new Error('Cannot guess path of ' + tmpPath) |
221 | } catch (err) { | ||
222 | logger.error('Cannot update youtube-dl.', { err }) | ||
223 | } | 223 | } |
224 | } | ||
225 | 224 | ||
226 | async function safeGetYoutubeDL () { | 225 | private normalizeObject (obj: any) { |
227 | let youtubeDL | 226 | const newObj: any = {} |
228 | 227 | ||
229 | try { | 228 | for (const key of Object.keys(obj)) { |
230 | youtubeDL = require('youtube-dl') | 229 | // Deprecated key |
231 | } catch (e) { | 230 | if (key === 'resolution') continue |
232 | // Download binary | ||
233 | await updateYoutubeDLBinary() | ||
234 | youtubeDL = require('youtube-dl') | ||
235 | } | ||
236 | 231 | ||
237 | return youtubeDL | 232 | const value = obj[key] |
238 | } | ||
239 | 233 | ||
240 | function buildOriginallyPublishedAt (obj: any) { | 234 | if (typeof value === 'string') { |
241 | let originallyPublishedAt: Date = null | 235 | newObj[key] = value.normalize() |
236 | } else { | ||
237 | newObj[key] = value | ||
238 | } | ||
239 | } | ||
242 | 240 | ||
243 | const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date) | 241 | return newObj |
244 | if (uploadDateMatcher) { | 242 | } |
245 | originallyPublishedAt = new Date() | ||
246 | originallyPublishedAt.setHours(0, 0, 0, 0) | ||
247 | 243 | ||
248 | const year = parseInt(uploadDateMatcher[1], 10) | 244 | private buildVideoInfo (obj: any): YoutubeDLInfo { |
249 | // Month starts from 0 | 245 | return { |
250 | const month = parseInt(uploadDateMatcher[2], 10) - 1 | 246 | name: this.titleTruncation(obj.title), |
251 | const day = parseInt(uploadDateMatcher[3], 10) | 247 | description: this.descriptionTruncation(obj.description), |
248 | category: this.getCategory(obj.categories), | ||
249 | licence: this.getLicence(obj.license), | ||
250 | language: this.getLanguage(obj.language), | ||
251 | nsfw: this.isNSFW(obj), | ||
252 | tags: this.getTags(obj.tags), | ||
253 | thumbnailUrl: obj.thumbnail || undefined, | ||
254 | originallyPublishedAt: this.buildOriginallyPublishedAt(obj), | ||
255 | ext: obj.ext | ||
256 | } | ||
257 | } | ||
252 | 258 | ||
253 | originallyPublishedAt.setFullYear(year, month, day) | 259 | private titleTruncation (title: string) { |
260 | return peertubeTruncate(title, { | ||
261 | length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max, | ||
262 | separator: /,? +/, | ||
263 | omission: ' […]' | ||
264 | }) | ||
254 | } | 265 | } |
255 | 266 | ||
256 | return originallyPublishedAt | 267 | private descriptionTruncation (description: string) { |
257 | } | 268 | if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined |
258 | 269 | ||
259 | // --------------------------------------------------------------------------- | 270 | return peertubeTruncate(description, { |
271 | length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, | ||
272 | separator: /,? +/, | ||
273 | omission: ' […]' | ||
274 | }) | ||
275 | } | ||
260 | 276 | ||
261 | export { | 277 | private isNSFW (info: any) { |
262 | updateYoutubeDLBinary, | 278 | return info.age_limit && info.age_limit >= 16 |
263 | getYoutubeDLVideoFormat, | 279 | } |
264 | downloadYoutubeDLVideo, | ||
265 | getYoutubeDLSubs, | ||
266 | getYoutubeDLInfo, | ||
267 | safeGetYoutubeDL, | ||
268 | buildOriginallyPublishedAt | ||
269 | } | ||
270 | 280 | ||
271 | // --------------------------------------------------------------------------- | 281 | private getTags (tags: any) { |
282 | if (Array.isArray(tags) === false) return [] | ||
272 | 283 | ||
273 | async function guessVideoPathWithExtension (tmpPath: string, sourceExt: string) { | 284 | return tags |
274 | if (!isVideoFileExtnameValid(sourceExt)) { | 285 | .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) |
275 | throw new Error('Invalid video extension ' + sourceExt) | 286 | .map(t => t.normalize()) |
287 | .slice(0, 5) | ||
276 | } | 288 | } |
277 | 289 | ||
278 | const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ] | 290 | private getLicence (licence: string) { |
291 | if (!licence) return undefined | ||
279 | 292 | ||
280 | for (const extension of extensions) { | 293 | if (licence.includes('Creative Commons Attribution')) return 1 |
281 | const path = tmpPath + extension | ||
282 | 294 | ||
283 | if (await pathExists(path)) return path | 295 | for (const key of Object.keys(VIDEO_LICENCES)) { |
284 | } | 296 | const peertubeLicence = VIDEO_LICENCES[key] |
297 | if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10) | ||
298 | } | ||
285 | 299 | ||
286 | throw new Error('Cannot guess path of ' + tmpPath) | 300 | return undefined |
287 | } | 301 | } |
288 | 302 | ||
289 | function normalizeObject (obj: any) { | 303 | private getCategory (categories: string[]) { |
290 | const newObj: any = {} | 304 | if (!categories) return undefined |
291 | 305 | ||
292 | for (const key of Object.keys(obj)) { | 306 | const categoryString = categories[0] |
293 | // Deprecated key | 307 | if (!categoryString || typeof categoryString !== 'string') return undefined |
294 | if (key === 'resolution') continue | ||
295 | 308 | ||
296 | const value = obj[key] | 309 | if (categoryString === 'News & Politics') return 11 |
297 | 310 | ||
298 | if (typeof value === 'string') { | 311 | for (const key of Object.keys(VIDEO_CATEGORIES)) { |
299 | newObj[key] = value.normalize() | 312 | const category = VIDEO_CATEGORIES[key] |
300 | } else { | 313 | if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10) |
301 | newObj[key] = value | ||
302 | } | 314 | } |
303 | } | ||
304 | 315 | ||
305 | return newObj | 316 | return undefined |
306 | } | ||
307 | |||
308 | function buildVideoInfo (obj: any): YoutubeDLInfo { | ||
309 | return { | ||
310 | name: titleTruncation(obj.title), | ||
311 | description: descriptionTruncation(obj.description), | ||
312 | category: getCategory(obj.categories), | ||
313 | licence: getLicence(obj.license), | ||
314 | language: getLanguage(obj.language), | ||
315 | nsfw: isNSFW(obj), | ||
316 | tags: getTags(obj.tags), | ||
317 | thumbnailUrl: obj.thumbnail || undefined, | ||
318 | originallyPublishedAt: buildOriginallyPublishedAt(obj), | ||
319 | ext: obj.ext | ||
320 | } | 317 | } |
321 | } | ||
322 | 318 | ||
323 | function titleTruncation (title: string) { | 319 | private getLanguage (language: string) { |
324 | return peertubeTruncate(title, { | 320 | return VIDEO_LANGUAGES[language] ? language : undefined |
325 | length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max, | 321 | } |
326 | separator: /,? +/, | ||
327 | omission: ' […]' | ||
328 | }) | ||
329 | } | ||
330 | 322 | ||
331 | function descriptionTruncation (description: string) { | 323 | private wrapWithProxyOptions (options: string[]) { |
332 | if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined | 324 | if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) { |
325 | logger.debug('Using proxy for YoutubeDL') | ||
333 | 326 | ||
334 | return peertubeTruncate(description, { | 327 | return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options) |
335 | length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, | 328 | } |
336 | separator: /,? +/, | ||
337 | omission: ' […]' | ||
338 | }) | ||
339 | } | ||
340 | 329 | ||
341 | function isNSFW (info: any) { | 330 | return options |
342 | return info.age_limit && info.age_limit >= 16 | 331 | } |
343 | } | ||
344 | 332 | ||
345 | function getTags (tags: any) { | 333 | // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js |
346 | if (Array.isArray(tags) === false) return [] | 334 | // We rewrote it to avoid sync calls |
335 | static async updateYoutubeDLBinary () { | ||
336 | logger.info('Updating youtubeDL binary.') | ||
347 | 337 | ||
348 | return tags | 338 | const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin') |
349 | .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) | 339 | const bin = join(binDirectory, 'youtube-dl') |
350 | .map(t => t.normalize()) | 340 | const detailsPath = join(binDirectory, 'details') |
351 | .slice(0, 5) | 341 | const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl' |
352 | } | ||
353 | 342 | ||
354 | function getLicence (licence: string) { | 343 | await ensureDir(binDirectory) |
355 | if (!licence) return undefined | ||
356 | 344 | ||
357 | if (licence.includes('Creative Commons Attribution')) return 1 | 345 | try { |
346 | const result = await got(url, { followRedirect: false }) | ||
358 | 347 | ||
359 | for (const key of Object.keys(VIDEO_LICENCES)) { | 348 | if (result.statusCode !== HttpStatusCode.FOUND_302) { |
360 | const peertubeLicence = VIDEO_LICENCES[key] | 349 | logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) |
361 | if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10) | 350 | return |
362 | } | 351 | } |
363 | 352 | ||
364 | return undefined | 353 | const newUrl = result.headers.location |
365 | } | 354 | const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1] |
366 | 355 | ||
367 | function getCategory (categories: string[]) { | 356 | const downloadFileStream = got.stream(newUrl) |
368 | if (!categories) return undefined | 357 | const writeStream = createWriteStream(bin, { mode: 493 }) |
369 | 358 | ||
370 | const categoryString = categories[0] | 359 | await pipelinePromise( |
371 | if (!categoryString || typeof categoryString !== 'string') return undefined | 360 | downloadFileStream, |
361 | writeStream | ||
362 | ) | ||
372 | 363 | ||
373 | if (categoryString === 'News & Politics') return 11 | 364 | const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) |
365 | await writeFile(detailsPath, details, { encoding: 'utf8' }) | ||
374 | 366 | ||
375 | for (const key of Object.keys(VIDEO_CATEGORIES)) { | 367 | logger.info('youtube-dl updated to version %s.', newVersion) |
376 | const category = VIDEO_CATEGORIES[key] | 368 | } catch (err) { |
377 | if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10) | 369 | logger.error('Cannot update youtube-dl.', { err }) |
370 | } | ||
378 | } | 371 | } |
379 | 372 | ||
380 | return undefined | 373 | static async safeGetYoutubeDL () { |
381 | } | 374 | let youtubeDL |
382 | |||
383 | function getLanguage (language: string) { | ||
384 | return VIDEO_LANGUAGES[language] ? language : undefined | ||
385 | } | ||
386 | 375 | ||
387 | function wrapWithProxyOptions (options: string[]) { | 376 | try { |
388 | if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) { | 377 | youtubeDL = require('youtube-dl') |
389 | logger.debug('Using proxy for YoutubeDL') | 378 | } catch (e) { |
379 | // Download binary | ||
380 | await this.updateYoutubeDLBinary() | ||
381 | youtubeDL = require('youtube-dl') | ||
382 | } | ||
390 | 383 | ||
391 | return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options) | 384 | return youtubeDL |
392 | } | 385 | } |
386 | } | ||
387 | |||
388 | // --------------------------------------------------------------------------- | ||
393 | 389 | ||
394 | return options | 390 | export { |
391 | YoutubeDL | ||
395 | } | 392 | } |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index a93c8b7fd..911734fa0 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -7,7 +7,7 @@ import { RecentlyAddedStrategy } from '../../shared/models/redundancy' | |||
7 | import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils' | 7 | import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils' |
8 | import { isArray } from '../helpers/custom-validators/misc' | 8 | import { isArray } from '../helpers/custom-validators/misc' |
9 | import { logger } from '../helpers/logger' | 9 | import { logger } from '../helpers/logger' |
10 | import { UserModel } from '../models/account/user' | 10 | import { UserModel } from '../models/user/user' |
11 | import { ApplicationModel, getServerActor } from '../models/application/application' | 11 | import { ApplicationModel, getServerActor } from '../models/application/application' |
12 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 12 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
13 | import { CONFIG, isEmailEnabled } from './config' | 13 | import { CONFIG, isEmailEnabled } from './config' |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 2864b0287..93c019121 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -19,7 +19,7 @@ function checkMissedConfig () { | |||
19 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', | 19 | 'csp.enabled', 'csp.report_only', 'csp.report_uri', |
20 | 'security.frameguard.enabled', | 20 | 'security.frameguard.enabled', |
21 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', | 21 | 'cache.previews.size', 'cache.captions.size', 'cache.torrents.size', 'admin.email', 'contact_form.enabled', |
22 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', | 22 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', 'signup.minimum_age', |
23 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | 23 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', |
24 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', | 24 | 'redundancy.videos.strategies', 'redundancy.videos.check_interval', |
25 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', | 25 | 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 5281d3a66..30a9823b9 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -185,6 +185,7 @@ const CONFIG = { | |||
185 | get ENABLED () { return config.get<boolean>('signup.enabled') }, | 185 | get ENABLED () { return config.get<boolean>('signup.enabled') }, |
186 | get LIMIT () { return config.get<number>('signup.limit') }, | 186 | get LIMIT () { return config.get<number>('signup.limit') }, |
187 | get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') }, | 187 | get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') }, |
188 | get MINIMUM_AGE () { return config.get<number>('signup.minimum_age') }, | ||
188 | FILTERS: { | 189 | FILTERS: { |
189 | CIDR: { | 190 | CIDR: { |
190 | get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') }, | 191 | get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') }, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 6f388420e..ab59320eb 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
24 | 24 | ||
25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
26 | 26 | ||
27 | const LAST_MIGRATION_VERSION = 645 | 27 | const LAST_MIGRATION_VERSION = 650 |
28 | 28 | ||
29 | // --------------------------------------------------------------------------- | 29 | // --------------------------------------------------------------------------- |
30 | 30 | ||
@@ -77,6 +77,7 @@ const SORTABLE_COLUMNS = { | |||
77 | // Don't forget to update peertube-search-index with the same values | 77 | // Don't forget to update peertube-search-index with the same values |
78 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], | 78 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], |
79 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], | 79 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], |
80 | VIDEO_PLAYLISTS_SEARCH: [ 'match', 'displayName', 'createdAt' ], | ||
80 | 81 | ||
81 | ABUSES: [ 'id', 'createdAt', 'state' ], | 82 | ABUSES: [ 'id', 'createdAt', 'state' ], |
82 | 83 | ||
@@ -152,7 +153,7 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = { | |||
152 | const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = { | 153 | const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = { |
153 | 'activitypub-http-broadcast': 1, | 154 | 'activitypub-http-broadcast': 1, |
154 | 'activitypub-http-unicast': 5, | 155 | 'activitypub-http-unicast': 5, |
155 | 'activitypub-http-fetcher': 1, | 156 | 'activitypub-http-fetcher': 3, |
156 | 'activitypub-cleaner': 1, | 157 | 'activitypub-cleaner': 1, |
157 | 'activitypub-follow': 1, | 158 | 'activitypub-follow': 1, |
158 | 'video-file-import': 1, | 159 | 'video-file-import': 1, |
@@ -245,7 +246,7 @@ const CONSTRAINTS_FIELDS = { | |||
245 | CAPTION_FILE: { | 246 | CAPTION_FILE: { |
246 | EXTNAME: [ '.vtt', '.srt' ], | 247 | EXTNAME: [ '.vtt', '.srt' ], |
247 | FILE_SIZE: { | 248 | FILE_SIZE: { |
248 | max: 2 * 1024 * 1024 // 2MB | 249 | max: 4 * 1024 * 1024 // 4MB |
249 | } | 250 | } |
250 | } | 251 | } |
251 | }, | 252 | }, |
@@ -274,7 +275,7 @@ const CONSTRAINTS_FIELDS = { | |||
274 | IMAGE: { | 275 | IMAGE: { |
275 | EXTNAME: [ '.png', '.jpg', '.jpeg', '.webp' ], | 276 | EXTNAME: [ '.png', '.jpg', '.jpeg', '.webp' ], |
276 | FILE_SIZE: { | 277 | FILE_SIZE: { |
277 | max: 2 * 1024 * 1024 // 2MB | 278 | max: 4 * 1024 * 1024 // 4MB |
278 | } | 279 | } |
279 | }, | 280 | }, |
280 | EXTNAME: [] as string[], | 281 | EXTNAME: [] as string[], |
@@ -296,7 +297,7 @@ const CONSTRAINTS_FIELDS = { | |||
296 | IMAGE: { | 297 | IMAGE: { |
297 | EXTNAME: [ '.jpg', '.jpeg' ], | 298 | EXTNAME: [ '.jpg', '.jpeg' ], |
298 | FILE_SIZE: { | 299 | FILE_SIZE: { |
299 | max: 2 * 1024 * 1024 // 2MB | 300 | max: 4 * 1024 * 1024 // 4MB |
300 | } | 301 | } |
301 | } | 302 | } |
302 | }, | 303 | }, |
@@ -307,7 +308,7 @@ const CONSTRAINTS_FIELDS = { | |||
307 | IMAGE: { | 308 | IMAGE: { |
308 | EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], | 309 | EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], |
309 | FILE_SIZE: { | 310 | FILE_SIZE: { |
310 | max: 2 * 1024 * 1024 // 2MB | 311 | max: 4 * 1024 * 1024 // 4MB |
311 | } | 312 | } |
312 | } | 313 | } |
313 | }, | 314 | }, |
@@ -447,9 +448,10 @@ const MIMETYPES = { | |||
447 | 'audio/ogg': '.ogg', | 448 | 'audio/ogg': '.ogg', |
448 | 'audio/x-ms-wma': '.wma', | 449 | 'audio/x-ms-wma': '.wma', |
449 | 'audio/wav': '.wav', | 450 | 'audio/wav': '.wav', |
451 | 'audio/x-wav': '.wav', | ||
450 | 'audio/x-flac': '.flac', | 452 | 'audio/x-flac': '.flac', |
451 | 'audio/flac': '.flac', | 453 | 'audio/flac': '.flac', |
452 | '‎audio/aac': '.aac', | 454 | 'audio/aac': '.aac', |
453 | 'audio/m4a': '.m4a', | 455 | 'audio/m4a': '.m4a', |
454 | 'audio/mp4': '.m4a', | 456 | 'audio/mp4': '.m4a', |
455 | 'audio/x-m4a': '.m4a', | 457 | 'audio/x-m4a': '.m4a', |
@@ -702,7 +704,8 @@ const CUSTOM_HTML_TAG_COMMENTS = { | |||
702 | TITLE: '<!-- title tag -->', | 704 | TITLE: '<!-- title tag -->', |
703 | DESCRIPTION: '<!-- description tag -->', | 705 | DESCRIPTION: '<!-- description tag -->', |
704 | CUSTOM_CSS: '<!-- custom css tag -->', | 706 | CUSTOM_CSS: '<!-- custom css tag -->', |
705 | META_TAGS: '<!-- meta tags -->' | 707 | META_TAGS: '<!-- meta tags -->', |
708 | SERVER_CONFIG: '<!-- server config -->' | ||
706 | } | 709 | } |
707 | 710 | ||
708 | // --------------------------------------------------------------------------- | 711 | // --------------------------------------------------------------------------- |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index edf12bc41..38e7a76d0 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -2,6 +2,9 @@ import { QueryTypes, Transaction } from 'sequelize' | |||
2 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' | 2 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' |
3 | import { TrackerModel } from '@server/models/server/tracker' | 3 | import { TrackerModel } from '@server/models/server/tracker' |
4 | import { VideoTrackerModel } from '@server/models/server/video-tracker' | 4 | import { VideoTrackerModel } from '@server/models/server/video-tracker' |
5 | import { UserModel } from '@server/models/user/user' | ||
6 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
7 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | ||
5 | import { isTestInstance } from '../helpers/core-utils' | 8 | import { isTestInstance } from '../helpers/core-utils' |
6 | import { logger } from '../helpers/logger' | 9 | import { logger } from '../helpers/logger' |
7 | import { AbuseModel } from '../models/abuse/abuse' | 10 | import { AbuseModel } from '../models/abuse/abuse' |
@@ -11,13 +14,9 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse' | |||
11 | import { AccountModel } from '../models/account/account' | 14 | import { AccountModel } from '../models/account/account' |
12 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 15 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
13 | import { AccountVideoRateModel } from '../models/account/account-video-rate' | 16 | import { AccountVideoRateModel } from '../models/account/account-video-rate' |
14 | import { ActorImageModel } from '../models/account/actor-image' | 17 | import { ActorModel } from '../models/actor/actor' |
15 | import { UserModel } from '../models/account/user' | 18 | import { ActorFollowModel } from '../models/actor/actor-follow' |
16 | import { UserNotificationModel } from '../models/account/user-notification' | 19 | import { ActorImageModel } from '../models/actor/actor-image' |
17 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | ||
18 | import { UserVideoHistoryModel } from '../models/account/user-video-history' | ||
19 | import { ActorModel } from '../models/activitypub/actor' | ||
20 | import { ActorFollowModel } from '../models/activitypub/actor-follow' | ||
21 | import { ApplicationModel } from '../models/application/application' | 20 | import { ApplicationModel } from '../models/application/application' |
22 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 21 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
23 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 22 | import { OAuthTokenModel } from '../models/oauth/oauth-token' |
@@ -25,6 +24,7 @@ import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | |||
25 | import { PluginModel } from '../models/server/plugin' | 24 | import { PluginModel } from '../models/server/plugin' |
26 | import { ServerModel } from '../models/server/server' | 25 | import { ServerModel } from '../models/server/server' |
27 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | 26 | import { ServerBlocklistModel } from '../models/server/server-blocklist' |
27 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' | ||
28 | import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' | 28 | import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' |
29 | import { TagModel } from '../models/video/tag' | 29 | import { TagModel } from '../models/video/tag' |
30 | import { ThumbnailModel } from '../models/video/thumbnail' | 30 | import { ThumbnailModel } from '../models/video/thumbnail' |
@@ -44,6 +44,7 @@ import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-pla | |||
44 | import { VideoTagModel } from '../models/video/video-tag' | 44 | import { VideoTagModel } from '../models/video/video-tag' |
45 | import { VideoViewModel } from '../models/video/video-view' | 45 | import { VideoViewModel } from '../models/video/video-view' |
46 | import { CONFIG } from './config' | 46 | import { CONFIG } from './config' |
47 | import { ActorCustomPageModel } from '@server/models/account/actor-custom-page' | ||
47 | 48 | ||
48 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 49 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
49 | 50 | ||
@@ -141,7 +142,8 @@ async function initDatabaseModels (silent: boolean) { | |||
141 | ThumbnailModel, | 142 | ThumbnailModel, |
142 | TrackerModel, | 143 | TrackerModel, |
143 | VideoTrackerModel, | 144 | VideoTrackerModel, |
144 | PluginModel | 145 | PluginModel, |
146 | ActorCustomPageModel | ||
145 | ]) | 147 | ]) |
146 | 148 | ||
147 | // Check extensions exist in the database | 149 | // Check extensions exist in the database |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 8dcff64e2..676f88653 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -2,7 +2,7 @@ import * as passwordGenerator from 'password-generator' | |||
2 | import { UserRole } from '../../shared' | 2 | import { UserRole } from '../../shared' |
3 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
4 | import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user' | 4 | import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user' |
5 | import { UserModel } from '../models/account/user' | 5 | import { UserModel } from '../models/user/user' |
6 | import { ApplicationModel } from '../models/application/application' | 6 | import { ApplicationModel } from '../models/application/application' |
7 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 7 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' | 8 | import { applicationExist, clientsExist, usersExist } from './checker-after-init' |
diff --git a/server/initializers/migrations/0080-video-channels.ts b/server/initializers/migrations/0080-video-channels.ts index 883224cb0..0e6952350 100644 --- a/server/initializers/migrations/0080-video-channels.ts +++ b/server/initializers/migrations/0080-video-channels.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { buildUUID } from '@server/helpers/uuid' | ||
1 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
2 | import { v4 as uuidv4 } from 'uuid' | ||
3 | 3 | ||
4 | async function up (utils: { | 4 | async function up (utils: { |
5 | transaction: Sequelize.Transaction | 5 | transaction: Sequelize.Transaction |
@@ -23,7 +23,7 @@ async function up (utils: { | |||
23 | { | 23 | { |
24 | const authors = await utils.db.Author.findAll() | 24 | const authors = await utils.db.Author.findAll() |
25 | for (const author of authors) { | 25 | for (const author of authors) { |
26 | author.uuid = uuidv4() | 26 | author.uuid = buildUUID() |
27 | await author.save() | 27 | await author.save() |
28 | } | 28 | } |
29 | } | 29 | } |
diff --git a/server/initializers/migrations/0345-video-playlists.ts b/server/initializers/migrations/0345-video-playlists.ts index 89a14a6ee..8dd631dff 100644 --- a/server/initializers/migrations/0345-video-playlists.ts +++ b/server/initializers/migrations/0345-video-playlists.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { buildUUID } from '@server/helpers/uuid' | ||
2 | import { VideoPlaylistPrivacy, VideoPlaylistType } from '../../../shared/models/videos' | 3 | import { VideoPlaylistPrivacy, VideoPlaylistType } from '../../../shared/models/videos' |
3 | import { v4 as uuidv4 } from 'uuid' | ||
4 | import { WEBSERVER } from '../constants' | 4 | import { WEBSERVER } from '../constants' |
5 | 5 | ||
6 | async function up (utils: { | 6 | async function up (utils: { |
@@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS "videoPlaylistElement" | |||
57 | const usernames = userResult.map(r => r.username) | 57 | const usernames = userResult.map(r => r.username) |
58 | 58 | ||
59 | for (const username of usernames) { | 59 | for (const username of usernames) { |
60 | const uuid = uuidv4() | 60 | const uuid = buildUUID() |
61 | 61 | ||
62 | const baseUrl = WEBSERVER.URL + '/video-playlists/' + uuid | 62 | const baseUrl = WEBSERVER.URL + '/video-playlists/' + uuid |
63 | const query = ` | 63 | const query = ` |
diff --git a/server/initializers/migrations/0560-user-feed-token.ts b/server/initializers/migrations/0560-user-feed-token.ts index 7c61def17..042301352 100644 --- a/server/initializers/migrations/0560-user-feed-token.ts +++ b/server/initializers/migrations/0560-user-feed-token.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { v4 as uuidv4 } from 'uuid' | 2 | import { buildUUID } from '@server/helpers/uuid' |
3 | 3 | ||
4 | async function up (utils: { | 4 | async function up (utils: { |
5 | transaction: Sequelize.Transaction | 5 | transaction: Sequelize.Transaction |
@@ -26,7 +26,7 @@ async function up (utils: { | |||
26 | const users = await utils.sequelize.query<any>(query, options) | 26 | const users = await utils.sequelize.query<any>(query, options) |
27 | 27 | ||
28 | for (const user of users) { | 28 | for (const user of users) { |
29 | const queryUpdate = `UPDATE "user" SET "feedToken" = '${uuidv4()}' WHERE id = ${user.id}` | 29 | const queryUpdate = `UPDATE "user" SET "feedToken" = '${buildUUID()}' WHERE id = ${user.id}` |
30 | await utils.sequelize.query(queryUpdate) | 30 | await utils.sequelize.query(queryUpdate) |
31 | } | 31 | } |
32 | } | 32 | } |
diff --git a/server/initializers/migrations/0650-actor-custom-pages.ts b/server/initializers/migrations/0650-actor-custom-pages.ts new file mode 100644 index 000000000..1338327e8 --- /dev/null +++ b/server/initializers/migrations/0650-actor-custom-pages.ts | |||
@@ -0,0 +1,33 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const query = ` | ||
11 | CREATE TABLE IF NOT EXISTS "actorCustomPage" ( | ||
12 | "id" serial, | ||
13 | "content" TEXT, | ||
14 | "type" varchar(255) NOT NULL, | ||
15 | "actorId" integer NOT NULL REFERENCES "actor" ("id") ON DELETE CASCADE ON UPDATE CASCADE, | ||
16 | "createdAt" timestamp WITH time zone NOT NULL, | ||
17 | "updatedAt" timestamp WITH time zone NOT NULL, | ||
18 | PRIMARY KEY ("id") | ||
19 | ); | ||
20 | ` | ||
21 | |||
22 | await utils.sequelize.query(query) | ||
23 | } | ||
24 | } | ||
25 | |||
26 | function down (options) { | ||
27 | throw new Error('Not implemented.') | ||
28 | } | ||
29 | |||
30 | export { | ||
31 | up, | ||
32 | down | ||
33 | } | ||
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts deleted file mode 100644 index 5fe7381c9..000000000 --- a/server/lib/activitypub/actor.ts +++ /dev/null | |||
@@ -1,594 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { extname } from 'path' | ||
3 | import { Op, Transaction } from 'sequelize' | ||
4 | import { URL } from 'url' | ||
5 | import { v4 as uuidv4 } from 'uuid' | ||
6 | import { getServerActor } from '@server/models/application/application' | ||
7 | import { ActorImageType } from '@shared/models' | ||
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
9 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | ||
10 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | ||
11 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
12 | import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' | ||
13 | import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' | ||
14 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
15 | import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' | ||
16 | import { logger } from '../../helpers/logger' | ||
17 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | ||
18 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' | ||
19 | import { getUrlFromWebfinger } from '../../helpers/webfinger' | ||
20 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | ||
21 | import { sequelizeTypescript } from '../../initializers/database' | ||
22 | import { AccountModel } from '../../models/account/account' | ||
23 | import { ActorImageModel } from '../../models/account/actor-image' | ||
24 | import { ActorModel } from '../../models/activitypub/actor' | ||
25 | import { ServerModel } from '../../models/server/server' | ||
26 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
27 | import { | ||
28 | MAccount, | ||
29 | MAccountDefault, | ||
30 | MActor, | ||
31 | MActorAccountChannelId, | ||
32 | MActorAccountChannelIdActor, | ||
33 | MActorAccountId, | ||
34 | MActorFull, | ||
35 | MActorFullActor, | ||
36 | MActorId, | ||
37 | MActorImage, | ||
38 | MActorImages, | ||
39 | MChannel | ||
40 | } from '../../types/models' | ||
41 | import { JobQueue } from '../job-queue' | ||
42 | |||
43 | // Set account keys, this could be long so process after the account creation and do not block the client | ||
44 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { | ||
45 | const { publicKey, privateKey } = await createPrivateAndPublicKeys() | ||
46 | |||
47 | actor.publicKey = publicKey | ||
48 | actor.privateKey = privateKey | ||
49 | |||
50 | return actor.save() | ||
51 | } | ||
52 | |||
53 | function getOrCreateActorAndServerAndModel ( | ||
54 | activityActor: string | ActivityPubActor, | ||
55 | fetchType: 'all', | ||
56 | recurseIfNeeded?: boolean, | ||
57 | updateCollections?: boolean | ||
58 | ): Promise<MActorFullActor> | ||
59 | |||
60 | function getOrCreateActorAndServerAndModel ( | ||
61 | activityActor: string | ActivityPubActor, | ||
62 | fetchType?: 'association-ids', | ||
63 | recurseIfNeeded?: boolean, | ||
64 | updateCollections?: boolean | ||
65 | ): Promise<MActorAccountChannelId> | ||
66 | |||
67 | async function getOrCreateActorAndServerAndModel ( | ||
68 | activityActor: string | ActivityPubActor, | ||
69 | fetchType: ActorFetchByUrlType = 'association-ids', | ||
70 | recurseIfNeeded = true, | ||
71 | updateCollections = false | ||
72 | ): Promise<MActorFullActor | MActorAccountChannelId> { | ||
73 | const actorUrl = getAPId(activityActor) | ||
74 | let created = false | ||
75 | let accountPlaylistsUrl: string | ||
76 | |||
77 | let actor = await fetchActorByUrl(actorUrl, fetchType) | ||
78 | // Orphan actor (not associated to an account of channel) so recreate it | ||
79 | if (actor && (!actor.Account && !actor.VideoChannel)) { | ||
80 | await actor.destroy() | ||
81 | actor = null | ||
82 | } | ||
83 | |||
84 | // We don't have this actor in our database, fetch it on remote | ||
85 | if (!actor) { | ||
86 | const { result } = await fetchRemoteActor(actorUrl) | ||
87 | if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl) | ||
88 | |||
89 | // Create the attributed to actor | ||
90 | // In PeerTube a video channel is owned by an account | ||
91 | let ownerActor: MActorFullActor | ||
92 | if (recurseIfNeeded === true && result.actor.type === 'Group') { | ||
93 | const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') | ||
94 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) | ||
95 | |||
96 | if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) { | ||
97 | throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`) | ||
98 | } | ||
99 | |||
100 | try { | ||
101 | // Don't recurse another time | ||
102 | const recurseIfNeeded = false | ||
103 | ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', recurseIfNeeded) | ||
104 | } catch (err) { | ||
105 | logger.error('Cannot get or create account attributed to video channel ' + actorUrl) | ||
106 | throw new Error(err) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor) | ||
111 | created = true | ||
112 | accountPlaylistsUrl = result.playlists | ||
113 | } | ||
114 | |||
115 | if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor | ||
116 | if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor | ||
117 | |||
118 | const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType) | ||
119 | if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.') | ||
120 | |||
121 | if ((created === true || refreshed === true) && updateCollections === true) { | ||
122 | const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } | ||
123 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) | ||
124 | } | ||
125 | |||
126 | // We created a new account: fetch the playlists | ||
127 | if (created === true && actor.Account && accountPlaylistsUrl) { | ||
128 | const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } | ||
129 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) | ||
130 | } | ||
131 | |||
132 | return actorRefreshed | ||
133 | } | ||
134 | |||
135 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) { | ||
136 | return new ActorModel({ | ||
137 | type, | ||
138 | url, | ||
139 | preferredUsername, | ||
140 | uuid, | ||
141 | publicKey: null, | ||
142 | privateKey: null, | ||
143 | followersCount: 0, | ||
144 | followingCount: 0, | ||
145 | inboxUrl: url + '/inbox', | ||
146 | outboxUrl: url + '/outbox', | ||
147 | sharedInboxUrl: WEBSERVER.URL + '/inbox', | ||
148 | followersUrl: url + '/followers', | ||
149 | followingUrl: url + '/following' | ||
150 | }) as MActor | ||
151 | } | ||
152 | |||
153 | async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) { | ||
154 | const followersCount = await fetchActorTotalItems(attributes.followers) | ||
155 | const followingCount = await fetchActorTotalItems(attributes.following) | ||
156 | |||
157 | actorInstance.type = attributes.type | ||
158 | actorInstance.preferredUsername = attributes.preferredUsername | ||
159 | actorInstance.url = attributes.id | ||
160 | actorInstance.publicKey = attributes.publicKey.publicKeyPem | ||
161 | actorInstance.followersCount = followersCount | ||
162 | actorInstance.followingCount = followingCount | ||
163 | actorInstance.inboxUrl = attributes.inbox | ||
164 | actorInstance.outboxUrl = attributes.outbox | ||
165 | actorInstance.followersUrl = attributes.followers | ||
166 | actorInstance.followingUrl = attributes.following | ||
167 | |||
168 | if (attributes.published) actorInstance.remoteCreatedAt = new Date(attributes.published) | ||
169 | |||
170 | if (attributes.endpoints?.sharedInbox) { | ||
171 | actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox | ||
172 | } | ||
173 | } | ||
174 | |||
175 | type ImageInfo = { | ||
176 | name: string | ||
177 | fileUrl: string | ||
178 | height: number | ||
179 | width: number | ||
180 | onDisk?: boolean | ||
181 | } | ||
182 | async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { | ||
183 | const oldImageModel = type === ActorImageType.AVATAR | ||
184 | ? actor.Avatar | ||
185 | : actor.Banner | ||
186 | |||
187 | if (oldImageModel) { | ||
188 | // Don't update the avatar if the file URL did not change | ||
189 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor | ||
190 | |||
191 | try { | ||
192 | await oldImageModel.destroy({ transaction: t }) | ||
193 | |||
194 | setActorImage(actor, type, null) | ||
195 | } catch (err) { | ||
196 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
197 | } | ||
198 | } | ||
199 | |||
200 | if (imageInfo) { | ||
201 | const imageModel = await ActorImageModel.create({ | ||
202 | filename: imageInfo.name, | ||
203 | onDisk: imageInfo.onDisk ?? false, | ||
204 | fileUrl: imageInfo.fileUrl, | ||
205 | height: imageInfo.height, | ||
206 | width: imageInfo.width, | ||
207 | type | ||
208 | }, { transaction: t }) | ||
209 | |||
210 | setActorImage(actor, type, imageModel) | ||
211 | } | ||
212 | |||
213 | return actor | ||
214 | } | ||
215 | |||
216 | async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { | ||
217 | try { | ||
218 | if (type === ActorImageType.AVATAR) { | ||
219 | await actor.Avatar.destroy({ transaction: t }) | ||
220 | |||
221 | actor.avatarId = null | ||
222 | actor.Avatar = null | ||
223 | } else { | ||
224 | await actor.Banner.destroy({ transaction: t }) | ||
225 | |||
226 | actor.bannerId = null | ||
227 | actor.Banner = null | ||
228 | } | ||
229 | } catch (err) { | ||
230 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) | ||
231 | } | ||
232 | |||
233 | return actor | ||
234 | } | ||
235 | |||
236 | async function fetchActorTotalItems (url: string) { | ||
237 | try { | ||
238 | const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true }) | ||
239 | |||
240 | return body.totalItems || 0 | ||
241 | } catch (err) { | ||
242 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) | ||
243 | return 0 | ||
244 | } | ||
245 | } | ||
246 | |||
247 | function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) { | ||
248 | const mimetypes = MIMETYPES.IMAGE | ||
249 | const icon = type === ActorImageType.AVATAR | ||
250 | ? actorJSON.icon | ||
251 | : actorJSON.image | ||
252 | |||
253 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined | ||
254 | |||
255 | let extension: string | ||
256 | |||
257 | if (icon.mediaType) { | ||
258 | extension = mimetypes.MIMETYPE_EXT[icon.mediaType] | ||
259 | } else { | ||
260 | const tmp = extname(icon.url) | ||
261 | |||
262 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp | ||
263 | } | ||
264 | |||
265 | if (!extension) return undefined | ||
266 | |||
267 | return { | ||
268 | name: uuidv4() + extension, | ||
269 | fileUrl: icon.url, | ||
270 | height: icon.height, | ||
271 | width: icon.width, | ||
272 | type | ||
273 | } | ||
274 | } | ||
275 | |||
276 | async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) { | ||
277 | // Don't fetch ourselves | ||
278 | const serverActor = await getServerActor() | ||
279 | if (serverActor.id === actor.id) { | ||
280 | logger.error('Cannot fetch our own outbox!') | ||
281 | return undefined | ||
282 | } | ||
283 | |||
284 | const payload = { | ||
285 | uri: actor.outboxUrl, | ||
286 | type: 'activity' as 'activity' | ||
287 | } | ||
288 | |||
289 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | ||
290 | } | ||
291 | |||
292 | async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> ( | ||
293 | actorArg: T, | ||
294 | fetchedType: ActorFetchByUrlType | ||
295 | ): Promise<{ actor: T | MActorFull, refreshed: boolean }> { | ||
296 | if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } | ||
297 | |||
298 | // We need more attributes | ||
299 | const actor = fetchedType === 'all' | ||
300 | ? actorArg as MActorFull | ||
301 | : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
302 | |||
303 | try { | ||
304 | let actorUrl: string | ||
305 | try { | ||
306 | actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
307 | } catch (err) { | ||
308 | logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err) | ||
309 | actorUrl = actor.url | ||
310 | } | ||
311 | |||
312 | const { result } = await fetchRemoteActor(actorUrl) | ||
313 | |||
314 | if (result === undefined) { | ||
315 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
316 | return { actor, refreshed: false } | ||
317 | } | ||
318 | |||
319 | return sequelizeTypescript.transaction(async t => { | ||
320 | updateInstanceWithAnother(actor, result.actor) | ||
321 | |||
322 | await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t) | ||
323 | await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t) | ||
324 | |||
325 | // Force update | ||
326 | actor.setDataValue('updatedAt', new Date()) | ||
327 | await actor.save({ transaction: t }) | ||
328 | |||
329 | if (actor.Account) { | ||
330 | actor.Account.name = result.name | ||
331 | actor.Account.description = result.summary | ||
332 | |||
333 | await actor.Account.save({ transaction: t }) | ||
334 | } else if (actor.VideoChannel) { | ||
335 | actor.VideoChannel.name = result.name | ||
336 | actor.VideoChannel.description = result.summary | ||
337 | actor.VideoChannel.support = result.support | ||
338 | |||
339 | await actor.VideoChannel.save({ transaction: t }) | ||
340 | } | ||
341 | |||
342 | return { refreshed: true, actor } | ||
343 | }) | ||
344 | } catch (err) { | ||
345 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
346 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) | ||
347 | actor.Account | ||
348 | ? await actor.Account.destroy() | ||
349 | : await actor.VideoChannel.destroy() | ||
350 | |||
351 | return { actor: undefined, refreshed: false } | ||
352 | } | ||
353 | |||
354 | logger.warn('Cannot refresh actor %s.', actor.url, { err }) | ||
355 | return { actor, refreshed: false } | ||
356 | } | ||
357 | } | ||
358 | |||
359 | export { | ||
360 | getOrCreateActorAndServerAndModel, | ||
361 | buildActorInstance, | ||
362 | generateAndSaveActorKeys, | ||
363 | fetchActorTotalItems, | ||
364 | getImageInfoIfExists, | ||
365 | updateActorInstance, | ||
366 | deleteActorImageInstance, | ||
367 | refreshActorIfNeeded, | ||
368 | updateActorImageInstance, | ||
369 | addFetchOutboxJob | ||
370 | } | ||
371 | |||
372 | // --------------------------------------------------------------------------- | ||
373 | |||
374 | function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { | ||
375 | const id = imageModel | ||
376 | ? imageModel.id | ||
377 | : null | ||
378 | |||
379 | if (type === ActorImageType.AVATAR) { | ||
380 | actorModel.avatarId = id | ||
381 | actorModel.Avatar = imageModel | ||
382 | } else { | ||
383 | actorModel.bannerId = id | ||
384 | actorModel.Banner = imageModel | ||
385 | } | ||
386 | |||
387 | return actorModel | ||
388 | } | ||
389 | |||
390 | function saveActorAndServerAndModelIfNotExist ( | ||
391 | result: FetchRemoteActorResult, | ||
392 | ownerActor?: MActorFullActor, | ||
393 | t?: Transaction | ||
394 | ): Bluebird<MActorFullActor> | Promise<MActorFullActor> { | ||
395 | const actor = result.actor | ||
396 | |||
397 | if (t !== undefined) return save(t) | ||
398 | |||
399 | return sequelizeTypescript.transaction(t => save(t)) | ||
400 | |||
401 | async function save (t: Transaction) { | ||
402 | const actorHost = new URL(actor.url).host | ||
403 | |||
404 | const serverOptions = { | ||
405 | where: { | ||
406 | host: actorHost | ||
407 | }, | ||
408 | defaults: { | ||
409 | host: actorHost | ||
410 | }, | ||
411 | transaction: t | ||
412 | } | ||
413 | const [ server ] = await ServerModel.findOrCreate(serverOptions) | ||
414 | |||
415 | // Save our new account in database | ||
416 | actor.serverId = server.id | ||
417 | |||
418 | // Avatar? | ||
419 | if (result.avatar) { | ||
420 | const avatar = await ActorImageModel.create({ | ||
421 | filename: result.avatar.name, | ||
422 | fileUrl: result.avatar.fileUrl, | ||
423 | width: result.avatar.width, | ||
424 | height: result.avatar.height, | ||
425 | onDisk: false, | ||
426 | type: ActorImageType.AVATAR | ||
427 | }, { transaction: t }) | ||
428 | |||
429 | actor.avatarId = avatar.id | ||
430 | } | ||
431 | |||
432 | // Banner? | ||
433 | if (result.banner) { | ||
434 | const banner = await ActorImageModel.create({ | ||
435 | filename: result.banner.name, | ||
436 | fileUrl: result.banner.fileUrl, | ||
437 | width: result.banner.width, | ||
438 | height: result.banner.height, | ||
439 | onDisk: false, | ||
440 | type: ActorImageType.BANNER | ||
441 | }, { transaction: t }) | ||
442 | |||
443 | actor.bannerId = banner.id | ||
444 | } | ||
445 | |||
446 | // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists | ||
447 | // (which could be false in a retried query) | ||
448 | const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ | ||
449 | defaults: actor.toJSON(), | ||
450 | where: { | ||
451 | [Op.or]: [ | ||
452 | { | ||
453 | url: actor.url | ||
454 | }, | ||
455 | { | ||
456 | serverId: actor.serverId, | ||
457 | preferredUsername: actor.preferredUsername | ||
458 | } | ||
459 | ] | ||
460 | }, | ||
461 | transaction: t | ||
462 | }) | ||
463 | |||
464 | // Try to fix non HTTPS accounts of remote instances that fixed their URL afterwards | ||
465 | if (created !== true && actorCreated.url !== actor.url) { | ||
466 | // Only fix http://example.com/account/djidane to https://example.com/account/djidane | ||
467 | if (actorCreated.url.replace(/^http:\/\//, '') !== actor.url.replace(/^https:\/\//, '')) { | ||
468 | throw new Error(`Actor from DB with URL ${actorCreated.url} does not correspond to actor ${actor.url}`) | ||
469 | } | ||
470 | |||
471 | actorCreated.url = actor.url | ||
472 | await actorCreated.save({ transaction: t }) | ||
473 | } | ||
474 | |||
475 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { | ||
476 | actorCreated.Account = await saveAccount(actorCreated, result, t) as MAccountDefault | ||
477 | actorCreated.Account.Actor = actorCreated | ||
478 | } else if (actorCreated.type === 'Group') { // Video channel | ||
479 | const channel = await saveVideoChannel(actorCreated, result, ownerActor, t) | ||
480 | actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: ownerActor.Account }) | ||
481 | } | ||
482 | |||
483 | actorCreated.Server = server | ||
484 | |||
485 | return actorCreated | ||
486 | } | ||
487 | } | ||
488 | |||
489 | type ImageResult = { | ||
490 | name: string | ||
491 | fileUrl: string | ||
492 | height: number | ||
493 | width: number | ||
494 | } | ||
495 | |||
496 | type FetchRemoteActorResult = { | ||
497 | actor: MActor | ||
498 | name: string | ||
499 | summary: string | ||
500 | support?: string | ||
501 | playlists?: string | ||
502 | avatar?: ImageResult | ||
503 | banner?: ImageResult | ||
504 | attributedTo: ActivityPubAttributedTo[] | ||
505 | } | ||
506 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { | ||
507 | logger.info('Fetching remote actor %s.', actorUrl) | ||
508 | |||
509 | const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true }) | ||
510 | const actorJSON = requestResult.body | ||
511 | |||
512 | if (sanitizeAndCheckActorObject(actorJSON) === false) { | ||
513 | logger.debug('Remote actor JSON is not valid.', { actorJSON }) | ||
514 | return { result: undefined, statusCode: requestResult.statusCode } | ||
515 | } | ||
516 | |||
517 | if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { | ||
518 | logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) | ||
519 | return { result: undefined, statusCode: requestResult.statusCode } | ||
520 | } | ||
521 | |||
522 | const followersCount = await fetchActorTotalItems(actorJSON.followers) | ||
523 | const followingCount = await fetchActorTotalItems(actorJSON.following) | ||
524 | |||
525 | const actor = new ActorModel({ | ||
526 | type: actorJSON.type, | ||
527 | preferredUsername: actorJSON.preferredUsername, | ||
528 | url: actorJSON.id, | ||
529 | publicKey: actorJSON.publicKey.publicKeyPem, | ||
530 | privateKey: null, | ||
531 | followersCount: followersCount, | ||
532 | followingCount: followingCount, | ||
533 | inboxUrl: actorJSON.inbox, | ||
534 | outboxUrl: actorJSON.outbox, | ||
535 | followersUrl: actorJSON.followers, | ||
536 | followingUrl: actorJSON.following, | ||
537 | |||
538 | sharedInboxUrl: actorJSON.endpoints?.sharedInbox | ||
539 | ? actorJSON.endpoints.sharedInbox | ||
540 | : null | ||
541 | }) | ||
542 | |||
543 | const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR) | ||
544 | const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER) | ||
545 | |||
546 | const name = actorJSON.name || actorJSON.preferredUsername | ||
547 | return { | ||
548 | statusCode: requestResult.statusCode, | ||
549 | result: { | ||
550 | actor, | ||
551 | name, | ||
552 | avatar: avatarInfo, | ||
553 | banner: bannerInfo, | ||
554 | summary: actorJSON.summary, | ||
555 | support: actorJSON.support, | ||
556 | playlists: actorJSON.playlists, | ||
557 | attributedTo: actorJSON.attributedTo | ||
558 | } | ||
559 | } | ||
560 | } | ||
561 | |||
562 | async function saveAccount (actor: MActorId, result: FetchRemoteActorResult, t: Transaction) { | ||
563 | const [ accountCreated ] = await AccountModel.findOrCreate({ | ||
564 | defaults: { | ||
565 | name: result.name, | ||
566 | description: result.summary, | ||
567 | actorId: actor.id | ||
568 | }, | ||
569 | where: { | ||
570 | actorId: actor.id | ||
571 | }, | ||
572 | transaction: t | ||
573 | }) | ||
574 | |||
575 | return accountCreated as MAccount | ||
576 | } | ||
577 | |||
578 | async function saveVideoChannel (actor: MActorId, result: FetchRemoteActorResult, ownerActor: MActorAccountId, t: Transaction) { | ||
579 | const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({ | ||
580 | defaults: { | ||
581 | name: result.name, | ||
582 | description: result.summary, | ||
583 | support: result.support, | ||
584 | actorId: actor.id, | ||
585 | accountId: ownerActor.Account.id | ||
586 | }, | ||
587 | where: { | ||
588 | actorId: actor.id | ||
589 | }, | ||
590 | transaction: t | ||
591 | }) | ||
592 | |||
593 | return videoChannelCreated as MChannel | ||
594 | } | ||
diff --git a/server/lib/activitypub/actors/get.ts b/server/lib/activitypub/actors/get.ts new file mode 100644 index 000000000..8681ea02a --- /dev/null +++ b/server/lib/activitypub/actors/get.ts | |||
@@ -0,0 +1,122 @@ | |||
1 | |||
2 | import { checkUrlsSameHost, getAPId } from '@server/helpers/activitypub' | ||
3 | import { retryTransactionWrapper } from '@server/helpers/database-utils' | ||
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { JobQueue } from '@server/lib/job-queue' | ||
6 | import { ActorLoadByUrlType, loadActorByUrl } from '@server/lib/model-loaders' | ||
7 | import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models' | ||
8 | import { ActivityPubActor } from '@shared/models' | ||
9 | import { refreshActorIfNeeded } from './refresh' | ||
10 | import { APActorCreator, fetchRemoteActor } from './shared' | ||
11 | |||
12 | function getOrCreateAPActor ( | ||
13 | activityActor: string | ActivityPubActor, | ||
14 | fetchType: 'all', | ||
15 | recurseIfNeeded?: boolean, | ||
16 | updateCollections?: boolean | ||
17 | ): Promise<MActorFullActor> | ||
18 | |||
19 | function getOrCreateAPActor ( | ||
20 | activityActor: string | ActivityPubActor, | ||
21 | fetchType?: 'association-ids', | ||
22 | recurseIfNeeded?: boolean, | ||
23 | updateCollections?: boolean | ||
24 | ): Promise<MActorAccountChannelId> | ||
25 | |||
26 | async function getOrCreateAPActor ( | ||
27 | activityActor: string | ActivityPubActor, | ||
28 | fetchType: ActorLoadByUrlType = 'association-ids', | ||
29 | recurseIfNeeded = true, | ||
30 | updateCollections = false | ||
31 | ): Promise<MActorFullActor | MActorAccountChannelId> { | ||
32 | const actorUrl = getAPId(activityActor) | ||
33 | let actor = await loadActorFromDB(actorUrl, fetchType) | ||
34 | |||
35 | let created = false | ||
36 | let accountPlaylistsUrl: string | ||
37 | |||
38 | // We don't have this actor in our database, fetch it on remote | ||
39 | if (!actor) { | ||
40 | const { actorObject } = await fetchRemoteActor(actorUrl) | ||
41 | if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl) | ||
42 | |||
43 | // actorUrl is just an alias/rediraction, so process object id instead | ||
44 | if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections) | ||
45 | |||
46 | // Create the attributed to actor | ||
47 | // In PeerTube a video channel is owned by an account | ||
48 | let ownerActor: MActorFullActor | ||
49 | if (recurseIfNeeded === true && actorObject.type === 'Group') { | ||
50 | ownerActor = await getOrCreateAPOwner(actorObject, actorUrl) | ||
51 | } | ||
52 | |||
53 | const creator = new APActorCreator(actorObject, ownerActor) | ||
54 | actor = await retryTransactionWrapper(creator.create.bind(creator)) | ||
55 | created = true | ||
56 | accountPlaylistsUrl = actorObject.playlists | ||
57 | } | ||
58 | |||
59 | if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor | ||
60 | if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor | ||
61 | |||
62 | const { actor: actorRefreshed, refreshed } = await refreshActorIfNeeded({ actor, fetchedType: fetchType }) | ||
63 | if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.') | ||
64 | |||
65 | await scheduleOutboxFetchIfNeeded(actor, created, refreshed, updateCollections) | ||
66 | await schedulePlaylistFetchIfNeeded(actor, created, accountPlaylistsUrl) | ||
67 | |||
68 | return actorRefreshed | ||
69 | } | ||
70 | |||
71 | // --------------------------------------------------------------------------- | ||
72 | |||
73 | export { | ||
74 | getOrCreateAPActor | ||
75 | } | ||
76 | |||
77 | // --------------------------------------------------------------------------- | ||
78 | |||
79 | async function loadActorFromDB (actorUrl: string, fetchType: ActorLoadByUrlType) { | ||
80 | let actor = await loadActorByUrl(actorUrl, fetchType) | ||
81 | |||
82 | // Orphan actor (not associated to an account of channel) so recreate it | ||
83 | if (actor && (!actor.Account && !actor.VideoChannel)) { | ||
84 | await actor.destroy() | ||
85 | actor = null | ||
86 | } | ||
87 | |||
88 | return actor | ||
89 | } | ||
90 | |||
91 | function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) { | ||
92 | const accountAttributedTo = actorObject.attributedTo.find(a => a.type === 'Person') | ||
93 | if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actorUrl) | ||
94 | |||
95 | if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) { | ||
96 | throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`) | ||
97 | } | ||
98 | |||
99 | try { | ||
100 | // Don't recurse another time | ||
101 | const recurseIfNeeded = false | ||
102 | return getOrCreateAPActor(accountAttributedTo.id, 'all', recurseIfNeeded) | ||
103 | } catch (err) { | ||
104 | logger.error('Cannot get or create account attributed to video channel ' + actorUrl) | ||
105 | throw new Error(err) | ||
106 | } | ||
107 | } | ||
108 | |||
109 | async function scheduleOutboxFetchIfNeeded (actor: MActor, created: boolean, refreshed: boolean, updateCollections: boolean) { | ||
110 | if ((created === true || refreshed === true) && updateCollections === true) { | ||
111 | const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } | ||
112 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) | ||
113 | } | ||
114 | } | ||
115 | |||
116 | async function schedulePlaylistFetchIfNeeded (actor: MActorAccountId, created: boolean, accountPlaylistsUrl: string) { | ||
117 | // We created a new account: fetch the playlists | ||
118 | if (created === true && actor.Account && accountPlaylistsUrl) { | ||
119 | const payload = { uri: accountPlaylistsUrl, type: 'account-playlists' as 'account-playlists' } | ||
120 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) | ||
121 | } | ||
122 | } | ||
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts new file mode 100644 index 000000000..443ad0a63 --- /dev/null +++ b/server/lib/activitypub/actors/image.ts | |||
@@ -0,0 +1,94 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
4 | import { MActorImage, MActorImages } from '@server/types/models' | ||
5 | import { ActorImageType } from '@shared/models' | ||
6 | |||
7 | type ImageInfo = { | ||
8 | name: string | ||
9 | fileUrl: string | ||
10 | height: number | ||
11 | width: number | ||
12 | onDisk?: boolean | ||
13 | } | ||
14 | |||
15 | async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { | ||
16 | const oldImageModel = type === ActorImageType.AVATAR | ||
17 | ? actor.Avatar | ||
18 | : actor.Banner | ||
19 | |||
20 | if (oldImageModel) { | ||
21 | // Don't update the avatar if the file URL did not change | ||
22 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor | ||
23 | |||
24 | try { | ||
25 | await oldImageModel.destroy({ transaction: t }) | ||
26 | |||
27 | setActorImage(actor, type, null) | ||
28 | } catch (err) { | ||
29 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | if (imageInfo) { | ||
34 | const imageModel = await ActorImageModel.create({ | ||
35 | filename: imageInfo.name, | ||
36 | onDisk: imageInfo.onDisk ?? false, | ||
37 | fileUrl: imageInfo.fileUrl, | ||
38 | height: imageInfo.height, | ||
39 | width: imageInfo.width, | ||
40 | type | ||
41 | }, { transaction: t }) | ||
42 | |||
43 | setActorImage(actor, type, imageModel) | ||
44 | } | ||
45 | |||
46 | return actor | ||
47 | } | ||
48 | |||
49 | async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { | ||
50 | try { | ||
51 | if (type === ActorImageType.AVATAR) { | ||
52 | await actor.Avatar.destroy({ transaction: t }) | ||
53 | |||
54 | actor.avatarId = null | ||
55 | actor.Avatar = null | ||
56 | } else { | ||
57 | await actor.Banner.destroy({ transaction: t }) | ||
58 | |||
59 | actor.bannerId = null | ||
60 | actor.Banner = null | ||
61 | } | ||
62 | } catch (err) { | ||
63 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) | ||
64 | } | ||
65 | |||
66 | return actor | ||
67 | } | ||
68 | |||
69 | // --------------------------------------------------------------------------- | ||
70 | |||
71 | export { | ||
72 | ImageInfo, | ||
73 | |||
74 | updateActorImageInstance, | ||
75 | deleteActorImageInstance | ||
76 | } | ||
77 | |||
78 | // --------------------------------------------------------------------------- | ||
79 | |||
80 | function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { | ||
81 | const id = imageModel | ||
82 | ? imageModel.id | ||
83 | : null | ||
84 | |||
85 | if (type === ActorImageType.AVATAR) { | ||
86 | actorModel.avatarId = id | ||
87 | actorModel.Avatar = imageModel | ||
88 | } else { | ||
89 | actorModel.bannerId = id | ||
90 | actorModel.Banner = imageModel | ||
91 | } | ||
92 | |||
93 | return actorModel | ||
94 | } | ||
diff --git a/server/lib/activitypub/actors/index.ts b/server/lib/activitypub/actors/index.ts new file mode 100644 index 000000000..5ee2a6f1a --- /dev/null +++ b/server/lib/activitypub/actors/index.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | export * from './get' | ||
2 | export * from './image' | ||
3 | export * from './keys' | ||
4 | export * from './refresh' | ||
5 | export * from './updater' | ||
6 | export * from './webfinger' | ||
diff --git a/server/lib/activitypub/actors/keys.ts b/server/lib/activitypub/actors/keys.ts new file mode 100644 index 000000000..c3d18abd8 --- /dev/null +++ b/server/lib/activitypub/actors/keys.ts | |||
@@ -0,0 +1,16 @@ | |||
1 | import { createPrivateAndPublicKeys } from '@server/helpers/peertube-crypto' | ||
2 | import { MActor } from '@server/types/models' | ||
3 | |||
4 | // Set account keys, this could be long so process after the account creation and do not block the client | ||
5 | async function generateAndSaveActorKeys <T extends MActor> (actor: T) { | ||
6 | const { publicKey, privateKey } = await createPrivateAndPublicKeys() | ||
7 | |||
8 | actor.publicKey = publicKey | ||
9 | actor.privateKey = privateKey | ||
10 | |||
11 | return actor.save() | ||
12 | } | ||
13 | |||
14 | export { | ||
15 | generateAndSaveActorKeys | ||
16 | } | ||
diff --git a/server/lib/activitypub/actors/refresh.ts b/server/lib/activitypub/actors/refresh.ts new file mode 100644 index 000000000..b2fe3932f --- /dev/null +++ b/server/lib/activitypub/actors/refresh.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
2 | import { PromiseCache } from '@server/helpers/promise-cache' | ||
3 | import { PeerTubeRequestError } from '@server/helpers/requests' | ||
4 | import { ActorLoadByUrlType } from '@server/lib/model-loaders' | ||
5 | import { ActorModel } from '@server/models/actor/actor' | ||
6 | import { MActorAccountChannelId, MActorFull } from '@server/types/models' | ||
7 | import { HttpStatusCode } from '@shared/core-utils' | ||
8 | import { fetchRemoteActor } from './shared' | ||
9 | import { APActorUpdater } from './updater' | ||
10 | import { getUrlFromWebfinger } from './webfinger' | ||
11 | |||
12 | type RefreshResult <T> = Promise<{ actor: T | MActorFull, refreshed: boolean }> | ||
13 | |||
14 | type RefreshOptions <T> = { | ||
15 | actor: T | ||
16 | fetchedType: ActorLoadByUrlType | ||
17 | } | ||
18 | |||
19 | const promiseCache = new PromiseCache(doRefresh, (options: RefreshOptions<MActorFull | MActorAccountChannelId>) => options.actor.url) | ||
20 | |||
21 | function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <T> { | ||
22 | const actorArg = options.actor | ||
23 | if (!actorArg.isOutdated()) return Promise.resolve({ actor: actorArg, refreshed: false }) | ||
24 | |||
25 | return promiseCache.run(options) | ||
26 | } | ||
27 | |||
28 | export { | ||
29 | refreshActorIfNeeded | ||
30 | } | ||
31 | |||
32 | // --------------------------------------------------------------------------- | ||
33 | |||
34 | async function doRefresh <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <MActorFull> { | ||
35 | const { actor: actorArg, fetchedType } = options | ||
36 | |||
37 | // We need more attributes | ||
38 | const actor = fetchedType === 'all' | ||
39 | ? actorArg as MActorFull | ||
40 | : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) | ||
41 | |||
42 | const lTags = loggerTagsFactory('ap', 'actor', 'refresh', actor.url) | ||
43 | |||
44 | logger.info('Refreshing actor %s.', actor.url, lTags()) | ||
45 | |||
46 | try { | ||
47 | const actorUrl = await getActorUrl(actor) | ||
48 | const { actorObject } = await fetchRemoteActor(actorUrl) | ||
49 | |||
50 | if (actorObject === undefined) { | ||
51 | logger.warn('Cannot fetch remote actor in refresh actor.') | ||
52 | return { actor, refreshed: false } | ||
53 | } | ||
54 | |||
55 | const updater = new APActorUpdater(actorObject, actor) | ||
56 | await updater.update() | ||
57 | |||
58 | return { refreshed: true, actor } | ||
59 | } catch (err) { | ||
60 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
61 | logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url, lTags()) | ||
62 | |||
63 | actor.Account | ||
64 | ? await actor.Account.destroy() | ||
65 | : await actor.VideoChannel.destroy() | ||
66 | |||
67 | return { actor: undefined, refreshed: false } | ||
68 | } | ||
69 | |||
70 | logger.warn('Cannot refresh actor %s.', actor.url, { err, ...lTags() }) | ||
71 | return { actor, refreshed: false } | ||
72 | } | ||
73 | } | ||
74 | |||
75 | function getActorUrl (actor: MActorFull) { | ||
76 | return getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost()) | ||
77 | .catch(err => { | ||
78 | logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err) | ||
79 | return actor.url | ||
80 | }) | ||
81 | } | ||
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts new file mode 100644 index 000000000..999aed97d --- /dev/null +++ b/server/lib/activitypub/actors/shared/creator.ts | |||
@@ -0,0 +1,149 @@ | |||
1 | import { Op, Transaction } from 'sequelize' | ||
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | import { AccountModel } from '@server/models/account/account' | ||
4 | import { ActorModel } from '@server/models/actor/actor' | ||
5 | import { ServerModel } from '@server/models/server/server' | ||
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
7 | import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' | ||
8 | import { ActivityPubActor, ActorImageType } from '@shared/models' | ||
9 | import { updateActorImageInstance } from '../image' | ||
10 | import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes' | ||
11 | import { fetchActorFollowsCount } from './url-to-object' | ||
12 | |||
13 | export class APActorCreator { | ||
14 | |||
15 | constructor ( | ||
16 | private readonly actorObject: ActivityPubActor, | ||
17 | private readonly ownerActor?: MActorFullActor | ||
18 | ) { | ||
19 | |||
20 | } | ||
21 | |||
22 | async create (): Promise<MActorFullActor> { | ||
23 | const { followersCount, followingCount } = await fetchActorFollowsCount(this.actorObject) | ||
24 | |||
25 | const actorInstance = new ActorModel(getActorAttributesFromObject(this.actorObject, followersCount, followingCount)) | ||
26 | |||
27 | return sequelizeTypescript.transaction(async t => { | ||
28 | const server = await this.setServer(actorInstance, t) | ||
29 | |||
30 | await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t) | ||
31 | await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t) | ||
32 | |||
33 | const { actorCreated, created } = await this.saveActor(actorInstance, t) | ||
34 | |||
35 | await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) | ||
36 | |||
37 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance | ||
38 | actorCreated.Account = await this.saveAccount(actorCreated, t) as MAccountDefault | ||
39 | actorCreated.Account.Actor = actorCreated | ||
40 | } | ||
41 | |||
42 | if (actorCreated.type === 'Group') { // Video channel | ||
43 | const channel = await this.saveVideoChannel(actorCreated, t) | ||
44 | actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: this.ownerActor.Account }) | ||
45 | } | ||
46 | |||
47 | actorCreated.Server = server | ||
48 | |||
49 | return actorCreated | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | private async setServer (actor: MActor, t: Transaction) { | ||
54 | const actorHost = new URL(actor.url).host | ||
55 | |||
56 | const serverOptions = { | ||
57 | where: { | ||
58 | host: actorHost | ||
59 | }, | ||
60 | defaults: { | ||
61 | host: actorHost | ||
62 | }, | ||
63 | transaction: t | ||
64 | } | ||
65 | const [ server ] = await ServerModel.findOrCreate(serverOptions) | ||
66 | |||
67 | // Save our new account in database | ||
68 | actor.serverId = server.id | ||
69 | |||
70 | return server as MServer | ||
71 | } | ||
72 | |||
73 | private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { | ||
74 | const imageInfo = getImageInfoFromObject(this.actorObject, type) | ||
75 | if (!imageInfo) return | ||
76 | |||
77 | return updateActorImageInstance(actor as MActorImages, type, imageInfo, t) | ||
78 | } | ||
79 | |||
80 | private async saveActor (actor: MActor, t: Transaction) { | ||
81 | // Force the actor creation using findOrCreate() instead of save() | ||
82 | // Sometimes Sequelize skips the save() when it thinks the instance already exists | ||
83 | // (which could be false in a retried query) | ||
84 | const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ | ||
85 | defaults: actor.toJSON(), | ||
86 | where: { | ||
87 | [Op.or]: [ | ||
88 | { | ||
89 | url: actor.url | ||
90 | }, | ||
91 | { | ||
92 | serverId: actor.serverId, | ||
93 | preferredUsername: actor.preferredUsername | ||
94 | } | ||
95 | ] | ||
96 | }, | ||
97 | transaction: t | ||
98 | }) | ||
99 | |||
100 | return { actorCreated, created } | ||
101 | } | ||
102 | |||
103 | private async tryToFixActorUrlIfNeeded (actorCreated: MActor, newActor: MActor, created: boolean, t: Transaction) { | ||
104 | // Try to fix non HTTPS accounts of remote instances that fixed their URL afterwards | ||
105 | if (created !== true && actorCreated.url !== newActor.url) { | ||
106 | // Only fix http://example.com/account/djidane to https://example.com/account/djidane | ||
107 | if (actorCreated.url.replace(/^http:\/\//, '') !== newActor.url.replace(/^https:\/\//, '')) { | ||
108 | throw new Error(`Actor from DB with URL ${actorCreated.url} does not correspond to actor ${newActor.url}`) | ||
109 | } | ||
110 | |||
111 | actorCreated.url = newActor.url | ||
112 | await actorCreated.save({ transaction: t }) | ||
113 | } | ||
114 | } | ||
115 | |||
116 | private async saveAccount (actor: MActorId, t: Transaction) { | ||
117 | const [ accountCreated ] = await AccountModel.findOrCreate({ | ||
118 | defaults: { | ||
119 | name: getActorDisplayNameFromObject(this.actorObject), | ||
120 | description: this.actorObject.summary, | ||
121 | actorId: actor.id | ||
122 | }, | ||
123 | where: { | ||
124 | actorId: actor.id | ||
125 | }, | ||
126 | transaction: t | ||
127 | }) | ||
128 | |||
129 | return accountCreated as MAccount | ||
130 | } | ||
131 | |||
132 | private async saveVideoChannel (actor: MActorId, t: Transaction) { | ||
133 | const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({ | ||
134 | defaults: { | ||
135 | name: getActorDisplayNameFromObject(this.actorObject), | ||
136 | description: this.actorObject.summary, | ||
137 | support: this.actorObject.support, | ||
138 | actorId: actor.id, | ||
139 | accountId: this.ownerActor.Account.id | ||
140 | }, | ||
141 | where: { | ||
142 | actorId: actor.id | ||
143 | }, | ||
144 | transaction: t | ||
145 | }) | ||
146 | |||
147 | return videoChannelCreated as MChannel | ||
148 | } | ||
149 | } | ||
diff --git a/server/lib/activitypub/actors/shared/index.ts b/server/lib/activitypub/actors/shared/index.ts new file mode 100644 index 000000000..52af1a8e1 --- /dev/null +++ b/server/lib/activitypub/actors/shared/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './creator' | ||
2 | export * from './object-to-model-attributes' | ||
3 | export * from './url-to-object' | ||
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts new file mode 100644 index 000000000..1612b3ad0 --- /dev/null +++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts | |||
@@ -0,0 +1,70 @@ | |||
1 | import { getLowercaseExtension } from '@server/helpers/core-utils' | ||
2 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | ||
3 | import { buildUUID } from '@server/helpers/uuid' | ||
4 | import { MIMETYPES } from '@server/initializers/constants' | ||
5 | import { ActorModel } from '@server/models/actor/actor' | ||
6 | import { FilteredModelAttributes } from '@server/types' | ||
7 | import { ActivityPubActor, ActorImageType } from '@shared/models' | ||
8 | |||
9 | function getActorAttributesFromObject ( | ||
10 | actorObject: ActivityPubActor, | ||
11 | followersCount: number, | ||
12 | followingCount: number | ||
13 | ): FilteredModelAttributes<ActorModel> { | ||
14 | return { | ||
15 | type: actorObject.type, | ||
16 | preferredUsername: actorObject.preferredUsername, | ||
17 | url: actorObject.id, | ||
18 | publicKey: actorObject.publicKey.publicKeyPem, | ||
19 | privateKey: null, | ||
20 | followersCount, | ||
21 | followingCount, | ||
22 | inboxUrl: actorObject.inbox, | ||
23 | outboxUrl: actorObject.outbox, | ||
24 | followersUrl: actorObject.followers, | ||
25 | followingUrl: actorObject.following, | ||
26 | |||
27 | sharedInboxUrl: actorObject.endpoints?.sharedInbox | ||
28 | ? actorObject.endpoints.sharedInbox | ||
29 | : null | ||
30 | } | ||
31 | } | ||
32 | |||
33 | function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { | ||
34 | const mimetypes = MIMETYPES.IMAGE | ||
35 | const icon = type === ActorImageType.AVATAR | ||
36 | ? actorObject.icon | ||
37 | : actorObject.image | ||
38 | |||
39 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined | ||
40 | |||
41 | let extension: string | ||
42 | |||
43 | if (icon.mediaType) { | ||
44 | extension = mimetypes.MIMETYPE_EXT[icon.mediaType] | ||
45 | } else { | ||
46 | const tmp = getLowercaseExtension(icon.url) | ||
47 | |||
48 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp | ||
49 | } | ||
50 | |||
51 | if (!extension) return undefined | ||
52 | |||
53 | return { | ||
54 | name: buildUUID() + extension, | ||
55 | fileUrl: icon.url, | ||
56 | height: icon.height, | ||
57 | width: icon.width, | ||
58 | type | ||
59 | } | ||
60 | } | ||
61 | |||
62 | function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { | ||
63 | return actorObject.name || actorObject.preferredUsername | ||
64 | } | ||
65 | |||
66 | export { | ||
67 | getActorAttributesFromObject, | ||
68 | getImageInfoFromObject, | ||
69 | getActorDisplayNameFromObject | ||
70 | } | ||
diff --git a/server/lib/activitypub/actors/shared/url-to-object.ts b/server/lib/activitypub/actors/shared/url-to-object.ts new file mode 100644 index 000000000..f4f16b044 --- /dev/null +++ b/server/lib/activitypub/actors/shared/url-to-object.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | |||
2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | ||
3 | import { sanitizeAndCheckActorObject } from '@server/helpers/custom-validators/activitypub/actor' | ||
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { doJSONRequest } from '@server/helpers/requests' | ||
6 | import { ActivityPubActor, ActivityPubOrderedCollection } from '@shared/models' | ||
7 | |||
8 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode: number, actorObject: ActivityPubActor }> { | ||
9 | logger.info('Fetching remote actor %s.', actorUrl) | ||
10 | |||
11 | const { body, statusCode } = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true }) | ||
12 | |||
13 | if (sanitizeAndCheckActorObject(body) === false) { | ||
14 | logger.debug('Remote actor JSON is not valid.', { actorJSON: body }) | ||
15 | return { actorObject: undefined, statusCode: statusCode } | ||
16 | } | ||
17 | |||
18 | if (checkUrlsSameHost(body.id, actorUrl) !== true) { | ||
19 | logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, body.id) | ||
20 | return { actorObject: undefined, statusCode: statusCode } | ||
21 | } | ||
22 | |||
23 | return { | ||
24 | statusCode, | ||
25 | |||
26 | actorObject: body | ||
27 | } | ||
28 | } | ||
29 | |||
30 | async function fetchActorFollowsCount (actorObject: ActivityPubActor) { | ||
31 | const followersCount = await fetchActorTotalItems(actorObject.followers) | ||
32 | const followingCount = await fetchActorTotalItems(actorObject.following) | ||
33 | |||
34 | return { followersCount, followingCount } | ||
35 | } | ||
36 | |||
37 | // --------------------------------------------------------------------------- | ||
38 | export { | ||
39 | fetchActorFollowsCount, | ||
40 | fetchRemoteActor | ||
41 | } | ||
42 | |||
43 | // --------------------------------------------------------------------------- | ||
44 | |||
45 | async function fetchActorTotalItems (url: string) { | ||
46 | try { | ||
47 | const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true }) | ||
48 | |||
49 | return body.totalItems || 0 | ||
50 | } catch (err) { | ||
51 | logger.warn('Cannot fetch remote actor count %s.', url, { err }) | ||
52 | return 0 | ||
53 | } | ||
54 | } | ||
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts new file mode 100644 index 000000000..de5e03eee --- /dev/null +++ b/server/lib/activitypub/actors/updater.ts | |||
@@ -0,0 +1,90 @@ | |||
1 | import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
4 | import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' | ||
5 | import { ActivityPubActor, ActorImageType } from '@shared/models' | ||
6 | import { updateActorImageInstance } from './image' | ||
7 | import { fetchActorFollowsCount } from './shared' | ||
8 | import { getImageInfoFromObject } from './shared/object-to-model-attributes' | ||
9 | |||
10 | export class APActorUpdater { | ||
11 | |||
12 | private accountOrChannel: MAccount | MChannel | ||
13 | |||
14 | private readonly actorFieldsSave: object | ||
15 | private readonly accountOrChannelFieldsSave: object | ||
16 | |||
17 | constructor ( | ||
18 | private readonly actorObject: ActivityPubActor, | ||
19 | private readonly actor: MActorFull | ||
20 | ) { | ||
21 | this.actorFieldsSave = this.actor.toJSON() | ||
22 | |||
23 | if (this.actorObject.type === 'Group') this.accountOrChannel = this.actor.VideoChannel | ||
24 | else this.accountOrChannel = this.actor.Account | ||
25 | |||
26 | this.accountOrChannelFieldsSave = this.accountOrChannel.toJSON() | ||
27 | } | ||
28 | |||
29 | async update () { | ||
30 | const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR) | ||
31 | const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER) | ||
32 | |||
33 | try { | ||
34 | await this.updateActorInstance(this.actor, this.actorObject) | ||
35 | |||
36 | this.accountOrChannel.name = this.actorObject.name || this.actorObject.preferredUsername | ||
37 | this.accountOrChannel.description = this.actorObject.summary | ||
38 | |||
39 | if (this.accountOrChannel instanceof VideoChannelModel) this.accountOrChannel.support = this.actorObject.support | ||
40 | |||
41 | await runInReadCommittedTransaction(async t => { | ||
42 | await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t) | ||
43 | await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t) | ||
44 | }) | ||
45 | |||
46 | await runInReadCommittedTransaction(async t => { | ||
47 | await this.actor.save({ transaction: t }) | ||
48 | await this.accountOrChannel.save({ transaction: t }) | ||
49 | }) | ||
50 | |||
51 | logger.info('Remote account %s updated', this.actorObject.url) | ||
52 | } catch (err) { | ||
53 | if (this.actor !== undefined && this.actorFieldsSave !== undefined) { | ||
54 | resetSequelizeInstance(this.actor, this.actorFieldsSave) | ||
55 | } | ||
56 | |||
57 | if (this.accountOrChannel !== undefined && this.accountOrChannelFieldsSave !== undefined) { | ||
58 | resetSequelizeInstance(this.accountOrChannel, this.accountOrChannelFieldsSave) | ||
59 | } | ||
60 | |||
61 | // This is just a debug because we will retry the insert | ||
62 | logger.debug('Cannot update the remote account.', { err }) | ||
63 | throw err | ||
64 | } | ||
65 | } | ||
66 | |||
67 | private async updateActorInstance (actorInstance: MActor, actorObject: ActivityPubActor) { | ||
68 | const { followersCount, followingCount } = await fetchActorFollowsCount(actorObject) | ||
69 | |||
70 | actorInstance.type = actorObject.type | ||
71 | actorInstance.preferredUsername = actorObject.preferredUsername | ||
72 | actorInstance.url = actorObject.id | ||
73 | actorInstance.publicKey = actorObject.publicKey.publicKeyPem | ||
74 | actorInstance.followersCount = followersCount | ||
75 | actorInstance.followingCount = followingCount | ||
76 | actorInstance.inboxUrl = actorObject.inbox | ||
77 | actorInstance.outboxUrl = actorObject.outbox | ||
78 | actorInstance.followersUrl = actorObject.followers | ||
79 | actorInstance.followingUrl = actorObject.following | ||
80 | |||
81 | if (actorObject.published) actorInstance.remoteCreatedAt = new Date(actorObject.published) | ||
82 | |||
83 | if (actorObject.endpoints?.sharedInbox) { | ||
84 | actorInstance.sharedInboxUrl = actorObject.endpoints.sharedInbox | ||
85 | } | ||
86 | |||
87 | // Force actor update | ||
88 | actorInstance.changed('updatedAt', true) | ||
89 | } | ||
90 | } | ||
diff --git a/server/helpers/webfinger.ts b/server/lib/activitypub/actors/webfinger.ts index da7e88077..1c7ec4717 100644 --- a/server/helpers/webfinger.ts +++ b/server/lib/activitypub/actors/webfinger.ts | |||
@@ -1,16 +1,16 @@ | |||
1 | import * as WebFinger from 'webfinger.js' | 1 | import * as WebFinger from 'webfinger.js' |
2 | import { WebFingerData } from '../../shared' | 2 | import { isProdInstance } from '@server/helpers/core-utils' |
3 | import { ActorModel } from '../models/activitypub/actor' | 3 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' |
4 | import { isTestInstance } from './core-utils' | 4 | import { REQUEST_TIMEOUT, WEBSERVER } from '@server/initializers/constants' |
5 | import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' | 5 | import { ActorModel } from '@server/models/actor/actor' |
6 | import { WEBSERVER } from '../initializers/constants' | 6 | import { MActorFull } from '@server/types/models' |
7 | import { MActorFull } from '../types/models' | 7 | import { WebFingerData } from '@shared/models' |
8 | 8 | ||
9 | const webfinger = new WebFinger({ | 9 | const webfinger = new WebFinger({ |
10 | webfist_fallback: false, | 10 | webfist_fallback: false, |
11 | tls_only: isTestInstance(), | 11 | tls_only: isProdInstance(), |
12 | uri_fallback: false, | 12 | uri_fallback: false, |
13 | request_timeout: 3000 | 13 | request_timeout: REQUEST_TIMEOUT |
14 | }) | 14 | }) |
15 | 15 | ||
16 | async function loadActorUrlOrGetFromWebfinger (uriArg: string) { | 16 | async function loadActorUrlOrGetFromWebfinger (uriArg: string) { |
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts index 2986714d3..d0558f191 100644 --- a/server/lib/activitypub/audience.ts +++ b/server/lib/activitypub/audience.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { ActivityAudience } from '../../../shared/models/activitypub' | 2 | import { ActivityAudience } from '../../../shared/models/activitypub' |
3 | import { ACTIVITY_PUB } from '../../initializers/constants' | 3 | import { ACTIVITY_PUB } from '../../initializers/constants' |
4 | import { ActorModel } from '../../models/activitypub/actor' | 4 | import { ActorModel } from '../../models/actor/actor' |
5 | import { VideoModel } from '../../models/video/video' | 5 | import { VideoModel } from '../../models/video/video' |
6 | import { VideoShareModel } from '../../models/video/video-share' | 6 | import { VideoShareModel } from '../../models/video/video-share' |
7 | import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models' | 7 | import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models' |
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index 2e6dd34e0..a16d2cd93 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -1,54 +1,27 @@ | |||
1 | import { CacheFileObject } from '../../../shared/index' | ||
2 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | ||
3 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
4 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models' | 2 | import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models' |
3 | import { CacheFileObject } from '../../../shared/index' | ||
4 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | ||
6 | 6 | ||
7 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId) { | 7 | async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { |
8 | 8 | const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) | |
9 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { | ||
10 | const url = cacheFileObject.url | ||
11 | |||
12 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
14 | 9 | ||
15 | return { | 10 | if (redundancyModel) { |
16 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | 11 | return updateCacheFile(cacheFileObject, redundancyModel, video, byActor, t) |
17 | url: cacheFileObject.id, | ||
18 | fileUrl: url.href, | ||
19 | strategy: null, | ||
20 | videoStreamingPlaylistId: playlist.id, | ||
21 | actorId: byActor.id | ||
22 | } | ||
23 | } | 12 | } |
24 | 13 | ||
25 | const url = cacheFileObject.url | 14 | return createCacheFile(cacheFileObject, video, byActor, t) |
26 | const videoFile = video.VideoFiles.find(f => { | ||
27 | return f.resolution === url.height && f.fps === url.fps | ||
28 | }) | ||
29 | |||
30 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) | ||
31 | |||
32 | return { | ||
33 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | ||
34 | url: cacheFileObject.id, | ||
35 | fileUrl: url.href, | ||
36 | strategy: null, | ||
37 | videoFileId: videoFile.id, | ||
38 | actorId: byActor.id | ||
39 | } | ||
40 | } | 15 | } |
41 | 16 | ||
42 | async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { | 17 | // --------------------------------------------------------------------------- |
43 | const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) | ||
44 | 18 | ||
45 | if (!redundancyModel) { | 19 | export { |
46 | await createCacheFile(cacheFileObject, video, byActor, t) | 20 | createOrUpdateCacheFile |
47 | } else { | ||
48 | await updateCacheFile(cacheFileObject, redundancyModel, video, byActor, t) | ||
49 | } | ||
50 | } | 21 | } |
51 | 22 | ||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
52 | function createCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { | 25 | function createCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { |
53 | const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor) | 26 | const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor) |
54 | 27 | ||
@@ -74,9 +47,37 @@ function updateCacheFile ( | |||
74 | return redundancyModel.save({ transaction: t }) | 47 | return redundancyModel.save({ transaction: t }) |
75 | } | 48 | } |
76 | 49 | ||
77 | export { | 50 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId) { |
78 | createOrUpdateCacheFile, | 51 | |
79 | createCacheFile, | 52 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { |
80 | updateCacheFile, | 53 | const url = cacheFileObject.url |
81 | cacheFileActivityObjectToDBAttributes | 54 | |
55 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
56 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
57 | |||
58 | return { | ||
59 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | ||
60 | url: cacheFileObject.id, | ||
61 | fileUrl: url.href, | ||
62 | strategy: null, | ||
63 | videoStreamingPlaylistId: playlist.id, | ||
64 | actorId: byActor.id | ||
65 | } | ||
66 | } | ||
67 | |||
68 | const url = cacheFileObject.url | ||
69 | const videoFile = video.VideoFiles.find(f => { | ||
70 | return f.resolution === url.height && f.fps === url.fps | ||
71 | }) | ||
72 | |||
73 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) | ||
74 | |||
75 | return { | ||
76 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | ||
77 | url: cacheFileObject.id, | ||
78 | fileUrl: url.href, | ||
79 | strategy: null, | ||
80 | videoFileId: videoFile.id, | ||
81 | actorId: byActor.id | ||
82 | } | ||
82 | } | 83 | } |
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index 278abf7de..cd117f571 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts | |||
@@ -3,7 +3,7 @@ import { URL } from 'url' | |||
3 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 3 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { doJSONRequest } from '../../helpers/requests' | 5 | import { doJSONRequest } from '../../helpers/requests' |
6 | import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants' | 6 | import { ACTIVITY_PUB, WEBSERVER } from '../../initializers/constants' |
7 | 7 | ||
8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) | 8 | type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) |
9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) | 9 | type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) |
@@ -13,10 +13,7 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction | |||
13 | 13 | ||
14 | logger.info('Crawling ActivityPub data on %s.', url) | 14 | logger.info('Crawling ActivityPub data on %s.', url) |
15 | 15 | ||
16 | const options = { | 16 | const options = { activityPub: true } |
17 | activityPub: true, | ||
18 | timeout: REQUEST_TIMEOUT | ||
19 | } | ||
20 | 17 | ||
21 | const startDate = new Date() | 18 | const startDate = new Date() |
22 | 19 | ||
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts index 351499bd1..c1bd667e0 100644 --- a/server/lib/activitypub/follow.ts +++ b/server/lib/activitypub/follow.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import { MActorFollowActors } from '../../types/models' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { logger } from '../../helpers/logger' | ||
2 | import { CONFIG } from '../../initializers/config' | 4 | import { CONFIG } from '../../initializers/config' |
3 | import { SERVER_ACTOR_NAME } from '../../initializers/constants' | 5 | import { SERVER_ACTOR_NAME } from '../../initializers/constants' |
4 | import { JobQueue } from '../job-queue' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { ServerModel } from '../../models/server/server' | 6 | import { ServerModel } from '../../models/server/server' |
7 | import { getServerActor } from '@server/models/application/application' | 7 | import { MActorFollowActors } from '../../types/models' |
8 | import { JobQueue } from '../job-queue' | ||
8 | 9 | ||
9 | async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { | 10 | async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors, transaction?: Transaction) { |
10 | if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return | 11 | if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return |
11 | 12 | ||
12 | const follower = actorFollow.ActorFollower | 13 | const follower = actorFollow.ActorFollower |
@@ -16,7 +17,7 @@ async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { | |||
16 | 17 | ||
17 | const me = await getServerActor() | 18 | const me = await getServerActor() |
18 | 19 | ||
19 | const server = await ServerModel.load(follower.serverId) | 20 | const server = await ServerModel.load(follower.serverId, transaction) |
20 | const host = server.host | 21 | const host = server.host |
21 | 22 | ||
22 | const payload = { | 23 | const payload = { |
diff --git a/server/lib/activitypub/outbox.ts b/server/lib/activitypub/outbox.ts new file mode 100644 index 000000000..ecdc33a77 --- /dev/null +++ b/server/lib/activitypub/outbox.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { ActorModel } from '@server/models/actor/actor' | ||
3 | import { getServerActor } from '@server/models/application/application' | ||
4 | import { JobQueue } from '../job-queue' | ||
5 | |||
6 | async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) { | ||
7 | // Don't fetch ourselves | ||
8 | const serverActor = await getServerActor() | ||
9 | if (serverActor.id === actor.id) { | ||
10 | logger.error('Cannot fetch our own outbox!') | ||
11 | return undefined | ||
12 | } | ||
13 | |||
14 | const payload = { | ||
15 | uri: actor.outboxUrl, | ||
16 | type: 'activity' as 'activity' | ||
17 | } | ||
18 | |||
19 | return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) | ||
20 | } | ||
21 | |||
22 | export { | ||
23 | addFetchOutboxJob | ||
24 | } | ||
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts deleted file mode 100644 index 7166c68a6..000000000 --- a/server/lib/activitypub/playlist.ts +++ /dev/null | |||
@@ -1,204 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
4 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
5 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
6 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
7 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
8 | import { isArray } from '../../helpers/custom-validators/misc' | ||
9 | import { logger } from '../../helpers/logger' | ||
10 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' | ||
11 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../../initializers/database' | ||
13 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | ||
14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | ||
15 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' | ||
16 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' | ||
17 | import { FilteredModelAttributes } from '../../types/sequelize' | ||
18 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
19 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
20 | import { crawlCollectionPage } from './crawl' | ||
21 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | ||
22 | |||
23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | ||
24 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | ||
25 | ? VideoPlaylistPrivacy.PUBLIC | ||
26 | : VideoPlaylistPrivacy.UNLISTED | ||
27 | |||
28 | return { | ||
29 | name: playlistObject.name, | ||
30 | description: playlistObject.content, | ||
31 | privacy, | ||
32 | url: playlistObject.id, | ||
33 | uuid: playlistObject.uuid, | ||
34 | ownerAccountId: byAccount.id, | ||
35 | videoChannelId: null, | ||
36 | createdAt: new Date(playlistObject.published), | ||
37 | updatedAt: new Date(playlistObject.updated) | ||
38 | } | ||
39 | } | ||
40 | |||
41 | function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) { | ||
42 | return { | ||
43 | position: elementObject.position, | ||
44 | url: elementObject.id, | ||
45 | startTimestamp: elementObject.startTimestamp || null, | ||
46 | stopTimestamp: elementObject.stopTimestamp || null, | ||
47 | videoPlaylistId: videoPlaylist.id, | ||
48 | videoId: video.id | ||
49 | } | ||
50 | } | ||
51 | |||
52 | async function createAccountPlaylists (playlistUrls: string[], account: MAccountDefault) { | ||
53 | await Bluebird.map(playlistUrls, async playlistUrl => { | ||
54 | try { | ||
55 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) | ||
56 | if (exists === true) return | ||
57 | |||
58 | // Fetch url | ||
59 | const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true }) | ||
60 | |||
61 | if (!isPlaylistObjectValid(body)) { | ||
62 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) | ||
63 | } | ||
64 | |||
65 | if (!isArray(body.to)) { | ||
66 | throw new Error('Playlist does not have an audience.') | ||
67 | } | ||
68 | |||
69 | return createOrUpdateVideoPlaylist(body, account, body.to) | ||
70 | } catch (err) { | ||
71 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err }) | ||
72 | } | ||
73 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
74 | } | ||
75 | |||
76 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | ||
77 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to) | ||
78 | |||
79 | if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) { | ||
80 | const actor = await getOrCreateActorAndServerAndModel(playlistObject.attributedTo[0]) | ||
81 | |||
82 | if (actor.VideoChannel) { | ||
83 | playlistAttributes.videoChannelId = actor.VideoChannel.id | ||
84 | } else { | ||
85 | logger.warn('Attributed to of video playlist %s is not a video channel.', playlistObject.id, { playlistObject }) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | const [ playlist ] = await VideoPlaylistModel.upsert<MVideoPlaylist>(playlistAttributes, { returning: true }) | ||
90 | |||
91 | let accItems: string[] = [] | ||
92 | await crawlCollectionPage<string>(playlistObject.id, items => { | ||
93 | accItems = accItems.concat(items) | ||
94 | |||
95 | return Promise.resolve() | ||
96 | }) | ||
97 | |||
98 | const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null) | ||
99 | |||
100 | if (playlistObject.icon) { | ||
101 | try { | ||
102 | const thumbnailModel = await createPlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist: refreshedPlaylist }) | ||
103 | await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined) | ||
104 | } catch (err) { | ||
105 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) | ||
106 | } | ||
107 | } else if (refreshedPlaylist.hasThumbnail()) { | ||
108 | await refreshedPlaylist.Thumbnail.destroy() | ||
109 | refreshedPlaylist.Thumbnail = null | ||
110 | } | ||
111 | |||
112 | return resetVideoPlaylistElements(accItems, refreshedPlaylist) | ||
113 | } | ||
114 | |||
115 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> { | ||
116 | if (!videoPlaylist.isOutdated()) return videoPlaylist | ||
117 | |||
118 | try { | ||
119 | const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) | ||
120 | |||
121 | if (playlistObject === undefined) { | ||
122 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) | ||
123 | |||
124 | await videoPlaylist.setAsRefreshed() | ||
125 | return videoPlaylist | ||
126 | } | ||
127 | |||
128 | const byAccount = videoPlaylist.OwnerAccount | ||
129 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, playlistObject.to) | ||
130 | |||
131 | return videoPlaylist | ||
132 | } catch (err) { | ||
133 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
134 | logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url) | ||
135 | |||
136 | await videoPlaylist.destroy() | ||
137 | return undefined | ||
138 | } | ||
139 | |||
140 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) | ||
141 | |||
142 | await videoPlaylist.setAsRefreshed() | ||
143 | return videoPlaylist | ||
144 | } | ||
145 | } | ||
146 | |||
147 | // --------------------------------------------------------------------------- | ||
148 | |||
149 | export { | ||
150 | createAccountPlaylists, | ||
151 | playlistObjectToDBAttributes, | ||
152 | playlistElementObjectToDBAttributes, | ||
153 | createOrUpdateVideoPlaylist, | ||
154 | refreshVideoPlaylistIfNeeded | ||
155 | } | ||
156 | |||
157 | // --------------------------------------------------------------------------- | ||
158 | |||
159 | async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) { | ||
160 | const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = [] | ||
161 | |||
162 | await Bluebird.map(elementUrls, async elementUrl => { | ||
163 | try { | ||
164 | const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true }) | ||
165 | |||
166 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) | ||
167 | |||
168 | if (checkUrlsSameHost(body.id, elementUrl) !== true) { | ||
169 | throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`) | ||
170 | } | ||
171 | |||
172 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: { id: body.url }, fetchType: 'only-video' }) | ||
173 | |||
174 | elementsToCreate.push(playlistElementObjectToDBAttributes(body, playlist, video)) | ||
175 | } catch (err) { | ||
176 | logger.warn('Cannot add playlist element %s.', elementUrl, { err }) | ||
177 | } | ||
178 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
179 | |||
180 | await sequelizeTypescript.transaction(async t => { | ||
181 | await VideoPlaylistElementModel.deleteAllOf(playlist.id, t) | ||
182 | |||
183 | for (const element of elementsToCreate) { | ||
184 | await VideoPlaylistElementModel.create(element, { transaction: t }) | ||
185 | } | ||
186 | }) | ||
187 | |||
188 | logger.info('Reset playlist %s with %s elements.', playlist.url, elementsToCreate.length) | ||
189 | |||
190 | return undefined | ||
191 | } | ||
192 | |||
193 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | ||
194 | logger.info('Fetching remote playlist %s.', playlistUrl) | ||
195 | |||
196 | const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true }) | ||
197 | |||
198 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { | ||
199 | logger.debug('Remote video playlist JSON is not valid.', { body }) | ||
200 | return { statusCode, playlistObject: undefined } | ||
201 | } | ||
202 | |||
203 | return { statusCode, playlistObject: body } | ||
204 | } | ||
diff --git a/server/lib/activitypub/playlists/create-update.ts b/server/lib/activitypub/playlists/create-update.ts new file mode 100644 index 000000000..ea3e61ac5 --- /dev/null +++ b/server/lib/activitypub/playlists/create-update.ts | |||
@@ -0,0 +1,156 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { getAPId } from '@server/helpers/activitypub' | ||
3 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
5 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' | ||
6 | import { sequelizeTypescript } from '@server/initializers/database' | ||
7 | import { updatePlaylistMiniatureFromUrl } from '@server/lib/thumbnail' | ||
8 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
9 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | ||
10 | import { FilteredModelAttributes } from '@server/types' | ||
11 | import { MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models' | ||
12 | import { AttributesOnly } from '@shared/core-utils' | ||
13 | import { PlaylistObject } from '@shared/models' | ||
14 | import { getOrCreateAPActor } from '../actors' | ||
15 | import { crawlCollectionPage } from '../crawl' | ||
16 | import { getOrCreateAPVideo } from '../videos' | ||
17 | import { | ||
18 | fetchRemotePlaylistElement, | ||
19 | fetchRemoteVideoPlaylist, | ||
20 | playlistElementObjectToDBAttributes, | ||
21 | playlistObjectToDBAttributes | ||
22 | } from './shared' | ||
23 | |||
24 | const lTags = loggerTagsFactory('ap', 'video-playlist') | ||
25 | |||
26 | async function createAccountPlaylists (playlistUrls: string[]) { | ||
27 | await Bluebird.map(playlistUrls, async playlistUrl => { | ||
28 | try { | ||
29 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) | ||
30 | if (exists === true) return | ||
31 | |||
32 | const { playlistObject } = await fetchRemoteVideoPlaylist(playlistUrl) | ||
33 | |||
34 | if (playlistObject === undefined) { | ||
35 | throw new Error(`Cannot refresh remote playlist ${playlistUrl}: invalid body.`) | ||
36 | } | ||
37 | |||
38 | return createOrUpdateVideoPlaylist(playlistObject) | ||
39 | } catch (err) { | ||
40 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err, ...lTags(playlistUrl) }) | ||
41 | } | ||
42 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
43 | } | ||
44 | |||
45 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, to?: string[]) { | ||
46 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to) | ||
47 | |||
48 | await setVideoChannel(playlistObject, playlistAttributes) | ||
49 | |||
50 | const [ upsertPlaylist ] = await VideoPlaylistModel.upsert<MVideoPlaylistVideosLength>(playlistAttributes, { returning: true }) | ||
51 | |||
52 | const playlistElementUrls = await fetchElementUrls(playlistObject) | ||
53 | |||
54 | // Refetch playlist from DB since elements fetching could be long in time | ||
55 | const playlist = await VideoPlaylistModel.loadWithAccountAndChannel(upsertPlaylist.id, null) | ||
56 | |||
57 | await updatePlaylistThumbnail(playlistObject, playlist) | ||
58 | |||
59 | const elementsLength = await rebuildVideoPlaylistElements(playlistElementUrls, playlist) | ||
60 | playlist.setVideosLength(elementsLength) | ||
61 | |||
62 | return playlist | ||
63 | } | ||
64 | |||
65 | // --------------------------------------------------------------------------- | ||
66 | |||
67 | export { | ||
68 | createAccountPlaylists, | ||
69 | createOrUpdateVideoPlaylist | ||
70 | } | ||
71 | |||
72 | // --------------------------------------------------------------------------- | ||
73 | |||
74 | async function setVideoChannel (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) { | ||
75 | if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) { | ||
76 | throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject)) | ||
77 | } | ||
78 | |||
79 | const actor = await getOrCreateAPActor(playlistObject.attributedTo[0], 'all') | ||
80 | |||
81 | if (!actor.VideoChannel) { | ||
82 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) | ||
83 | return | ||
84 | } | ||
85 | |||
86 | playlistAttributes.videoChannelId = actor.VideoChannel.id | ||
87 | playlistAttributes.ownerAccountId = actor.VideoChannel.Account.id | ||
88 | } | ||
89 | |||
90 | async function fetchElementUrls (playlistObject: PlaylistObject) { | ||
91 | let accItems: string[] = [] | ||
92 | await crawlCollectionPage<string>(playlistObject.id, items => { | ||
93 | accItems = accItems.concat(items) | ||
94 | |||
95 | return Promise.resolve() | ||
96 | }) | ||
97 | |||
98 | return accItems | ||
99 | } | ||
100 | |||
101 | async function updatePlaylistThumbnail (playlistObject: PlaylistObject, playlist: MVideoPlaylistFull) { | ||
102 | if (playlistObject.icon) { | ||
103 | let thumbnailModel: MThumbnail | ||
104 | |||
105 | try { | ||
106 | thumbnailModel = await updatePlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist }) | ||
107 | await playlist.setAndSaveThumbnail(thumbnailModel, undefined) | ||
108 | } catch (err) { | ||
109 | logger.warn('Cannot set thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) }) | ||
110 | |||
111 | if (thumbnailModel) await thumbnailModel.removeThumbnail() | ||
112 | } | ||
113 | |||
114 | return | ||
115 | } | ||
116 | |||
117 | // Playlist does not have an icon, destroy existing one | ||
118 | if (playlist.hasThumbnail()) { | ||
119 | await playlist.Thumbnail.destroy() | ||
120 | playlist.Thumbnail = null | ||
121 | } | ||
122 | } | ||
123 | |||
124 | async function rebuildVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) { | ||
125 | const elementsToCreate = await buildElementsDBAttributes(elementUrls, playlist) | ||
126 | |||
127 | await sequelizeTypescript.transaction(async t => { | ||
128 | await VideoPlaylistElementModel.deleteAllOf(playlist.id, t) | ||
129 | |||
130 | for (const element of elementsToCreate) { | ||
131 | await VideoPlaylistElementModel.create(element, { transaction: t }) | ||
132 | } | ||
133 | }) | ||
134 | |||
135 | logger.info('Rebuilt playlist %s with %s elements.', playlist.url, elementsToCreate.length, lTags(playlist.uuid, playlist.url)) | ||
136 | |||
137 | return elementsToCreate.length | ||
138 | } | ||
139 | |||
140 | async function buildElementsDBAttributes (elementUrls: string[], playlist: MVideoPlaylist) { | ||
141 | const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = [] | ||
142 | |||
143 | await Bluebird.map(elementUrls, async elementUrl => { | ||
144 | try { | ||
145 | const { elementObject } = await fetchRemotePlaylistElement(elementUrl) | ||
146 | |||
147 | const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video' }) | ||
148 | |||
149 | elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video)) | ||
150 | } catch (err) { | ||
151 | logger.warn('Cannot add playlist element %s.', elementUrl, { err, ...lTags(playlist.uuid, playlist.url) }) | ||
152 | } | ||
153 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
154 | |||
155 | return elementsToCreate | ||
156 | } | ||
diff --git a/server/lib/activitypub/playlists/get.ts b/server/lib/activitypub/playlists/get.ts new file mode 100644 index 000000000..2c19c503a --- /dev/null +++ b/server/lib/activitypub/playlists/get.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { getAPId } from '@server/helpers/activitypub' | ||
2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
3 | import { MVideoPlaylistFullSummary } from '@server/types/models' | ||
4 | import { APObject } from '@shared/models' | ||
5 | import { createOrUpdateVideoPlaylist } from './create-update' | ||
6 | import { scheduleRefreshIfNeeded } from './refresh' | ||
7 | import { fetchRemoteVideoPlaylist } from './shared' | ||
8 | |||
9 | async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObject): Promise<MVideoPlaylistFullSummary> { | ||
10 | const playlistUrl = getAPId(playlistObjectArg) | ||
11 | |||
12 | const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) | ||
13 | |||
14 | if (playlistFromDatabase) { | ||
15 | scheduleRefreshIfNeeded(playlistFromDatabase) | ||
16 | |||
17 | return playlistFromDatabase | ||
18 | } | ||
19 | |||
20 | const { playlistObject } = await fetchRemoteVideoPlaylist(playlistUrl) | ||
21 | if (!playlistObject) throw new Error('Cannot fetch remote playlist with url: ' + playlistUrl) | ||
22 | |||
23 | // playlistUrl is just an alias/rediraction, so process object id instead | ||
24 | if (playlistObject.id !== playlistUrl) return getOrCreateAPVideoPlaylist(playlistObject) | ||
25 | |||
26 | const playlistCreated = await createOrUpdateVideoPlaylist(playlistObject) | ||
27 | |||
28 | return playlistCreated | ||
29 | } | ||
30 | |||
31 | // --------------------------------------------------------------------------- | ||
32 | |||
33 | export { | ||
34 | getOrCreateAPVideoPlaylist | ||
35 | } | ||
diff --git a/server/lib/activitypub/playlists/index.ts b/server/lib/activitypub/playlists/index.ts new file mode 100644 index 000000000..e2470a674 --- /dev/null +++ b/server/lib/activitypub/playlists/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './get' | ||
2 | export * from './create-update' | ||
3 | export * from './refresh' | ||
diff --git a/server/lib/activitypub/playlists/refresh.ts b/server/lib/activitypub/playlists/refresh.ts new file mode 100644 index 000000000..ef3cb3fe4 --- /dev/null +++ b/server/lib/activitypub/playlists/refresh.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
2 | import { PeerTubeRequestError } from '@server/helpers/requests' | ||
3 | import { JobQueue } from '@server/lib/job-queue' | ||
4 | import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models' | ||
5 | import { HttpStatusCode } from '@shared/core-utils' | ||
6 | import { createOrUpdateVideoPlaylist } from './create-update' | ||
7 | import { fetchRemoteVideoPlaylist } from './shared' | ||
8 | |||
9 | function scheduleRefreshIfNeeded (playlist: MVideoPlaylist) { | ||
10 | if (!playlist.isOutdated()) return | ||
11 | |||
12 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: playlist.url } }) | ||
13 | } | ||
14 | |||
15 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> { | ||
16 | if (!videoPlaylist.isOutdated()) return videoPlaylist | ||
17 | |||
18 | const lTags = loggerTagsFactory('ap', 'video-playlist', 'refresh', videoPlaylist.uuid, videoPlaylist.url) | ||
19 | |||
20 | logger.info('Refreshing playlist %s.', videoPlaylist.url, lTags()) | ||
21 | |||
22 | try { | ||
23 | const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) | ||
24 | |||
25 | if (playlistObject === undefined) { | ||
26 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url, lTags()) | ||
27 | |||
28 | await videoPlaylist.setAsRefreshed() | ||
29 | return videoPlaylist | ||
30 | } | ||
31 | |||
32 | await createOrUpdateVideoPlaylist(playlistObject) | ||
33 | |||
34 | return videoPlaylist | ||
35 | } catch (err) { | ||
36 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
37 | logger.info('Cannot refresh not existing playlist %s. Deleting it.', videoPlaylist.url, lTags()) | ||
38 | |||
39 | await videoPlaylist.destroy() | ||
40 | return undefined | ||
41 | } | ||
42 | |||
43 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err, ...lTags() }) | ||
44 | |||
45 | await videoPlaylist.setAsRefreshed() | ||
46 | return videoPlaylist | ||
47 | } | ||
48 | } | ||
49 | |||
50 | export { | ||
51 | scheduleRefreshIfNeeded, | ||
52 | refreshVideoPlaylistIfNeeded | ||
53 | } | ||
diff --git a/server/lib/activitypub/playlists/shared/index.ts b/server/lib/activitypub/playlists/shared/index.ts new file mode 100644 index 000000000..a217f2291 --- /dev/null +++ b/server/lib/activitypub/playlists/shared/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './object-to-model-attributes' | ||
2 | export * from './url-to-object' | ||
diff --git a/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts b/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts new file mode 100644 index 000000000..70fd335bc --- /dev/null +++ b/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { ACTIVITY_PUB } from '@server/initializers/constants' | ||
2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
3 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | ||
4 | import { MVideoId, MVideoPlaylistId } from '@server/types/models' | ||
5 | import { AttributesOnly } from '@shared/core-utils' | ||
6 | import { PlaylistElementObject, PlaylistObject, VideoPlaylistPrivacy } from '@shared/models' | ||
7 | |||
8 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) { | ||
9 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | ||
10 | ? VideoPlaylistPrivacy.PUBLIC | ||
11 | : VideoPlaylistPrivacy.UNLISTED | ||
12 | |||
13 | return { | ||
14 | name: playlistObject.name, | ||
15 | description: playlistObject.content, | ||
16 | privacy, | ||
17 | url: playlistObject.id, | ||
18 | uuid: playlistObject.uuid, | ||
19 | ownerAccountId: null, | ||
20 | videoChannelId: null, | ||
21 | createdAt: new Date(playlistObject.published), | ||
22 | updatedAt: new Date(playlistObject.updated) | ||
23 | } as AttributesOnly<VideoPlaylistModel> | ||
24 | } | ||
25 | |||
26 | function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) { | ||
27 | return { | ||
28 | position: elementObject.position, | ||
29 | url: elementObject.id, | ||
30 | startTimestamp: elementObject.startTimestamp || null, | ||
31 | stopTimestamp: elementObject.stopTimestamp || null, | ||
32 | videoPlaylistId: videoPlaylist.id, | ||
33 | videoId: video.id | ||
34 | } as AttributesOnly<VideoPlaylistElementModel> | ||
35 | } | ||
36 | |||
37 | export { | ||
38 | playlistObjectToDBAttributes, | ||
39 | playlistElementObjectToDBAttributes | ||
40 | } | ||
diff --git a/server/lib/activitypub/playlists/shared/url-to-object.ts b/server/lib/activitypub/playlists/shared/url-to-object.ts new file mode 100644 index 000000000..ec8c01255 --- /dev/null +++ b/server/lib/activitypub/playlists/shared/url-to-object.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import { isArray } from 'lodash' | ||
2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | ||
3 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '@server/helpers/custom-validators/activitypub/playlist' | ||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
5 | import { doJSONRequest } from '@server/helpers/requests' | ||
6 | import { PlaylistElementObject, PlaylistObject } from '@shared/models' | ||
7 | |||
8 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | ||
9 | const lTags = loggerTagsFactory('ap', 'video-playlist', playlistUrl) | ||
10 | |||
11 | logger.info('Fetching remote playlist %s.', playlistUrl, lTags()) | ||
12 | |||
13 | const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true }) | ||
14 | |||
15 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { | ||
16 | logger.debug('Remote video playlist JSON is not valid.', { body, ...lTags() }) | ||
17 | return { statusCode, playlistObject: undefined } | ||
18 | } | ||
19 | |||
20 | if (!isArray(body.to)) { | ||
21 | logger.debug('Remote video playlist JSON does not have a valid audience.', { body, ...lTags() }) | ||
22 | return { statusCode, playlistObject: undefined } | ||
23 | } | ||
24 | |||
25 | return { statusCode, playlistObject: body } | ||
26 | } | ||
27 | |||
28 | async function fetchRemotePlaylistElement (elementUrl: string): Promise<{ statusCode: number, elementObject: PlaylistElementObject }> { | ||
29 | const lTags = loggerTagsFactory('ap', 'video-playlist', 'element', elementUrl) | ||
30 | |||
31 | logger.debug('Fetching remote playlist element %s.', elementUrl, lTags()) | ||
32 | |||
33 | const { body, statusCode } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true }) | ||
34 | |||
35 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in fetch playlist element ${elementUrl}`) | ||
36 | |||
37 | if (checkUrlsSameHost(body.id, elementUrl) !== true) { | ||
38 | throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`) | ||
39 | } | ||
40 | |||
41 | return { statusCode, elementObject: body } | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | fetchRemoteVideoPlaylist, | ||
46 | fetchRemotePlaylistElement | ||
47 | } | ||
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts index 1799829f8..077b01eda 100644 --- a/server/lib/activitypub/process/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { ActivityAccept } from '../../../../shared/models/activitypub' | 1 | import { ActivityAccept } from '../../../../shared/models/activitypub' |
2 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 2 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
3 | import { addFetchOutboxJob } from '../actor' | ||
4 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 3 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
5 | import { MActorDefault, MActorSignature } from '../../../types/models' | 4 | import { MActorDefault, MActorSignature } from '../../../types/models' |
5 | import { addFetchOutboxJob } from '../outbox' | ||
6 | 6 | ||
7 | async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) { | 7 | async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) { |
8 | const { byActor: targetActor, inboxActor } = options | 8 | const { byActor: targetActor, inboxActor } = options |
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts index 63082466e..ec23c705e 100644 --- a/server/lib/activitypub/process/process-announce.ts +++ b/server/lib/activitypub/process/process-announce.ts | |||
@@ -3,7 +3,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
3 | import { sequelizeTypescript } from '../../../initializers/database' | 3 | import { sequelizeTypescript } from '../../../initializers/database' |
4 | import { VideoShareModel } from '../../../models/video/video-share' | 4 | import { VideoShareModel } from '../../../models/video/video-share' |
5 | import { forwardVideoRelatedActivity } from '../send/utils' | 5 | import { forwardVideoRelatedActivity } from '../send/utils' |
6 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 6 | import { getOrCreateAPVideo } from '../videos' |
7 | import { Notifier } from '../../notifier' | 7 | import { Notifier } from '../../notifier' |
8 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
@@ -32,7 +32,7 @@ async function processVideoShare (actorAnnouncer: MActorSignature, activity: Act | |||
32 | let videoCreated: boolean | 32 | let videoCreated: boolean |
33 | 33 | ||
34 | try { | 34 | try { |
35 | const result = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri }) | 35 | const result = await getOrCreateAPVideo({ videoObject: objectUri }) |
36 | video = result.video | 36 | video = result.video |
37 | videoCreated = result.created | 37 | videoCreated = result.created |
38 | } catch (err) { | 38 | } catch (err) { |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 9cded4dec..70e048d6e 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 2 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
2 | import { ActivityCreate, CacheFileObject, VideoObject } from '../../../../shared' | 3 | import { ActivityCreate, CacheFileObject, VideoObject } from '../../../../shared' |
3 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' |
@@ -9,11 +10,10 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' | |||
9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 10 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
10 | import { Notifier } from '../../notifier' | 11 | import { Notifier } from '../../notifier' |
11 | import { createOrUpdateCacheFile } from '../cache-file' | 12 | import { createOrUpdateCacheFile } from '../cache-file' |
12 | import { createOrUpdateVideoPlaylist } from '../playlist' | 13 | import { createOrUpdateVideoPlaylist } from '../playlists' |
13 | import { forwardVideoRelatedActivity } from '../send/utils' | 14 | import { forwardVideoRelatedActivity } from '../send/utils' |
14 | import { resolveThread } from '../video-comments' | 15 | import { resolveThread } from '../video-comments' |
15 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 16 | import { getOrCreateAPVideo } from '../videos' |
16 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
17 | 17 | ||
18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { |
19 | const { activity, byActor } = options | 19 | const { activity, byActor } = options |
@@ -55,7 +55,7 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) { | |||
55 | const videoToCreateData = activity.object as VideoObject | 55 | const videoToCreateData = activity.object as VideoObject |
56 | 56 | ||
57 | const syncParam = { likes: false, dislikes: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } | 57 | const syncParam = { likes: false, dislikes: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } |
58 | const { video, created } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData, syncParam }) | 58 | const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) |
59 | 59 | ||
60 | if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) | 60 | if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video) |
61 | 61 | ||
@@ -67,7 +67,7 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor | |||
67 | 67 | ||
68 | const cacheFile = activity.object as CacheFileObject | 68 | const cacheFile = activity.object as CacheFileObject |
69 | 69 | ||
70 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) | 70 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) |
71 | 71 | ||
72 | await sequelizeTypescript.transaction(async t => { | 72 | await sequelizeTypescript.transaction(async t => { |
73 | return createOrUpdateCacheFile(cacheFile, video, byActor, t) | 73 | return createOrUpdateCacheFile(cacheFile, video, byActor, t) |
@@ -128,5 +128,5 @@ async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorS | |||
128 | 128 | ||
129 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) | 129 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) |
130 | 130 | ||
131 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to) | 131 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) |
132 | } | 132 | } |
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index 88a968318..1d2279df5 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts | |||
@@ -2,7 +2,7 @@ import { ActivityDelete } from '../../../../shared/models/activitypub' | |||
2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { sequelizeTypescript } from '../../../initializers/database' | 4 | import { sequelizeTypescript } from '../../../initializers/database' |
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/actor/actor' |
6 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' |
@@ -16,7 +16,6 @@ import { | |||
16 | MChannelActor, | 16 | MChannelActor, |
17 | MCommentOwnerVideo | 17 | MCommentOwnerVideo |
18 | } from '../../../types/models' | 18 | } from '../../../types/models' |
19 | import { markCommentAsDeleted } from '../../video-comment' | ||
20 | import { forwardVideoRelatedActivity } from '../send/utils' | 19 | import { forwardVideoRelatedActivity } from '../send/utils' |
21 | 20 | ||
22 | async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) { | 21 | async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) { |
@@ -130,7 +129,7 @@ async function processDeleteVideoChannel (videoChannelToRemove: MChannelActor) { | |||
130 | 129 | ||
131 | function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCommentOwnerVideo, activity: ActivityDelete) { | 130 | function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCommentOwnerVideo, activity: ActivityDelete) { |
132 | // Already deleted | 131 | // Already deleted |
133 | if (videoComment.isDeleted()) return | 132 | if (videoComment.isDeleted()) return Promise.resolve() |
134 | 133 | ||
135 | logger.debug('Removing remote video comment "%s".', videoComment.url) | 134 | logger.debug('Removing remote video comment "%s".', videoComment.url) |
136 | 135 | ||
@@ -139,11 +138,9 @@ function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCom | |||
139 | throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`) | 138 | throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`) |
140 | } | 139 | } |
141 | 140 | ||
142 | await sequelizeTypescript.transaction(async t => { | 141 | videoComment.markAsDeleted() |
143 | markCommentAsDeleted(videoComment) | ||
144 | 142 | ||
145 | await videoComment.save() | 143 | await videoComment.save({ transaction: t }) |
146 | }) | ||
147 | 144 | ||
148 | if (videoComment.Video.isOwned()) { | 145 | if (videoComment.Video.isOwned()) { |
149 | // Don't resend the activity to the sender | 146 | // Don't resend the activity to the sender |
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts index 089c7b881..ecc57cd10 100644 --- a/server/lib/activitypub/process/process-dislike.ts +++ b/server/lib/activitypub/process/process-dislike.ts | |||
@@ -6,7 +6,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat | |||
6 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 6 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
7 | import { MActorSignature } from '../../../types/models' | 7 | import { MActorSignature } from '../../../types/models' |
8 | import { forwardVideoRelatedActivity } from '../send/utils' | 8 | import { forwardVideoRelatedActivity } from '../send/utils' |
9 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 9 | import { getOrCreateAPVideo } from '../videos' |
10 | 10 | ||
11 | async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { | 11 | async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { |
12 | const { activity, byActor } = options | 12 | const { activity, byActor } = options |
@@ -30,7 +30,7 @@ async function processDislike (activity: ActivityCreate | ActivityDislike, byAct | |||
30 | 30 | ||
31 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) | 31 | if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) |
32 | 32 | ||
33 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislikeObject }) | 33 | const { video } = await getOrCreateAPVideo({ videoObject: dislikeObject }) |
34 | 34 | ||
35 | return sequelizeTypescript.transaction(async t => { | 35 | return sequelizeTypescript.transaction(async t => { |
36 | const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) | 36 | const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) |
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts index 38d684512..f85238f8e 100644 --- a/server/lib/activitypub/process/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts | |||
@@ -1,17 +1,17 @@ | |||
1 | import { getServerActor } from '@server/models/application/application' | ||
1 | import { ActivityFollow } from '../../../../shared/models/activitypub' | 2 | import { ActivityFollow } from '../../../../shared/models/activitypub' |
3 | import { getAPId } from '../../../helpers/activitypub' | ||
2 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
3 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
4 | import { sequelizeTypescript } from '../../../initializers/database' | ||
5 | import { ActorModel } from '../../../models/activitypub/actor' | ||
6 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
7 | import { sendAccept, sendReject } from '../send' | ||
8 | import { Notifier } from '../../notifier' | ||
9 | import { getAPId } from '../../../helpers/activitypub' | ||
10 | import { CONFIG } from '../../../initializers/config' | 6 | import { CONFIG } from '../../../initializers/config' |
7 | import { sequelizeTypescript } from '../../../initializers/database' | ||
8 | import { ActorModel } from '../../../models/actor/actor' | ||
9 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | ||
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 10 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MActorFollowActors, MActorSignature } from '../../../types/models' | 11 | import { MActorFollowActors, MActorSignature } from '../../../types/models' |
12 | import { Notifier } from '../../notifier' | ||
13 | import { autoFollowBackIfNeeded } from '../follow' | 13 | import { autoFollowBackIfNeeded } from '../follow' |
14 | import { getServerActor } from '@server/models/application/application' | 14 | import { sendAccept, sendReject } from '../send' |
15 | 15 | ||
16 | async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { | 16 | async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { |
17 | const { activity, byActor } = options | 17 | const { activity, byActor } = options |
@@ -43,7 +43,7 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ | |||
43 | if (isFollowingInstance && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) { | 43 | if (isFollowingInstance && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) { |
44 | logger.info('Rejecting %s because instance followers are disabled.', targetActor.url) | 44 | logger.info('Rejecting %s because instance followers are disabled.', targetActor.url) |
45 | 45 | ||
46 | await sendReject(activityId, byActor, targetActor) | 46 | sendReject(activityId, byActor, targetActor) |
47 | 47 | ||
48 | return { actorFollow: undefined as MActorFollowActors } | 48 | return { actorFollow: undefined as MActorFollowActors } |
49 | } | 49 | } |
@@ -84,8 +84,9 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ | |||
84 | 84 | ||
85 | // Target sends to actor he accepted the follow request | 85 | // Target sends to actor he accepted the follow request |
86 | if (actorFollow.state === 'accepted') { | 86 | if (actorFollow.state === 'accepted') { |
87 | await sendAccept(actorFollow) | 87 | sendAccept(actorFollow) |
88 | await autoFollowBackIfNeeded(actorFollow) | 88 | |
89 | await autoFollowBackIfNeeded(actorFollow, t) | ||
89 | } | 90 | } |
90 | 91 | ||
91 | return { actorFollow, created, isFollowingInstance, targetActor } | 92 | return { actorFollow, created, isFollowingInstance, targetActor } |
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index 8688b3b47..cd4e86cbb 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts | |||
@@ -6,7 +6,7 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat | |||
6 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 6 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
7 | import { MActorSignature } from '../../../types/models' | 7 | import { MActorSignature } from '../../../types/models' |
8 | import { forwardVideoRelatedActivity } from '../send/utils' | 8 | import { forwardVideoRelatedActivity } from '../send/utils' |
9 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 9 | import { getOrCreateAPVideo } from '../videos' |
10 | 10 | ||
11 | async function processLikeActivity (options: APProcessorOptions<ActivityLike>) { | 11 | async function processLikeActivity (options: APProcessorOptions<ActivityLike>) { |
12 | const { activity, byActor } = options | 12 | const { activity, byActor } = options |
@@ -27,7 +27,7 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik | |||
27 | const byAccount = byActor.Account | 27 | const byAccount = byActor.Account |
28 | if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) | 28 | if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) |
29 | 29 | ||
30 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoUrl }) | 30 | const { video } = await getOrCreateAPVideo({ videoObject: videoUrl }) |
31 | 31 | ||
32 | return sequelizeTypescript.transaction(async t => { | 32 | return sequelizeTypescript.transaction(async t => { |
33 | const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) | 33 | const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t) |
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts index 03b669fd9..7f7ab305f 100644 --- a/server/lib/activitypub/process/process-reject.ts +++ b/server/lib/activitypub/process/process-reject.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { ActivityReject } from '../../../../shared/models/activitypub/activity' | 1 | import { ActivityReject } from '../../../../shared/models/activitypub/activity' |
2 | import { sequelizeTypescript } from '../../../initializers/database' | 2 | import { sequelizeTypescript } from '../../../initializers/database' |
3 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
4 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 4 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
5 | import { MActor } from '../../../types/models' | 5 | import { MActor } from '../../../types/models' |
6 | 6 | ||
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index e520c2f0d..d4b2a795f 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts | |||
@@ -4,14 +4,14 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers/database' | 5 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 6 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/actor/actor' |
8 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 8 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
9 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' | 9 | import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' |
10 | import { VideoShareModel } from '../../../models/video/video-share' | 10 | import { VideoShareModel } from '../../../models/video/video-share' |
11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
12 | import { MActorSignature } from '../../../types/models' | 12 | import { MActorSignature } from '../../../types/models' |
13 | import { forwardVideoRelatedActivity } from '../send/utils' | 13 | import { forwardVideoRelatedActivity } from '../send/utils' |
14 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 14 | import { getOrCreateAPVideo } from '../videos' |
15 | 15 | ||
16 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { | 16 | async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { |
17 | const { activity, byActor } = options | 17 | const { activity, byActor } = options |
@@ -55,7 +55,7 @@ export { | |||
55 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { | 55 | async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { |
56 | const likeActivity = activity.object as ActivityLike | 56 | const likeActivity = activity.object as ActivityLike |
57 | 57 | ||
58 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: likeActivity.object }) | 58 | const { video } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) |
59 | 59 | ||
60 | return sequelizeTypescript.transaction(async t => { | 60 | return sequelizeTypescript.transaction(async t => { |
61 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 61 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
@@ -80,7 +80,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU | |||
80 | ? activity.object | 80 | ? activity.object |
81 | : activity.object.object as DislikeObject | 81 | : activity.object.object as DislikeObject |
82 | 82 | ||
83 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: dislike.object }) | 83 | const { video } = await getOrCreateAPVideo({ videoObject: dislike.object }) |
84 | 84 | ||
85 | return sequelizeTypescript.transaction(async t => { | 85 | return sequelizeTypescript.transaction(async t => { |
86 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) | 86 | if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) |
@@ -103,10 +103,10 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU | |||
103 | async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { | 103 | async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { |
104 | const cacheFileObject = activity.object.object as CacheFileObject | 104 | const cacheFileObject = activity.object.object as CacheFileObject |
105 | 105 | ||
106 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object }) | 106 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) |
107 | 107 | ||
108 | return sequelizeTypescript.transaction(async t => { | 108 | return sequelizeTypescript.transaction(async t => { |
109 | const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id) | 109 | const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) |
110 | if (!cacheFile) { | 110 | if (!cacheFile) { |
111 | logger.debug('Cannot undo unknown video cache %s.', cacheFileObject.id) | 111 | logger.debug('Cannot undo unknown video cache %s.', cacheFileObject.id) |
112 | return | 112 | return |
@@ -114,7 +114,7 @@ async function processUndoCacheFile (byActor: MActorSignature, activity: Activit | |||
114 | 114 | ||
115 | if (cacheFile.actorId !== byActor.id) throw new Error('Cannot delete redundancy ' + cacheFile.url + ' of another actor.') | 115 | if (cacheFile.actorId !== byActor.id) throw new Error('Cannot delete redundancy ' + cacheFile.url + ' of another actor.') |
116 | 116 | ||
117 | await cacheFile.destroy() | 117 | await cacheFile.destroy({ transaction: t }) |
118 | 118 | ||
119 | if (video.isOwned()) { | 119 | if (video.isOwned()) { |
120 | // Don't resend the activity to the sender | 120 | // Don't resend the activity to the sender |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 6df9b93b2..f40008a6b 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -1,23 +1,20 @@ | |||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
1 | import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' | 2 | import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' |
2 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | 3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' |
3 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' | 4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' |
5 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | ||
6 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | ||
7 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | ||
4 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
5 | import { sequelizeTypescript } from '../../../initializers/database' | 9 | import { sequelizeTypescript } from '../../../initializers/database' |
6 | import { AccountModel } from '../../../models/account/account' | 10 | import { ActorModel } from '../../../models/actor/actor' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 11 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
8 | import { VideoChannelModel } from '../../../models/video/video-channel' | 12 | import { MActorFull, MActorSignature } from '../../../types/models' |
9 | import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' | 13 | import { APActorUpdater } from '../actors/updater' |
10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' | ||
11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | ||
12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | ||
13 | import { createOrUpdateCacheFile } from '../cache-file' | 14 | import { createOrUpdateCacheFile } from '../cache-file' |
15 | import { createOrUpdateVideoPlaylist } from '../playlists' | ||
14 | import { forwardVideoRelatedActivity } from '../send/utils' | 16 | import { forwardVideoRelatedActivity } from '../send/utils' |
15 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | 17 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' |
16 | import { createOrUpdateVideoPlaylist } from '../playlist' | ||
17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' | ||
19 | import { isRedundancyAccepted } from '@server/lib/redundancy' | ||
20 | import { ActorImageType } from '@shared/models' | ||
21 | 18 | ||
22 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 19 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { |
23 | const { activity, byActor } = options | 20 | const { activity, byActor } = options |
@@ -25,7 +22,7 @@ async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate | |||
25 | const objectType = activity.object.type | 22 | const objectType = activity.object.type |
26 | 23 | ||
27 | if (objectType === 'Video') { | 24 | if (objectType === 'Video') { |
28 | return retryTransactionWrapper(processUpdateVideo, byActor, activity) | 25 | return retryTransactionWrapper(processUpdateVideo, activity) |
29 | } | 26 | } |
30 | 27 | ||
31 | if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { | 28 | if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') { |
@@ -55,7 +52,7 @@ export { | |||
55 | 52 | ||
56 | // --------------------------------------------------------------------------- | 53 | // --------------------------------------------------------------------------- |
57 | 54 | ||
58 | async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpdate) { | 55 | async function processUpdateVideo (activity: ActivityUpdate) { |
59 | const videoObject = activity.object as VideoObject | 56 | const videoObject = activity.object as VideoObject |
60 | 57 | ||
61 | if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { | 58 | if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { |
@@ -63,7 +60,7 @@ async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpd | |||
63 | return undefined | 60 | return undefined |
64 | } | 61 | } |
65 | 62 | ||
66 | const { video, created } = await getOrCreateVideoAndAccountAndChannel({ | 63 | const { video, created } = await getOrCreateAPVideo({ |
67 | videoObject: videoObject.id, | 64 | videoObject: videoObject.id, |
68 | allowRefresh: false, | 65 | allowRefresh: false, |
69 | fetchType: 'all' | 66 | fetchType: 'all' |
@@ -71,20 +68,8 @@ async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpd | |||
71 | // We did not have this video, it has been created so no need to update | 68 | // We did not have this video, it has been created so no need to update |
72 | if (created) return | 69 | if (created) return |
73 | 70 | ||
74 | // Load new channel | 71 | const updater = new APVideoUpdater(videoObject, video) |
75 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | 72 | return updater.update(activity.to) |
76 | |||
77 | const account = actor.Account as MAccountIdActor | ||
78 | account.Actor = actor | ||
79 | |||
80 | const updateOptions = { | ||
81 | video, | ||
82 | videoObject, | ||
83 | account, | ||
84 | channel: channelActor.VideoChannel, | ||
85 | overrideTo: activity.to | ||
86 | } | ||
87 | return updateVideoFromAP(updateOptions) | ||
88 | } | 73 | } |
89 | 74 | ||
90 | async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { | 75 | async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { |
@@ -97,7 +82,7 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ | |||
97 | return undefined | 82 | return undefined |
98 | } | 83 | } |
99 | 84 | ||
100 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object }) | 85 | const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) |
101 | 86 | ||
102 | await sequelizeTypescript.transaction(async t => { | 87 | await sequelizeTypescript.transaction(async t => { |
103 | await createOrUpdateCacheFile(cacheFileObject, video, byActor, t) | 88 | await createOrUpdateCacheFile(cacheFileObject, video, byActor, t) |
@@ -111,56 +96,13 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ | |||
111 | } | 96 | } |
112 | } | 97 | } |
113 | 98 | ||
114 | async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) { | 99 | async function processUpdateActor (actor: MActorFull, activity: ActivityUpdate) { |
115 | const actorAttributesToUpdate = activity.object as ActivityPubActor | 100 | const actorObject = activity.object as ActivityPubActor |
116 | 101 | ||
117 | logger.debug('Updating remote account "%s".', actorAttributesToUpdate.url) | 102 | logger.debug('Updating remote account "%s".', actorObject.url) |
118 | let accountOrChannelInstance: AccountModel | VideoChannelModel | ||
119 | let actorFieldsSave: object | ||
120 | let accountOrChannelFieldsSave: object | ||
121 | 103 | ||
122 | // Fetch icon? | 104 | const updater = new APActorUpdater(actorObject, actor) |
123 | const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR) | 105 | return updater.update() |
124 | const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER) | ||
125 | |||
126 | try { | ||
127 | await sequelizeTypescript.transaction(async t => { | ||
128 | actorFieldsSave = actor.toJSON() | ||
129 | |||
130 | if (actorAttributesToUpdate.type === 'Group') accountOrChannelInstance = actor.VideoChannel | ||
131 | else accountOrChannelInstance = actor.Account | ||
132 | |||
133 | accountOrChannelFieldsSave = accountOrChannelInstance.toJSON() | ||
134 | |||
135 | await updateActorInstance(actor, actorAttributesToUpdate) | ||
136 | |||
137 | await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t) | ||
138 | await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t) | ||
139 | |||
140 | await actor.save({ transaction: t }) | ||
141 | |||
142 | accountOrChannelInstance.name = actorAttributesToUpdate.name || actorAttributesToUpdate.preferredUsername | ||
143 | accountOrChannelInstance.description = actorAttributesToUpdate.summary | ||
144 | |||
145 | if (accountOrChannelInstance instanceof VideoChannelModel) accountOrChannelInstance.support = actorAttributesToUpdate.support | ||
146 | |||
147 | await accountOrChannelInstance.save({ transaction: t }) | ||
148 | }) | ||
149 | |||
150 | logger.info('Remote account %s updated', actorAttributesToUpdate.url) | ||
151 | } catch (err) { | ||
152 | if (actor !== undefined && actorFieldsSave !== undefined) { | ||
153 | resetSequelizeInstance(actor, actorFieldsSave) | ||
154 | } | ||
155 | |||
156 | if (accountOrChannelInstance !== undefined && accountOrChannelFieldsSave !== undefined) { | ||
157 | resetSequelizeInstance(accountOrChannelInstance, accountOrChannelFieldsSave) | ||
158 | } | ||
159 | |||
160 | // This is just a debug because we will retry the insert | ||
161 | logger.debug('Cannot update the remote account.', { err }) | ||
162 | throw err | ||
163 | } | ||
164 | } | 106 | } |
165 | 107 | ||
166 | async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { | 108 | async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { |
@@ -169,5 +111,5 @@ async function processUpdatePlaylist (byActor: MActorSignature, activity: Activi | |||
169 | 111 | ||
170 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) | 112 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) |
171 | 113 | ||
172 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to) | 114 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) |
173 | } | 115 | } |
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts index 84697673b..5593ee257 100644 --- a/server/lib/activitypub/process/process-view.ts +++ b/server/lib/activitypub/process/process-view.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 1 | import { getOrCreateAPVideo } from '../videos' |
2 | import { forwardVideoRelatedActivity } from '../send/utils' | 2 | import { forwardVideoRelatedActivity } from '../send/utils' |
3 | import { Redis } from '../../redis' | 3 | import { Redis } from '../../redis' |
4 | import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' | 4 | import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' |
5 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 5 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
6 | import { MActorSignature } from '../../../types/models' | 6 | import { MActorSignature } from '../../../types/models' |
7 | import { LiveManager } from '@server/lib/live-manager' | 7 | import { LiveManager } from '@server/lib/live/live-manager' |
8 | 8 | ||
9 | async function processViewActivity (options: APProcessorOptions<ActivityCreate | ActivityView>) { | 9 | async function processViewActivity (options: APProcessorOptions<ActivityCreate | ActivityView>) { |
10 | const { activity, byActor } = options | 10 | const { activity, byActor } = options |
@@ -24,12 +24,11 @@ async function processCreateView (activity: ActivityView | ActivityCreate, byAct | |||
24 | ? activity.object | 24 | ? activity.object |
25 | : (activity.object as ViewObject).object | 25 | : (activity.object as ViewObject).object |
26 | 26 | ||
27 | const options = { | 27 | const { video } = await getOrCreateAPVideo({ |
28 | videoObject, | 28 | videoObject, |
29 | fetchType: 'only-video' as 'only-video', | 29 | fetchType: 'only-video', |
30 | allowRefresh: false as false | 30 | allowRefresh: false |
31 | } | 31 | }) |
32 | const { video } = await getOrCreateVideoAndAccountAndChannel(options) | ||
33 | 32 | ||
34 | if (!video.isLive) { | 33 | if (!video.isLive) { |
35 | await Redis.Instance.addVideoView(video.id) | 34 | await Redis.Instance.addVideoView(video.id) |
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts index 5cef75665..02a23d098 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts | |||
@@ -1,22 +1,22 @@ | |||
1 | import { StatsManager } from '@server/lib/stat-manager' | ||
1 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
2 | import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' | 3 | import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' |
3 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
6 | import { MActorDefault, MActorSignature } from '../../../types/models' | ||
7 | import { getOrCreateAPActor } from '../actors' | ||
4 | import { processAcceptActivity } from './process-accept' | 8 | import { processAcceptActivity } from './process-accept' |
5 | import { processAnnounceActivity } from './process-announce' | 9 | import { processAnnounceActivity } from './process-announce' |
6 | import { processCreateActivity } from './process-create' | 10 | import { processCreateActivity } from './process-create' |
7 | import { processDeleteActivity } from './process-delete' | 11 | import { processDeleteActivity } from './process-delete' |
12 | import { processDislikeActivity } from './process-dislike' | ||
13 | import { processFlagActivity } from './process-flag' | ||
8 | import { processFollowActivity } from './process-follow' | 14 | import { processFollowActivity } from './process-follow' |
9 | import { processLikeActivity } from './process-like' | 15 | import { processLikeActivity } from './process-like' |
10 | import { processRejectActivity } from './process-reject' | 16 | import { processRejectActivity } from './process-reject' |
11 | import { processUndoActivity } from './process-undo' | 17 | import { processUndoActivity } from './process-undo' |
12 | import { processUpdateActivity } from './process-update' | 18 | import { processUpdateActivity } from './process-update' |
13 | import { getOrCreateActorAndServerAndModel } from '../actor' | ||
14 | import { processDislikeActivity } from './process-dislike' | ||
15 | import { processFlagActivity } from './process-flag' | ||
16 | import { processViewActivity } from './process-view' | 19 | import { processViewActivity } from './process-view' |
17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | ||
18 | import { MActorDefault, MActorSignature } from '../../../types/models' | ||
19 | import { StatsManager } from '@server/lib/stat-manager' | ||
20 | 20 | ||
21 | const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = { | 21 | const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = { |
22 | Create: processCreateActivity, | 22 | Create: processCreateActivity, |
@@ -65,7 +65,7 @@ async function processActivities ( | |||
65 | continue | 65 | continue |
66 | } | 66 | } |
67 | 67 | ||
68 | const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) | 68 | const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateAPActor(actorUrl) |
69 | actorsCache[actorUrl] = byActor | 69 | actorsCache[actorUrl] = byActor |
70 | 70 | ||
71 | const activityProcessor = processActivity[activity.type] | 71 | const activityProcessor = processActivity[activity.type] |
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts index e0acced18..d31f8c10b 100644 --- a/server/lib/activitypub/send/send-delete.ts +++ b/server/lib/activitypub/send/send-delete.ts | |||
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize' | |||
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' | 3 | import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/actor/actor' |
6 | import { VideoCommentModel } from '../../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../../models/video/video-comment' |
7 | import { VideoShareModel } from '../../../models/video/video-share' | 7 | import { VideoShareModel } from '../../../models/video/video-share' |
8 | import { MActorUrl } from '../../../types/models' | 8 | import { MActorUrl } from '../../../types/models' |
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts index 9254dc7c5..153e94295 100644 --- a/server/lib/activitypub/send/send-view.ts +++ b/server/lib/activitypub/send/send-view.ts | |||
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize' | |||
2 | import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models' | 2 | import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models' |
3 | import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' | 3 | import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/actor/actor' |
6 | import { audiencify, getAudience } from '../audience' | 6 | import { audiencify, getAudience } from '../audience' |
7 | import { getLocalVideoViewActivityPubUrl } from '../url' | 7 | import { getLocalVideoViewActivityPubUrl } from '../url' |
8 | import { sendVideoRelatedActivity } from './utils' | 8 | import { sendVideoRelatedActivity } from './utils' |
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts index 85a9f009d..7cd8030e1 100644 --- a/server/lib/activitypub/send/utils.ts +++ b/server/lib/activitypub/send/utils.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { ContextType } from '@shared/models/activitypub/context' | ||
2 | import { Activity, ActivityAudience } from '../../../../shared/models/activitypub' | 4 | import { Activity, ActivityAudience } from '../../../../shared/models/activitypub' |
5 | import { afterCommitIfTransaction } from '../../../helpers/database-utils' | ||
3 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
4 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/actor/actor' |
5 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 8 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
9 | import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models' | ||
6 | import { JobQueue } from '../../job-queue' | 10 | import { JobQueue } from '../../job-queue' |
7 | import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' | 11 | import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' |
8 | import { afterCommitIfTransaction } from '../../../helpers/database-utils' | ||
9 | import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models' | ||
10 | import { getServerActor } from '@server/models/application/application' | ||
11 | import { ContextType } from '@shared/models/activitypub/context' | ||
12 | 12 | ||
13 | async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { | 13 | async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { |
14 | byActor: MActorLight | 14 | byActor: MActorLight |
@@ -22,7 +22,9 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud | |||
22 | 22 | ||
23 | // Send to origin | 23 | // Send to origin |
24 | if (video.isOwned() === false) { | 24 | if (video.isOwned() === false) { |
25 | const accountActor = (video as MVideoAccountLight).VideoChannel?.Account?.Actor || await ActorModel.loadAccountActorByVideoId(video.id) | 25 | let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor |
26 | |||
27 | if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction) | ||
26 | 28 | ||
27 | const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo) | 29 | const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo) |
28 | const activity = activityBuilder(audience) | 30 | const activity = activityBuilder(audience) |
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index c22fa0893..1ff01a175 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -7,7 +7,7 @@ import { doJSONRequest } from '../../helpers/requests' | |||
7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
8 | import { VideoShareModel } from '../../models/video/video-share' | 8 | import { VideoShareModel } from '../../models/video/video-share' |
9 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' | 9 | import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' |
10 | import { getOrCreateActorAndServerAndModel } from './actor' | 10 | import { getOrCreateAPActor } from './actors' |
11 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' | 11 | import { sendUndoAnnounce, sendVideoAnnounce } from './send' |
12 | import { getLocalVideoAnnounceActivityPubUrl } from './url' | 12 | import { getLocalVideoAnnounceActivityPubUrl } from './url' |
13 | 13 | ||
@@ -40,23 +40,7 @@ async function changeVideoChannelShare ( | |||
40 | async function addVideoShares (shareUrls: string[], video: MVideoId) { | 40 | async function addVideoShares (shareUrls: string[], video: MVideoId) { |
41 | await Bluebird.map(shareUrls, async shareUrl => { | 41 | await Bluebird.map(shareUrls, async shareUrl => { |
42 | try { | 42 | try { |
43 | const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true }) | 43 | await addVideoShare(shareUrl, video) |
44 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
45 | |||
46 | const actorUrl = getAPId(body.actor) | ||
47 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | ||
48 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | ||
49 | } | ||
50 | |||
51 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
52 | |||
53 | const entry = { | ||
54 | actorId: actor.id, | ||
55 | videoId: video.id, | ||
56 | url: shareUrl | ||
57 | } | ||
58 | |||
59 | await VideoShareModel.upsert(entry) | ||
60 | } catch (err) { | 44 | } catch (err) { |
61 | logger.warn('Cannot add share %s.', shareUrl, { err }) | 45 | logger.warn('Cannot add share %s.', shareUrl, { err }) |
62 | } | 46 | } |
@@ -71,6 +55,26 @@ export { | |||
71 | 55 | ||
72 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
73 | 57 | ||
58 | async function addVideoShare (shareUrl: string, video: MVideoId) { | ||
59 | const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true }) | ||
60 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
61 | |||
62 | const actorUrl = getAPId(body.actor) | ||
63 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | ||
64 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | ||
65 | } | ||
66 | |||
67 | const actor = await getOrCreateAPActor(actorUrl) | ||
68 | |||
69 | const entry = { | ||
70 | actorId: actor.id, | ||
71 | videoId: video.id, | ||
72 | url: shareUrl | ||
73 | } | ||
74 | |||
75 | await VideoShareModel.upsert(entry) | ||
76 | } | ||
77 | |||
74 | async function shareByServer (video: MVideo, t: Transaction) { | 78 | async function shareByServer (video: MVideo, t: Transaction) { |
75 | const serverActor = await getServerActor() | 79 | const serverActor = await getServerActor() |
76 | 80 | ||
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index e23e0c0e7..6b7f9504f 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -6,8 +6,8 @@ import { doJSONRequest } from '../../helpers/requests' | |||
6 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 6 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
7 | import { VideoCommentModel } from '../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../models/video/video-comment' |
8 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' | 8 | import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' |
9 | import { getOrCreateActorAndServerAndModel } from './actor' | 9 | import { getOrCreateAPActor } from './actors' |
10 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | 10 | import { getOrCreateAPVideo } from './videos' |
11 | 11 | ||
12 | type ResolveThreadParams = { | 12 | type ResolveThreadParams = { |
13 | url: string | 13 | url: string |
@@ -29,10 +29,11 @@ async function addVideoComments (commentUrls: string[]) { | |||
29 | 29 | ||
30 | async function resolveThread (params: ResolveThreadParams): ResolveThreadResult { | 30 | async function resolveThread (params: ResolveThreadParams): ResolveThreadResult { |
31 | const { url, isVideo } = params | 31 | const { url, isVideo } = params |
32 | |||
32 | if (params.commentCreated === undefined) params.commentCreated = false | 33 | if (params.commentCreated === undefined) params.commentCreated = false |
33 | if (params.comments === undefined) params.comments = [] | 34 | if (params.comments === undefined) params.comments = [] |
34 | 35 | ||
35 | // If it is not a video, or if we don't know if it's a video | 36 | // If it is not a video, or if we don't know if it's a video, try to get the thread from DB |
36 | if (isVideo === false || isVideo === undefined) { | 37 | if (isVideo === false || isVideo === undefined) { |
37 | const result = await resolveCommentFromDB(params) | 38 | const result = await resolveCommentFromDB(params) |
38 | if (result) return result | 39 | if (result) return result |
@@ -42,7 +43,7 @@ async function resolveThread (params: ResolveThreadParams): ResolveThreadResult | |||
42 | // If it is a video, or if we don't know if it's a video | 43 | // If it is a video, or if we don't know if it's a video |
43 | if (isVideo === true || isVideo === undefined) { | 44 | if (isVideo === true || isVideo === undefined) { |
44 | // Keep await so we catch the exception | 45 | // Keep await so we catch the exception |
45 | return await tryResolveThreadFromVideo(params) | 46 | return await tryToResolveThreadFromVideo(params) |
46 | } | 47 | } |
47 | } catch (err) { | 48 | } catch (err) { |
48 | logger.debug('Cannot resolve thread from video %s, maybe because it was not a video', url, { err }) | 49 | logger.debug('Cannot resolve thread from video %s, maybe because it was not a video', url, { err }) |
@@ -62,34 +63,32 @@ async function resolveCommentFromDB (params: ResolveThreadParams) { | |||
62 | const { url, comments, commentCreated } = params | 63 | const { url, comments, commentCreated } = params |
63 | 64 | ||
64 | const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideoUrlAndAccount(url) | 65 | const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideoUrlAndAccount(url) |
65 | if (commentFromDatabase) { | 66 | if (!commentFromDatabase) return undefined |
66 | let parentComments = comments.concat([ commentFromDatabase ]) | ||
67 | 67 | ||
68 | // Speed up things and resolve directly the thread | 68 | let parentComments = comments.concat([ commentFromDatabase ]) |
69 | if (commentFromDatabase.InReplyToVideoComment) { | ||
70 | const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC') | ||
71 | 69 | ||
72 | parentComments = parentComments.concat(data) | 70 | // Speed up things and resolve directly the thread |
73 | } | 71 | if (commentFromDatabase.InReplyToVideoComment) { |
72 | const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC') | ||
74 | 73 | ||
75 | return resolveThread({ | 74 | parentComments = parentComments.concat(data) |
76 | url: commentFromDatabase.Video.url, | ||
77 | comments: parentComments, | ||
78 | isVideo: true, | ||
79 | commentCreated | ||
80 | }) | ||
81 | } | 75 | } |
82 | 76 | ||
83 | return undefined | 77 | return resolveThread({ |
78 | url: commentFromDatabase.Video.url, | ||
79 | comments: parentComments, | ||
80 | isVideo: true, | ||
81 | commentCreated | ||
82 | }) | ||
84 | } | 83 | } |
85 | 84 | ||
86 | async function tryResolveThreadFromVideo (params: ResolveThreadParams) { | 85 | async function tryToResolveThreadFromVideo (params: ResolveThreadParams) { |
87 | const { url, comments, commentCreated } = params | 86 | const { url, comments, commentCreated } = params |
88 | 87 | ||
89 | // Maybe it's a reply to a video? | 88 | // Maybe it's a reply to a video? |
90 | // If yes, it's done: we resolved all the thread | 89 | // If yes, it's done: we resolved all the thread |
91 | const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false } | 90 | const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false } |
92 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam }) | 91 | const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam }) |
93 | 92 | ||
94 | if (video.isOwned() && !video.hasPrivacyForFederation()) { | 93 | if (video.isOwned() && !video.hasPrivacyForFederation()) { |
95 | throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation') | 94 | throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation') |
@@ -148,7 +147,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) { | |||
148 | } | 147 | } |
149 | 148 | ||
150 | const actor = actorUrl | 149 | const actor = actorUrl |
151 | ? await getOrCreateActorAndServerAndModel(actorUrl, 'all') | 150 | ? await getOrCreateAPActor(actorUrl, 'all') |
152 | : null | 151 | : null |
153 | 152 | ||
154 | const comment = new VideoCommentModel({ | 153 | const comment = new VideoCommentModel({ |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index f40c07fea..9fb97ef84 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -3,44 +3,23 @@ import { Transaction } from 'sequelize' | |||
3 | import { doJSONRequest } from '@server/helpers/requests' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
4 | import { VideoRateType } from '../../../shared/models/videos' | 4 | import { VideoRateType } from '../../../shared/models/videos' |
5 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 5 | import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
6 | import { logger } from '../../helpers/logger' | 6 | import { logger, loggerTagsFactory } from '../../helpers/logger' |
7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | 7 | import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 8 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
9 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' | 9 | import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' |
10 | import { getOrCreateActorAndServerAndModel } from './actor' | 10 | import { getOrCreateAPActor } from './actors' |
11 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' | 11 | import { sendLike, sendUndoDislike, sendUndoLike } from './send' |
12 | import { sendDislike } from './send/send-dislike' | 12 | import { sendDislike } from './send/send-dislike' |
13 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' | 13 | import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' |
14 | 14 | ||
15 | const lTags = loggerTagsFactory('ap', 'video-rate', 'create') | ||
16 | |||
15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { | 17 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { |
16 | await Bluebird.map(ratesUrl, async rateUrl => { | 18 | await Bluebird.map(ratesUrl, async rateUrl => { |
17 | try { | 19 | try { |
18 | // Fetch url | 20 | await createRate(rateUrl, video, rate) |
19 | const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true }) | ||
20 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
21 | |||
22 | const actorUrl = getAPId(body.actor) | ||
23 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | ||
24 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | ||
25 | } | ||
26 | |||
27 | if (checkUrlsSameHost(body.id, rateUrl) !== true) { | ||
28 | throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) | ||
29 | } | ||
30 | |||
31 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
32 | |||
33 | const entry = { | ||
34 | videoId: video.id, | ||
35 | accountId: actor.Account.id, | ||
36 | type: rate, | ||
37 | url: body.id | ||
38 | } | ||
39 | |||
40 | // Video "likes"/"dislikes" will be updated by the caller | ||
41 | await AccountVideoRateModel.upsert(entry) | ||
42 | } catch (err) { | 21 | } catch (err) { |
43 | logger.warn('Cannot add rate %s.', rateUrl, { err }) | 22 | logger.info('Cannot add rate %s.', rateUrl, { err, ...lTags(rateUrl, video.uuid, video.url) }) |
44 | } | 23 | } |
45 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | 24 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) |
46 | } | 25 | } |
@@ -73,8 +52,39 @@ function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVid | |||
73 | : getVideoDislikeActivityPubUrlByLocalActor(actor, video) | 52 | : getVideoDislikeActivityPubUrlByLocalActor(actor, video) |
74 | } | 53 | } |
75 | 54 | ||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
76 | export { | 57 | export { |
77 | getLocalRateUrl, | 58 | getLocalRateUrl, |
78 | createRates, | 59 | createRates, |
79 | sendVideoRateChange | 60 | sendVideoRateChange |
80 | } | 61 | } |
62 | |||
63 | // --------------------------------------------------------------------------- | ||
64 | |||
65 | async function createRate (rateUrl: string, video: MVideo, rate: VideoRateType) { | ||
66 | // Fetch url | ||
67 | const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true }) | ||
68 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
69 | |||
70 | const actorUrl = getAPId(body.actor) | ||
71 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | ||
72 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | ||
73 | } | ||
74 | |||
75 | if (checkUrlsSameHost(body.id, rateUrl) !== true) { | ||
76 | throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) | ||
77 | } | ||
78 | |||
79 | const actor = await getOrCreateAPActor(actorUrl) | ||
80 | |||
81 | const entry = { | ||
82 | videoId: video.id, | ||
83 | accountId: actor.Account.id, | ||
84 | type: rate, | ||
85 | url: body.id | ||
86 | } | ||
87 | |||
88 | // Video "likes"/"dislikes" will be updated by the caller | ||
89 | await AccountVideoRateModel.upsert(entry) | ||
90 | } | ||
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts deleted file mode 100644 index 127a0dd8a..000000000 --- a/server/lib/activitypub/videos.ts +++ /dev/null | |||
@@ -1,931 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { maxBy, minBy } from 'lodash' | ||
3 | import * as magnetUtil from 'magnet-uri' | ||
4 | import { basename } from 'path' | ||
5 | import { Transaction } from 'sequelize/types' | ||
6 | import { TrackerModel } from '@server/models/server/tracker' | ||
7 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
9 | import { | ||
10 | ActivityHashTagObject, | ||
11 | ActivityMagnetUrlObject, | ||
12 | ActivityPlaylistSegmentHashesObject, | ||
13 | ActivityPlaylistUrlObject, | ||
14 | ActivitypubHttpFetcherPayload, | ||
15 | ActivityTagObject, | ||
16 | ActivityUrlObject, | ||
17 | ActivityVideoUrlObject | ||
18 | } from '../../../shared/index' | ||
19 | import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' | ||
20 | import { VideoPrivacy } from '../../../shared/models/videos' | ||
21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
22 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
23 | import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | ||
24 | import { | ||
25 | isAPVideoFileUrlMetadataObject, | ||
26 | isAPVideoTrackerUrlObject, | ||
27 | sanitizeAndCheckVideoTorrentObject | ||
28 | } from '../../helpers/custom-validators/activitypub/videos' | ||
29 | import { isArray } from '../../helpers/custom-validators/misc' | ||
30 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | ||
31 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | ||
32 | import { logger } from '../../helpers/logger' | ||
33 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' | ||
34 | import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' | ||
35 | import { | ||
36 | ACTIVITY_PUB, | ||
37 | MIMETYPES, | ||
38 | P2P_MEDIA_LOADER_PEER_VERSION, | ||
39 | PREVIEWS_SIZE, | ||
40 | REMOTE_SCHEME, | ||
41 | THUMBNAILS_SIZE | ||
42 | } from '../../initializers/constants' | ||
43 | import { sequelizeTypescript } from '../../initializers/database' | ||
44 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | ||
45 | import { VideoModel } from '../../models/video/video' | ||
46 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
47 | import { VideoCommentModel } from '../../models/video/video-comment' | ||
48 | import { VideoFileModel } from '../../models/video/video-file' | ||
49 | import { VideoShareModel } from '../../models/video/video-share' | ||
50 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | ||
51 | import { | ||
52 | MAccountIdActor, | ||
53 | MChannelAccountLight, | ||
54 | MChannelDefault, | ||
55 | MChannelId, | ||
56 | MStreamingPlaylist, | ||
57 | MStreamingPlaylistFilesVideo, | ||
58 | MStreamingPlaylistVideo, | ||
59 | MVideo, | ||
60 | MVideoAccountLight, | ||
61 | MVideoAccountLightBlacklistAllFiles, | ||
62 | MVideoAP, | ||
63 | MVideoAPWithoutCaption, | ||
64 | MVideoCaption, | ||
65 | MVideoFile, | ||
66 | MVideoFullLight, | ||
67 | MVideoId, | ||
68 | MVideoImmutable, | ||
69 | MVideoThumbnail, | ||
70 | MVideoWithHost | ||
71 | } from '../../types/models' | ||
72 | import { MThumbnail } from '../../types/models/video/thumbnail' | ||
73 | import { FilteredModelAttributes } from '../../types/sequelize' | ||
74 | import { ActorFollowScoreCache } from '../files-cache' | ||
75 | import { JobQueue } from '../job-queue' | ||
76 | import { Notifier } from '../notifier' | ||
77 | import { PeerTubeSocket } from '../peertube-socket' | ||
78 | import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail' | ||
79 | import { setVideoTags } from '../video' | ||
80 | import { autoBlacklistVideoIfNeeded } from '../video-blacklist' | ||
81 | import { generateTorrentFileName } from '../video-paths' | ||
82 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
83 | import { crawlCollectionPage } from './crawl' | ||
84 | import { sendCreateVideo, sendUpdateVideo } from './send' | ||
85 | import { addVideoShares, shareVideoByServerAndChannel } from './share' | ||
86 | import { addVideoComments } from './video-comments' | ||
87 | import { createRates } from './video-rates' | ||
88 | |||
89 | async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: Transaction) { | ||
90 | const video = videoArg as MVideoAP | ||
91 | |||
92 | if ( | ||
93 | // Check this is not a blacklisted video, or unfederated blacklisted video | ||
94 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && | ||
95 | // Check the video is public/unlisted and published | ||
96 | video.hasPrivacyForFederation() && video.hasStateForFederation() | ||
97 | ) { | ||
98 | // Fetch more attributes that we will need to serialize in AP object | ||
99 | if (isArray(video.VideoCaptions) === false) { | ||
100 | video.VideoCaptions = await video.$get('VideoCaptions', { | ||
101 | attributes: [ 'filename', 'language' ], | ||
102 | transaction | ||
103 | }) | ||
104 | } | ||
105 | |||
106 | if (isNewVideo) { | ||
107 | // Now we'll add the video's meta data to our followers | ||
108 | await sendCreateVideo(video, transaction) | ||
109 | await shareVideoByServerAndChannel(video, transaction) | ||
110 | } else { | ||
111 | await sendUpdateVideo(video, transaction) | ||
112 | } | ||
113 | } | ||
114 | } | ||
115 | |||
116 | async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> { | ||
117 | logger.info('Fetching remote video %s.', videoUrl) | ||
118 | |||
119 | const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true }) | ||
120 | |||
121 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { | ||
122 | logger.debug('Remote video JSON is not valid.', { body }) | ||
123 | return { statusCode, videoObject: undefined } | ||
124 | } | ||
125 | |||
126 | return { statusCode, videoObject: body } | ||
127 | } | ||
128 | |||
129 | async function fetchRemoteVideoDescription (video: MVideoAccountLight) { | ||
130 | const host = video.VideoChannel.Account.Actor.Server.host | ||
131 | const path = video.getDescriptionAPIPath() | ||
132 | const url = REMOTE_SCHEME.HTTP + '://' + host + path | ||
133 | |||
134 | const { body } = await doJSONRequest<any>(url) | ||
135 | return body.description || '' | ||
136 | } | ||
137 | |||
138 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { | ||
139 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') | ||
140 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) | ||
141 | |||
142 | if (checkUrlsSameHost(channel.id, videoObject.id) !== true) { | ||
143 | throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`) | ||
144 | } | ||
145 | |||
146 | return getOrCreateActorAndServerAndModel(channel.id, 'all') | ||
147 | } | ||
148 | |||
149 | type SyncParam = { | ||
150 | likes: boolean | ||
151 | dislikes: boolean | ||
152 | shares: boolean | ||
153 | comments: boolean | ||
154 | thumbnail: boolean | ||
155 | refreshVideo?: boolean | ||
156 | } | ||
157 | async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) { | ||
158 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) | ||
159 | |||
160 | const jobPayloads: ActivitypubHttpFetcherPayload[] = [] | ||
161 | |||
162 | if (syncParam.likes === true) { | ||
163 | const handler = items => createRates(items, video, 'like') | ||
164 | const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate) | ||
165 | |||
166 | await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner) | ||
167 | .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.likes })) | ||
168 | } else { | ||
169 | jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) | ||
170 | } | ||
171 | |||
172 | if (syncParam.dislikes === true) { | ||
173 | const handler = items => createRates(items, video, 'dislike') | ||
174 | const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate) | ||
175 | |||
176 | await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner) | ||
177 | .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.dislikes })) | ||
178 | } else { | ||
179 | jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) | ||
180 | } | ||
181 | |||
182 | if (syncParam.shares === true) { | ||
183 | const handler = items => addVideoShares(items, video) | ||
184 | const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) | ||
185 | |||
186 | await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner) | ||
187 | .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: fetchedVideo.shares })) | ||
188 | } else { | ||
189 | jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) | ||
190 | } | ||
191 | |||
192 | if (syncParam.comments === true) { | ||
193 | const handler = items => addVideoComments(items) | ||
194 | const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) | ||
195 | |||
196 | await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner) | ||
197 | .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: fetchedVideo.comments })) | ||
198 | } else { | ||
199 | jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' }) | ||
200 | } | ||
201 | |||
202 | await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })) | ||
203 | } | ||
204 | |||
205 | type GetVideoResult <T> = Promise<{ | ||
206 | video: T | ||
207 | created: boolean | ||
208 | autoBlacklisted?: boolean | ||
209 | }> | ||
210 | |||
211 | type GetVideoParamAll = { | ||
212 | videoObject: { id: string } | string | ||
213 | syncParam?: SyncParam | ||
214 | fetchType?: 'all' | ||
215 | allowRefresh?: boolean | ||
216 | } | ||
217 | |||
218 | type GetVideoParamImmutable = { | ||
219 | videoObject: { id: string } | string | ||
220 | syncParam?: SyncParam | ||
221 | fetchType: 'only-immutable-attributes' | ||
222 | allowRefresh: false | ||
223 | } | ||
224 | |||
225 | type GetVideoParamOther = { | ||
226 | videoObject: { id: string } | string | ||
227 | syncParam?: SyncParam | ||
228 | fetchType?: 'all' | 'only-video' | ||
229 | allowRefresh?: boolean | ||
230 | } | ||
231 | |||
232 | function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles> | ||
233 | function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable> | ||
234 | function getOrCreateVideoAndAccountAndChannel ( | ||
235 | options: GetVideoParamOther | ||
236 | ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> | ||
237 | async function getOrCreateVideoAndAccountAndChannel ( | ||
238 | options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther | ||
239 | ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { | ||
240 | // Default params | ||
241 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | ||
242 | const fetchType = options.fetchType || 'all' | ||
243 | const allowRefresh = options.allowRefresh !== false | ||
244 | |||
245 | // Get video url | ||
246 | const videoUrl = getAPId(options.videoObject) | ||
247 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | ||
248 | |||
249 | if (videoFromDatabase) { | ||
250 | // If allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type | ||
251 | if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) { | ||
252 | const refreshOptions = { | ||
253 | video: videoFromDatabase as MVideoThumbnail, | ||
254 | fetchedType: fetchType, | ||
255 | syncParam | ||
256 | } | ||
257 | |||
258 | if (syncParam.refreshVideo === true) { | ||
259 | videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | ||
260 | } else { | ||
261 | await JobQueue.Instance.createJobWithPromise({ | ||
262 | type: 'activitypub-refresher', | ||
263 | payload: { type: 'video', url: videoFromDatabase.url } | ||
264 | }) | ||
265 | } | ||
266 | } | ||
267 | |||
268 | return { video: videoFromDatabase, created: false } | ||
269 | } | ||
270 | |||
271 | const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) | ||
272 | if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl) | ||
273 | |||
274 | const actor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo) | ||
275 | const videoChannel = actor.VideoChannel | ||
276 | |||
277 | try { | ||
278 | const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(createVideo, fetchedVideo, videoChannel, syncParam.thumbnail) | ||
279 | |||
280 | await syncVideoExternalAttributes(videoCreated, fetchedVideo, syncParam) | ||
281 | |||
282 | return { video: videoCreated, created: true, autoBlacklisted } | ||
283 | } catch (err) { | ||
284 | // Maybe a concurrent getOrCreateVideoAndAccountAndChannel call created this video | ||
285 | if (err.name === 'SequelizeUniqueConstraintError') { | ||
286 | const fallbackVideo = await fetchVideoByUrl(videoUrl, fetchType) | ||
287 | if (fallbackVideo) return { video: fallbackVideo, created: false } | ||
288 | } | ||
289 | |||
290 | throw err | ||
291 | } | ||
292 | } | ||
293 | |||
294 | async function updateVideoFromAP (options: { | ||
295 | video: MVideoAccountLightBlacklistAllFiles | ||
296 | videoObject: VideoObject | ||
297 | account: MAccountIdActor | ||
298 | channel: MChannelDefault | ||
299 | overrideTo?: string[] | ||
300 | }) { | ||
301 | const { video, videoObject, account, channel, overrideTo } = options | ||
302 | |||
303 | logger.debug('Updating remote video "%s".', options.videoObject.uuid, { videoObject: options.videoObject, account, channel }) | ||
304 | |||
305 | let videoFieldsSave: any | ||
306 | const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE | ||
307 | const wasUnlistedVideo = video.privacy === VideoPrivacy.UNLISTED | ||
308 | |||
309 | try { | ||
310 | let thumbnailModel: MThumbnail | ||
311 | |||
312 | try { | ||
313 | thumbnailModel = await createVideoMiniatureFromUrl({ | ||
314 | downloadUrl: getThumbnailFromIcons(videoObject).url, | ||
315 | video, | ||
316 | type: ThumbnailType.MINIATURE | ||
317 | }) | ||
318 | } catch (err) { | ||
319 | logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) | ||
320 | } | ||
321 | |||
322 | const videoUpdated = await sequelizeTypescript.transaction(async t => { | ||
323 | const sequelizeOptions = { transaction: t } | ||
324 | |||
325 | videoFieldsSave = video.toJSON() | ||
326 | |||
327 | // Check we can update the channel: we trust the remote server | ||
328 | const oldVideoChannel = video.VideoChannel | ||
329 | |||
330 | if (!oldVideoChannel.Actor.serverId || !channel.Actor.serverId) { | ||
331 | throw new Error('Cannot check old channel/new channel validity because `serverId` is null') | ||
332 | } | ||
333 | |||
334 | if (oldVideoChannel.Actor.serverId !== channel.Actor.serverId) { | ||
335 | throw new Error('New channel ' + channel.Actor.url + ' is not on the same server than new channel ' + oldVideoChannel.Actor.url) | ||
336 | } | ||
337 | |||
338 | const to = overrideTo || videoObject.to | ||
339 | const videoData = videoActivityObjectToDBAttributes(channel, videoObject, to) | ||
340 | video.name = videoData.name | ||
341 | video.uuid = videoData.uuid | ||
342 | video.url = videoData.url | ||
343 | video.category = videoData.category | ||
344 | video.licence = videoData.licence | ||
345 | video.language = videoData.language | ||
346 | video.description = videoData.description | ||
347 | video.support = videoData.support | ||
348 | video.nsfw = videoData.nsfw | ||
349 | video.commentsEnabled = videoData.commentsEnabled | ||
350 | video.downloadEnabled = videoData.downloadEnabled | ||
351 | video.waitTranscoding = videoData.waitTranscoding | ||
352 | video.state = videoData.state | ||
353 | video.duration = videoData.duration | ||
354 | video.createdAt = videoData.createdAt | ||
355 | video.publishedAt = videoData.publishedAt | ||
356 | video.originallyPublishedAt = videoData.originallyPublishedAt | ||
357 | video.privacy = videoData.privacy | ||
358 | video.channelId = videoData.channelId | ||
359 | video.views = videoData.views | ||
360 | video.isLive = videoData.isLive | ||
361 | |||
362 | // Ensures we update the updated video attribute | ||
363 | video.changed('updatedAt', true) | ||
364 | |||
365 | const videoUpdated = await video.save(sequelizeOptions) as MVideoFullLight | ||
366 | |||
367 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
368 | |||
369 | const previewIcon = getPreviewFromIcons(videoObject) | ||
370 | if (videoUpdated.getPreview() && previewIcon) { | ||
371 | const previewModel = createPlaceholderThumbnail({ | ||
372 | fileUrl: previewIcon.url, | ||
373 | video, | ||
374 | type: ThumbnailType.PREVIEW, | ||
375 | size: previewIcon | ||
376 | }) | ||
377 | await videoUpdated.addAndSaveThumbnail(previewModel, t) | ||
378 | } | ||
379 | |||
380 | { | ||
381 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) | ||
382 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | ||
383 | |||
384 | // Remove video files that do not exist anymore | ||
385 | const destroyTasks = deleteNonExistingModels(videoUpdated.VideoFiles, newVideoFiles, t) | ||
386 | await Promise.all(destroyTasks) | ||
387 | |||
388 | // Update or add other one | ||
389 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t)) | ||
390 | videoUpdated.VideoFiles = await Promise.all(upsertTasks) | ||
391 | } | ||
392 | |||
393 | { | ||
394 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles) | ||
395 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
396 | |||
397 | // Remove video playlists that do not exist anymore | ||
398 | const destroyTasks = deleteNonExistingModels(videoUpdated.VideoStreamingPlaylists, newStreamingPlaylists, t) | ||
399 | await Promise.all(destroyTasks) | ||
400 | |||
401 | let oldStreamingPlaylistFiles: MVideoFile[] = [] | ||
402 | for (const videoStreamingPlaylist of videoUpdated.VideoStreamingPlaylists) { | ||
403 | oldStreamingPlaylistFiles = oldStreamingPlaylistFiles.concat(videoStreamingPlaylist.VideoFiles) | ||
404 | } | ||
405 | |||
406 | videoUpdated.VideoStreamingPlaylists = [] | ||
407 | |||
408 | for (const playlistAttributes of streamingPlaylistAttributes) { | ||
409 | const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t }) | ||
410 | .then(([ streamingPlaylist ]) => streamingPlaylist as MStreamingPlaylistFilesVideo) | ||
411 | streamingPlaylistModel.Video = videoUpdated | ||
412 | |||
413 | const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject) | ||
414 | .map(a => new VideoFileModel(a)) | ||
415 | const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t) | ||
416 | await Promise.all(destroyTasks) | ||
417 | |||
418 | // Update or add other one | ||
419 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t)) | ||
420 | streamingPlaylistModel.VideoFiles = await Promise.all(upsertTasks) | ||
421 | |||
422 | videoUpdated.VideoStreamingPlaylists.push(streamingPlaylistModel) | ||
423 | } | ||
424 | } | ||
425 | |||
426 | { | ||
427 | // Update Tags | ||
428 | const tags = videoObject.tag | ||
429 | .filter(isAPHashTagObject) | ||
430 | .map(tag => tag.name) | ||
431 | await setVideoTags({ video: videoUpdated, tags, transaction: t }) | ||
432 | } | ||
433 | |||
434 | // Update trackers | ||
435 | { | ||
436 | const trackers = getTrackerUrls(videoObject, videoUpdated) | ||
437 | await setVideoTrackers({ video: videoUpdated, trackers, transaction: t }) | ||
438 | } | ||
439 | |||
440 | { | ||
441 | // Update captions | ||
442 | await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) | ||
443 | |||
444 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { | ||
445 | const caption = new VideoCaptionModel({ | ||
446 | videoId: videoUpdated.id, | ||
447 | filename: VideoCaptionModel.generateCaptionName(c.identifier), | ||
448 | language: c.identifier, | ||
449 | fileUrl: c.url | ||
450 | }) as MVideoCaption | ||
451 | |||
452 | return VideoCaptionModel.insertOrReplaceLanguage(caption, t) | ||
453 | }) | ||
454 | await Promise.all(videoCaptionsPromises) | ||
455 | } | ||
456 | |||
457 | { | ||
458 | // Create or update existing live | ||
459 | if (video.isLive) { | ||
460 | const [ videoLive ] = await VideoLiveModel.upsert({ | ||
461 | saveReplay: videoObject.liveSaveReplay, | ||
462 | permanentLive: videoObject.permanentLive, | ||
463 | videoId: video.id | ||
464 | }, { transaction: t, returning: true }) | ||
465 | |||
466 | videoUpdated.VideoLive = videoLive | ||
467 | } else { // Delete existing live if it exists | ||
468 | await VideoLiveModel.destroy({ | ||
469 | where: { | ||
470 | videoId: video.id | ||
471 | }, | ||
472 | transaction: t | ||
473 | }) | ||
474 | |||
475 | videoUpdated.VideoLive = null | ||
476 | } | ||
477 | } | ||
478 | |||
479 | return videoUpdated | ||
480 | }) | ||
481 | |||
482 | await autoBlacklistVideoIfNeeded({ | ||
483 | video: videoUpdated, | ||
484 | user: undefined, | ||
485 | isRemote: true, | ||
486 | isNew: false, | ||
487 | transaction: undefined | ||
488 | }) | ||
489 | |||
490 | // Notify our users? | ||
491 | if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) | ||
492 | |||
493 | if (videoUpdated.isLive) { | ||
494 | PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) | ||
495 | PeerTubeSocket.Instance.sendVideoViewsUpdate(videoUpdated) | ||
496 | } | ||
497 | |||
498 | logger.info('Remote video with uuid %s updated', videoObject.uuid) | ||
499 | |||
500 | return videoUpdated | ||
501 | } catch (err) { | ||
502 | if (video !== undefined && videoFieldsSave !== undefined) { | ||
503 | resetSequelizeInstance(video, videoFieldsSave) | ||
504 | } | ||
505 | |||
506 | // This is just a debug because we will retry the insert | ||
507 | logger.debug('Cannot update the remote video.', { err }) | ||
508 | throw err | ||
509 | } | ||
510 | } | ||
511 | |||
512 | async function refreshVideoIfNeeded (options: { | ||
513 | video: MVideoThumbnail | ||
514 | fetchedType: VideoFetchByUrlType | ||
515 | syncParam: SyncParam | ||
516 | }): Promise<MVideoThumbnail> { | ||
517 | if (!options.video.isOutdated()) return options.video | ||
518 | |||
519 | // We need more attributes if the argument video was fetched with not enough joints | ||
520 | const video = options.fetchedType === 'all' | ||
521 | ? options.video as MVideoAccountLightBlacklistAllFiles | ||
522 | : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
523 | |||
524 | try { | ||
525 | const { videoObject } = await fetchRemoteVideo(video.url) | ||
526 | |||
527 | if (videoObject === undefined) { | ||
528 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url) | ||
529 | |||
530 | await video.setAsRefreshed() | ||
531 | return video | ||
532 | } | ||
533 | |||
534 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | ||
535 | |||
536 | const updateOptions = { | ||
537 | video, | ||
538 | videoObject, | ||
539 | account: channelActor.VideoChannel.Account, | ||
540 | channel: channelActor.VideoChannel | ||
541 | } | ||
542 | await updateVideoFromAP(updateOptions) | ||
543 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
544 | |||
545 | ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId) | ||
546 | |||
547 | return video | ||
548 | } catch (err) { | ||
549 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
550 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url) | ||
551 | |||
552 | // Video does not exist anymore | ||
553 | await video.destroy() | ||
554 | return undefined | ||
555 | } | ||
556 | |||
557 | logger.warn('Cannot refresh video %s.', options.video.url, { err }) | ||
558 | |||
559 | ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) | ||
560 | |||
561 | // Don't refresh in loop | ||
562 | await video.setAsRefreshed() | ||
563 | return video | ||
564 | } | ||
565 | } | ||
566 | |||
567 | export { | ||
568 | updateVideoFromAP, | ||
569 | refreshVideoIfNeeded, | ||
570 | federateVideoIfNeeded, | ||
571 | fetchRemoteVideo, | ||
572 | getOrCreateVideoAndAccountAndChannel, | ||
573 | fetchRemoteVideoDescription, | ||
574 | getOrCreateVideoChannelFromVideoObject | ||
575 | } | ||
576 | |||
577 | // --------------------------------------------------------------------------- | ||
578 | |||
579 | function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject { | ||
580 | const urlMediaType = url.mediaType | ||
581 | |||
582 | return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/') | ||
583 | } | ||
584 | |||
585 | function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject { | ||
586 | return url && url.mediaType === 'application/x-mpegURL' | ||
587 | } | ||
588 | |||
589 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | ||
590 | return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json' | ||
591 | } | ||
592 | |||
593 | function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject { | ||
594 | return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' | ||
595 | } | ||
596 | |||
597 | function isAPHashTagObject (url: any): url is ActivityHashTagObject { | ||
598 | return url && url.type === 'Hashtag' | ||
599 | } | ||
600 | |||
601 | async function createVideo (videoObject: VideoObject, channel: MChannelAccountLight, waitThumbnail = false) { | ||
602 | logger.debug('Adding remote video %s.', videoObject.id) | ||
603 | |||
604 | const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) | ||
605 | const video = VideoModel.build(videoData) as MVideoThumbnail | ||
606 | |||
607 | const promiseThumbnail = createVideoMiniatureFromUrl({ | ||
608 | downloadUrl: getThumbnailFromIcons(videoObject).url, | ||
609 | video, | ||
610 | type: ThumbnailType.MINIATURE | ||
611 | }).catch(err => { | ||
612 | logger.error('Cannot create miniature from url.', { err }) | ||
613 | return undefined | ||
614 | }) | ||
615 | |||
616 | let thumbnailModel: MThumbnail | ||
617 | if (waitThumbnail === true) { | ||
618 | thumbnailModel = await promiseThumbnail | ||
619 | } | ||
620 | |||
621 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
622 | try { | ||
623 | const sequelizeOptions = { transaction: t } | ||
624 | |||
625 | const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight | ||
626 | videoCreated.VideoChannel = channel | ||
627 | |||
628 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
629 | |||
630 | const previewIcon = getPreviewFromIcons(videoObject) | ||
631 | if (previewIcon) { | ||
632 | const previewModel = createPlaceholderThumbnail({ | ||
633 | fileUrl: previewIcon.url, | ||
634 | video: videoCreated, | ||
635 | type: ThumbnailType.PREVIEW, | ||
636 | size: previewIcon | ||
637 | }) | ||
638 | |||
639 | await videoCreated.addAndSaveThumbnail(previewModel, t) | ||
640 | } | ||
641 | |||
642 | // Process files | ||
643 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) | ||
644 | |||
645 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | ||
646 | const videoFiles = await Promise.all(videoFilePromises) | ||
647 | |||
648 | const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles) | ||
649 | videoCreated.VideoStreamingPlaylists = [] | ||
650 | |||
651 | for (const playlistAttributes of streamingPlaylistsAttributes) { | ||
652 | const playlist = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t }) as MStreamingPlaylistFilesVideo | ||
653 | playlist.Video = videoCreated | ||
654 | |||
655 | const playlistFiles = videoFileActivityUrlToDBAttributes(playlist, playlistAttributes.tagAPObject) | ||
656 | const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t })) | ||
657 | playlist.VideoFiles = await Promise.all(videoFilePromises) | ||
658 | |||
659 | videoCreated.VideoStreamingPlaylists.push(playlist) | ||
660 | } | ||
661 | |||
662 | // Process tags | ||
663 | const tags = videoObject.tag | ||
664 | .filter(isAPHashTagObject) | ||
665 | .map(t => t.name) | ||
666 | await setVideoTags({ video: videoCreated, tags, transaction: t }) | ||
667 | |||
668 | // Process captions | ||
669 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { | ||
670 | const caption = new VideoCaptionModel({ | ||
671 | videoId: videoCreated.id, | ||
672 | filename: VideoCaptionModel.generateCaptionName(c.identifier), | ||
673 | language: c.identifier, | ||
674 | fileUrl: c.url | ||
675 | }) as MVideoCaption | ||
676 | |||
677 | return VideoCaptionModel.insertOrReplaceLanguage(caption, t) | ||
678 | }) | ||
679 | await Promise.all(videoCaptionsPromises) | ||
680 | |||
681 | // Process trackers | ||
682 | { | ||
683 | const trackers = getTrackerUrls(videoObject, videoCreated) | ||
684 | await setVideoTrackers({ video: videoCreated, trackers, transaction: t }) | ||
685 | } | ||
686 | |||
687 | videoCreated.VideoFiles = videoFiles | ||
688 | |||
689 | if (videoCreated.isLive) { | ||
690 | const videoLive = new VideoLiveModel({ | ||
691 | streamKey: null, | ||
692 | saveReplay: videoObject.liveSaveReplay, | ||
693 | permanentLive: videoObject.permanentLive, | ||
694 | videoId: videoCreated.id | ||
695 | }) | ||
696 | |||
697 | videoCreated.VideoLive = await videoLive.save({ transaction: t }) | ||
698 | } | ||
699 | |||
700 | // We added a video in this channel, set it as updated | ||
701 | await channel.setAsUpdated(t) | ||
702 | |||
703 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | ||
704 | video: videoCreated, | ||
705 | user: undefined, | ||
706 | isRemote: true, | ||
707 | isNew: true, | ||
708 | transaction: t | ||
709 | }) | ||
710 | |||
711 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) | ||
712 | |||
713 | return { autoBlacklisted, videoCreated } | ||
714 | } catch (err) { | ||
715 | // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released | ||
716 | // Remove thumbnail | ||
717 | if (thumbnailModel) await thumbnailModel.removeThumbnail() | ||
718 | |||
719 | throw err | ||
720 | } | ||
721 | }) | ||
722 | |||
723 | if (waitThumbnail === false) { | ||
724 | // Error is already caught above | ||
725 | // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
726 | promiseThumbnail.then(thumbnailModel => { | ||
727 | if (!thumbnailModel) return | ||
728 | |||
729 | thumbnailModel = videoCreated.id | ||
730 | |||
731 | return thumbnailModel.save() | ||
732 | }) | ||
733 | } | ||
734 | |||
735 | return { autoBlacklisted, videoCreated } | ||
736 | } | ||
737 | |||
738 | function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) { | ||
739 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | ||
740 | ? VideoPrivacy.PUBLIC | ||
741 | : VideoPrivacy.UNLISTED | ||
742 | |||
743 | const duration = videoObject.duration.replace(/[^\d]+/, '') | ||
744 | const language = videoObject.language?.identifier | ||
745 | |||
746 | const category = videoObject.category | ||
747 | ? parseInt(videoObject.category.identifier, 10) | ||
748 | : undefined | ||
749 | |||
750 | const licence = videoObject.licence | ||
751 | ? parseInt(videoObject.licence.identifier, 10) | ||
752 | : undefined | ||
753 | |||
754 | const description = videoObject.content || null | ||
755 | const support = videoObject.support || null | ||
756 | |||
757 | return { | ||
758 | name: videoObject.name, | ||
759 | uuid: videoObject.uuid, | ||
760 | url: videoObject.id, | ||
761 | category, | ||
762 | licence, | ||
763 | language, | ||
764 | description, | ||
765 | support, | ||
766 | nsfw: videoObject.sensitive, | ||
767 | commentsEnabled: videoObject.commentsEnabled, | ||
768 | downloadEnabled: videoObject.downloadEnabled, | ||
769 | waitTranscoding: videoObject.waitTranscoding, | ||
770 | isLive: videoObject.isLiveBroadcast, | ||
771 | state: videoObject.state, | ||
772 | channelId: videoChannel.id, | ||
773 | duration: parseInt(duration, 10), | ||
774 | createdAt: new Date(videoObject.published), | ||
775 | publishedAt: new Date(videoObject.published), | ||
776 | |||
777 | originallyPublishedAt: videoObject.originallyPublishedAt | ||
778 | ? new Date(videoObject.originallyPublishedAt) | ||
779 | : null, | ||
780 | |||
781 | updatedAt: new Date(videoObject.updated), | ||
782 | views: videoObject.views, | ||
783 | likes: 0, | ||
784 | dislikes: 0, | ||
785 | remote: true, | ||
786 | privacy | ||
787 | } | ||
788 | } | ||
789 | |||
790 | function videoFileActivityUrlToDBAttributes ( | ||
791 | videoOrPlaylist: MVideo | MStreamingPlaylistVideo, | ||
792 | urls: (ActivityTagObject | ActivityUrlObject)[] | ||
793 | ) { | ||
794 | const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] | ||
795 | |||
796 | if (fileUrls.length === 0) return [] | ||
797 | |||
798 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] | ||
799 | for (const fileUrl of fileUrls) { | ||
800 | // Fetch associated magnet uri | ||
801 | const magnet = urls.filter(isAPMagnetUrlObject) | ||
802 | .find(u => u.height === fileUrl.height) | ||
803 | |||
804 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) | ||
805 | |||
806 | const parsed = magnetUtil.decode(magnet.href) | ||
807 | if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) { | ||
808 | throw new Error('Cannot parse magnet URI ' + magnet.href) | ||
809 | } | ||
810 | |||
811 | const torrentUrl = Array.isArray(parsed.xs) | ||
812 | ? parsed.xs[0] | ||
813 | : parsed.xs | ||
814 | |||
815 | // Fetch associated metadata url, if any | ||
816 | const metadata = urls.filter(isAPVideoFileUrlMetadataObject) | ||
817 | .find(u => { | ||
818 | return u.height === fileUrl.height && | ||
819 | u.fps === fileUrl.fps && | ||
820 | u.rel.includes(fileUrl.mediaType) | ||
821 | }) | ||
822 | |||
823 | const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType) | ||
824 | const resolution = fileUrl.height | ||
825 | const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id | ||
826 | const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null | ||
827 | |||
828 | const attribute = { | ||
829 | extname, | ||
830 | infoHash: parsed.infoHash, | ||
831 | resolution, | ||
832 | size: fileUrl.size, | ||
833 | fps: fileUrl.fps || -1, | ||
834 | metadataUrl: metadata?.href, | ||
835 | |||
836 | // Use the name of the remote file because we don't proxify video file requests | ||
837 | filename: basename(fileUrl.href), | ||
838 | fileUrl: fileUrl.href, | ||
839 | |||
840 | torrentUrl, | ||
841 | // Use our own torrent name since we proxify torrent requests | ||
842 | torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution), | ||
843 | |||
844 | // This is a video file owned by a video or by a streaming playlist | ||
845 | videoId, | ||
846 | videoStreamingPlaylistId | ||
847 | } | ||
848 | |||
849 | attributes.push(attribute) | ||
850 | } | ||
851 | |||
852 | return attributes | ||
853 | } | ||
854 | |||
855 | function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) { | ||
856 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | ||
857 | if (playlistUrls.length === 0) return [] | ||
858 | |||
859 | const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = [] | ||
860 | for (const playlistUrlObject of playlistUrls) { | ||
861 | const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject) | ||
862 | |||
863 | let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] | ||
864 | |||
865 | // FIXME: backward compatibility introduced in v2.1.0 | ||
866 | if (files.length === 0) files = videoFiles | ||
867 | |||
868 | if (!segmentsSha256UrlObject) { | ||
869 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
870 | continue | ||
871 | } | ||
872 | |||
873 | const attribute = { | ||
874 | type: VideoStreamingPlaylistType.HLS, | ||
875 | playlistUrl: playlistUrlObject.href, | ||
876 | segmentsSha256Url: segmentsSha256UrlObject.href, | ||
877 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files), | ||
878 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | ||
879 | videoId: video.id, | ||
880 | tagAPObject: playlistUrlObject.tag | ||
881 | } | ||
882 | |||
883 | attributes.push(attribute) | ||
884 | } | ||
885 | |||
886 | return attributes | ||
887 | } | ||
888 | |||
889 | function getThumbnailFromIcons (videoObject: VideoObject) { | ||
890 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) | ||
891 | // Fallback if there are not valid icons | ||
892 | if (validIcons.length === 0) validIcons = videoObject.icon | ||
893 | |||
894 | return minBy(validIcons, 'width') | ||
895 | } | ||
896 | |||
897 | function getPreviewFromIcons (videoObject: VideoObject) { | ||
898 | const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth) | ||
899 | |||
900 | return maxBy(validIcons, 'width') | ||
901 | } | ||
902 | |||
903 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { | ||
904 | let wsFound = false | ||
905 | |||
906 | const trackers = object.url.filter(u => isAPVideoTrackerUrlObject(u)) | ||
907 | .map((u: ActivityTrackerUrlObject) => { | ||
908 | if (isArray(u.rel) && u.rel.includes('websocket')) wsFound = true | ||
909 | |||
910 | return u.href | ||
911 | }) | ||
912 | |||
913 | if (wsFound) return trackers | ||
914 | |||
915 | return [ | ||
916 | buildRemoteVideoBaseUrl(video, '/tracker/socket', REMOTE_SCHEME.WS), | ||
917 | buildRemoteVideoBaseUrl(video, '/tracker/announce') | ||
918 | ] | ||
919 | } | ||
920 | |||
921 | async function setVideoTrackers (options: { | ||
922 | video: MVideo | ||
923 | trackers: string[] | ||
924 | transaction?: Transaction | ||
925 | }) { | ||
926 | const { video, trackers, transaction } = options | ||
927 | |||
928 | const trackerInstances = await TrackerModel.findOrCreateTrackers(trackers, transaction) | ||
929 | |||
930 | await video.$set('Trackers', trackerInstances, { transaction }) | ||
931 | } | ||
diff --git a/server/lib/activitypub/videos/federate.ts b/server/lib/activitypub/videos/federate.ts new file mode 100644 index 000000000..bd0c54b0c --- /dev/null +++ b/server/lib/activitypub/videos/federate.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
3 | import { MVideoAP, MVideoAPWithoutCaption } from '@server/types/models' | ||
4 | import { sendCreateVideo, sendUpdateVideo } from '../send' | ||
5 | import { shareVideoByServerAndChannel } from '../share' | ||
6 | |||
7 | async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: Transaction) { | ||
8 | const video = videoArg as MVideoAP | ||
9 | |||
10 | if ( | ||
11 | // Check this is not a blacklisted video, or unfederated blacklisted video | ||
12 | (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && | ||
13 | // Check the video is public/unlisted and published | ||
14 | video.hasPrivacyForFederation() && video.hasStateForFederation() | ||
15 | ) { | ||
16 | // Fetch more attributes that we will need to serialize in AP object | ||
17 | if (isArray(video.VideoCaptions) === false) { | ||
18 | video.VideoCaptions = await video.$get('VideoCaptions', { | ||
19 | attributes: [ 'filename', 'language' ], | ||
20 | transaction | ||
21 | }) | ||
22 | } | ||
23 | |||
24 | if (isNewVideo) { | ||
25 | // Now we'll add the video's meta data to our followers | ||
26 | await sendCreateVideo(video, transaction) | ||
27 | await shareVideoByServerAndChannel(video, transaction) | ||
28 | } else { | ||
29 | await sendUpdateVideo(video, transaction) | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | |||
34 | export { | ||
35 | federateVideoIfNeeded | ||
36 | } | ||
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts new file mode 100644 index 000000000..f3e2f0625 --- /dev/null +++ b/server/lib/activitypub/videos/get.ts | |||
@@ -0,0 +1,113 @@ | |||
1 | import { getAPId } from '@server/helpers/activitypub' | ||
2 | import { retryTransactionWrapper } from '@server/helpers/database-utils' | ||
3 | import { JobQueue } from '@server/lib/job-queue' | ||
4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' | ||
5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' | ||
6 | import { APObject } from '@shared/models' | ||
7 | import { refreshVideoIfNeeded } from './refresh' | ||
8 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' | ||
9 | |||
10 | type GetVideoResult <T> = Promise<{ | ||
11 | video: T | ||
12 | created: boolean | ||
13 | autoBlacklisted?: boolean | ||
14 | }> | ||
15 | |||
16 | type GetVideoParamAll = { | ||
17 | videoObject: APObject | ||
18 | syncParam?: SyncParam | ||
19 | fetchType?: 'all' | ||
20 | allowRefresh?: boolean | ||
21 | } | ||
22 | |||
23 | type GetVideoParamImmutable = { | ||
24 | videoObject: APObject | ||
25 | syncParam?: SyncParam | ||
26 | fetchType: 'only-immutable-attributes' | ||
27 | allowRefresh: false | ||
28 | } | ||
29 | |||
30 | type GetVideoParamOther = { | ||
31 | videoObject: APObject | ||
32 | syncParam?: SyncParam | ||
33 | fetchType?: 'all' | 'only-video' | ||
34 | allowRefresh?: boolean | ||
35 | } | ||
36 | |||
37 | function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles> | ||
38 | function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable> | ||
39 | function getOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> | ||
40 | |||
41 | async function getOrCreateAPVideo ( | ||
42 | options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther | ||
43 | ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { | ||
44 | // Default params | ||
45 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | ||
46 | const fetchType = options.fetchType || 'all' | ||
47 | const allowRefresh = options.allowRefresh !== false | ||
48 | |||
49 | // Get video url | ||
50 | const videoUrl = getAPId(options.videoObject) | ||
51 | let videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType) | ||
52 | |||
53 | if (videoFromDatabase) { | ||
54 | if (allowRefresh === true) { | ||
55 | // Typings ensure allowRefresh === false in only-immutable-attributes fetch type | ||
56 | videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam) | ||
57 | } | ||
58 | |||
59 | return { video: videoFromDatabase, created: false } | ||
60 | } | ||
61 | |||
62 | const { videoObject } = await fetchRemoteVideo(videoUrl) | ||
63 | if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl) | ||
64 | |||
65 | // videoUrl is just an alias/rediraction, so process object id instead | ||
66 | if (videoObject.id !== videoUrl) return getOrCreateAPVideo({ ...options, fetchType: 'all', videoObject }) | ||
67 | |||
68 | try { | ||
69 | const creator = new APVideoCreator(videoObject) | ||
70 | const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(creator.create.bind(creator), syncParam.thumbnail) | ||
71 | |||
72 | await syncVideoExternalAttributes(videoCreated, videoObject, syncParam) | ||
73 | |||
74 | return { video: videoCreated, created: true, autoBlacklisted } | ||
75 | } catch (err) { | ||
76 | // Maybe a concurrent getOrCreateAPVideo call created this video | ||
77 | if (err.name === 'SequelizeUniqueConstraintError') { | ||
78 | const alreadyCreatedVideo = await loadVideoByUrl(videoUrl, fetchType) | ||
79 | if (alreadyCreatedVideo) return { video: alreadyCreatedVideo, created: false } | ||
80 | } | ||
81 | |||
82 | throw err | ||
83 | } | ||
84 | } | ||
85 | |||
86 | // --------------------------------------------------------------------------- | ||
87 | |||
88 | export { | ||
89 | getOrCreateAPVideo | ||
90 | } | ||
91 | |||
92 | // --------------------------------------------------------------------------- | ||
93 | |||
94 | async function scheduleRefresh (video: MVideoThumbnail, fetchType: VideoLoadByUrlType, syncParam: SyncParam) { | ||
95 | if (!video.isOutdated()) return video | ||
96 | |||
97 | const refreshOptions = { | ||
98 | video, | ||
99 | fetchedType: fetchType, | ||
100 | syncParam | ||
101 | } | ||
102 | |||
103 | if (syncParam.refreshVideo === true) { | ||
104 | return refreshVideoIfNeeded(refreshOptions) | ||
105 | } | ||
106 | |||
107 | await JobQueue.Instance.createJobWithPromise({ | ||
108 | type: 'activitypub-refresher', | ||
109 | payload: { type: 'video', url: video.url } | ||
110 | }) | ||
111 | |||
112 | return video | ||
113 | } | ||
diff --git a/server/lib/activitypub/videos/index.ts b/server/lib/activitypub/videos/index.ts new file mode 100644 index 000000000..b22062598 --- /dev/null +++ b/server/lib/activitypub/videos/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './federate' | ||
2 | export * from './get' | ||
3 | export * from './refresh' | ||
4 | export * from './updater' | ||
diff --git a/server/lib/activitypub/videos/refresh.ts b/server/lib/activitypub/videos/refresh.ts new file mode 100644 index 000000000..a7b82f286 --- /dev/null +++ b/server/lib/activitypub/videos/refresh.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
2 | import { PeerTubeRequestError } from '@server/helpers/requests' | ||
3 | import { ActorFollowScoreCache } from '@server/lib/files-cache' | ||
4 | import { VideoLoadByUrlType } from '@server/lib/model-loaders' | ||
5 | import { VideoModel } from '@server/models/video/video' | ||
6 | import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models' | ||
7 | import { HttpStatusCode } from '@shared/core-utils' | ||
8 | import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' | ||
9 | import { APVideoUpdater } from './updater' | ||
10 | |||
11 | async function refreshVideoIfNeeded (options: { | ||
12 | video: MVideoThumbnail | ||
13 | fetchedType: VideoLoadByUrlType | ||
14 | syncParam: SyncParam | ||
15 | }): Promise<MVideoThumbnail> { | ||
16 | if (!options.video.isOutdated()) return options.video | ||
17 | |||
18 | // We need more attributes if the argument video was fetched with not enough joints | ||
19 | const video = options.fetchedType === 'all' | ||
20 | ? options.video as MVideoAccountLightBlacklistAllFiles | ||
21 | : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) | ||
22 | |||
23 | const lTags = loggerTagsFactory('ap', 'video', 'refresh', video.uuid, video.url) | ||
24 | |||
25 | logger.info('Refreshing video %s.', video.url, lTags()) | ||
26 | |||
27 | try { | ||
28 | const { videoObject } = await fetchRemoteVideo(video.url) | ||
29 | |||
30 | if (videoObject === undefined) { | ||
31 | logger.warn('Cannot refresh remote video %s: invalid body.', video.url, lTags()) | ||
32 | |||
33 | await video.setAsRefreshed() | ||
34 | return video | ||
35 | } | ||
36 | |||
37 | const videoUpdater = new APVideoUpdater(videoObject, video) | ||
38 | await videoUpdater.update() | ||
39 | |||
40 | await syncVideoExternalAttributes(video, videoObject, options.syncParam) | ||
41 | |||
42 | ActorFollowScoreCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId) | ||
43 | |||
44 | return video | ||
45 | } catch (err) { | ||
46 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
47 | logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url, lTags()) | ||
48 | |||
49 | // Video does not exist anymore | ||
50 | await video.destroy() | ||
51 | return undefined | ||
52 | } | ||
53 | |||
54 | logger.warn('Cannot refresh video %s.', options.video.url, { err, ...lTags() }) | ||
55 | |||
56 | ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) | ||
57 | |||
58 | // Don't refresh in loop | ||
59 | await video.setAsRefreshed() | ||
60 | return video | ||
61 | } | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | export { | ||
67 | refreshVideoIfNeeded | ||
68 | } | ||
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts new file mode 100644 index 000000000..e89c94bcd --- /dev/null +++ b/server/lib/activitypub/videos/shared/abstract-builder.ts | |||
@@ -0,0 +1,173 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | ||
3 | import { deleteNonExistingModels } from '@server/helpers/database-utils' | ||
4 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | ||
5 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' | ||
6 | import { setVideoTags } from '@server/lib/video' | ||
7 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
8 | import { VideoFileModel } from '@server/models/video/video-file' | ||
9 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
10 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | ||
11 | import { MStreamingPlaylistFilesVideo, MThumbnail, MVideoCaption, MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models' | ||
12 | import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' | ||
13 | import { getOrCreateAPActor } from '../../actors' | ||
14 | import { | ||
15 | getCaptionAttributesFromObject, | ||
16 | getFileAttributesFromUrl, | ||
17 | getLiveAttributesFromObject, | ||
18 | getPreviewFromIcons, | ||
19 | getStreamingPlaylistAttributesFromObject, | ||
20 | getTagsFromObject, | ||
21 | getThumbnailFromIcons | ||
22 | } from './object-to-model-attributes' | ||
23 | import { getTrackerUrls, setVideoTrackers } from './trackers' | ||
24 | |||
25 | export abstract class APVideoAbstractBuilder { | ||
26 | protected abstract videoObject: VideoObject | ||
27 | protected abstract lTags: LoggerTagsFn | ||
28 | |||
29 | protected async getOrCreateVideoChannelFromVideoObject () { | ||
30 | const channel = this.videoObject.attributedTo.find(a => a.type === 'Group') | ||
31 | if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url) | ||
32 | |||
33 | if (checkUrlsSameHost(channel.id, this.videoObject.id) !== true) { | ||
34 | throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${this.videoObject.id}`) | ||
35 | } | ||
36 | |||
37 | return getOrCreateAPActor(channel.id, 'all') | ||
38 | } | ||
39 | |||
40 | protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> { | ||
41 | return updateVideoMiniatureFromUrl({ | ||
42 | downloadUrl: getThumbnailFromIcons(this.videoObject).url, | ||
43 | video, | ||
44 | type: ThumbnailType.MINIATURE | ||
45 | }).catch(err => { | ||
46 | logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err, ...this.lTags() }) | ||
47 | |||
48 | return undefined | ||
49 | }) | ||
50 | } | ||
51 | |||
52 | protected async setPreview (video: MVideoFullLight, t?: Transaction) { | ||
53 | // Don't fetch the preview that could be big, create a placeholder instead | ||
54 | const previewIcon = getPreviewFromIcons(this.videoObject) | ||
55 | if (!previewIcon) return | ||
56 | |||
57 | const previewModel = updatePlaceholderThumbnail({ | ||
58 | fileUrl: previewIcon.url, | ||
59 | video, | ||
60 | type: ThumbnailType.PREVIEW, | ||
61 | size: previewIcon | ||
62 | }) | ||
63 | |||
64 | await video.addAndSaveThumbnail(previewModel, t) | ||
65 | } | ||
66 | |||
67 | protected async setTags (video: MVideoFullLight, t: Transaction) { | ||
68 | const tags = getTagsFromObject(this.videoObject) | ||
69 | await setVideoTags({ video, tags, transaction: t }) | ||
70 | } | ||
71 | |||
72 | protected async setTrackers (video: MVideoFullLight, t: Transaction) { | ||
73 | const trackers = getTrackerUrls(this.videoObject, video) | ||
74 | await setVideoTrackers({ video, trackers, transaction: t }) | ||
75 | } | ||
76 | |||
77 | protected async insertOrReplaceCaptions (video: MVideoFullLight, t: Transaction) { | ||
78 | const existingCaptions = await VideoCaptionModel.listVideoCaptions(video.id, t) | ||
79 | |||
80 | let captionsToCreate = getCaptionAttributesFromObject(video, this.videoObject) | ||
81 | .map(a => new VideoCaptionModel(a) as MVideoCaption) | ||
82 | |||
83 | for (const existingCaption of existingCaptions) { | ||
84 | // Only keep captions that do not already exist | ||
85 | const filtered = captionsToCreate.filter(c => !c.isEqual(existingCaption)) | ||
86 | |||
87 | // This caption already exists, we don't need to destroy and create it | ||
88 | if (filtered.length !== captionsToCreate.length) { | ||
89 | captionsToCreate = filtered | ||
90 | continue | ||
91 | } | ||
92 | |||
93 | // Destroy this caption that does not exist anymore | ||
94 | await existingCaption.destroy({ transaction: t }) | ||
95 | } | ||
96 | |||
97 | for (const captionToCreate of captionsToCreate) { | ||
98 | await captionToCreate.save({ transaction: t }) | ||
99 | } | ||
100 | } | ||
101 | |||
102 | protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) { | ||
103 | const attributes = getLiveAttributesFromObject(video, this.videoObject) | ||
104 | const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true }) | ||
105 | |||
106 | video.VideoLive = videoLive | ||
107 | } | ||
108 | |||
109 | protected async setWebTorrentFiles (video: MVideoFullLight, t: Transaction) { | ||
110 | const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url) | ||
111 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | ||
112 | |||
113 | // Remove video files that do not exist anymore | ||
114 | const destroyTasks = deleteNonExistingModels(video.VideoFiles || [], newVideoFiles, t) | ||
115 | await Promise.all(destroyTasks) | ||
116 | |||
117 | // Update or add other one | ||
118 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t)) | ||
119 | video.VideoFiles = await Promise.all(upsertTasks) | ||
120 | } | ||
121 | |||
122 | protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) { | ||
123 | const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject, video.VideoFiles || []) | ||
124 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
125 | |||
126 | // Remove video playlists that do not exist anymore | ||
127 | const destroyTasks = deleteNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists, t) | ||
128 | await Promise.all(destroyTasks) | ||
129 | |||
130 | video.VideoStreamingPlaylists = [] | ||
131 | |||
132 | for (const playlistAttributes of streamingPlaylistAttributes) { | ||
133 | |||
134 | const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t) | ||
135 | streamingPlaylistModel.Video = video | ||
136 | |||
137 | await this.setStreamingPlaylistFiles(video, streamingPlaylistModel, playlistAttributes.tagAPObject, t) | ||
138 | |||
139 | video.VideoStreamingPlaylists.push(streamingPlaylistModel) | ||
140 | } | ||
141 | } | ||
142 | |||
143 | private async insertOrReplaceStreamingPlaylist (attributes: VideoStreamingPlaylistModel['_creationAttributes'], t: Transaction) { | ||
144 | const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t }) | ||
145 | |||
146 | return streamingPlaylist as MStreamingPlaylistFilesVideo | ||
147 | } | ||
148 | |||
149 | private getStreamingPlaylistFiles (video: MVideoFullLight, type: VideoStreamingPlaylistType) { | ||
150 | const playlist = video.VideoStreamingPlaylists.find(s => s.type === type) | ||
151 | if (!playlist) return [] | ||
152 | |||
153 | return playlist.VideoFiles | ||
154 | } | ||
155 | |||
156 | private async setStreamingPlaylistFiles ( | ||
157 | video: MVideoFullLight, | ||
158 | playlistModel: MStreamingPlaylistFilesVideo, | ||
159 | tagObjects: ActivityTagObject[], | ||
160 | t: Transaction | ||
161 | ) { | ||
162 | const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(video, playlistModel.type) | ||
163 | |||
164 | const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a)) | ||
165 | |||
166 | const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t) | ||
167 | await Promise.all(destroyTasks) | ||
168 | |||
169 | // Update or add other one | ||
170 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t)) | ||
171 | playlistModel.VideoFiles = await Promise.all(upsertTasks) | ||
172 | } | ||
173 | } | ||
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts new file mode 100644 index 000000000..ad3b88936 --- /dev/null +++ b/server/lib/activitypub/videos/shared/creator.ts | |||
@@ -0,0 +1,88 @@ | |||
1 | |||
2 | import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' | ||
3 | import { sequelizeTypescript } from '@server/initializers/database' | ||
4 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | ||
5 | import { VideoModel } from '@server/models/video/video' | ||
6 | import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models' | ||
7 | import { VideoObject } from '@shared/models' | ||
8 | import { APVideoAbstractBuilder } from './abstract-builder' | ||
9 | import { getVideoAttributesFromObject } from './object-to-model-attributes' | ||
10 | |||
11 | export class APVideoCreator extends APVideoAbstractBuilder { | ||
12 | protected lTags: LoggerTagsFn | ||
13 | |||
14 | constructor (protected readonly videoObject: VideoObject) { | ||
15 | super() | ||
16 | |||
17 | this.lTags = loggerTagsFactory('ap', 'video', 'create', this.videoObject.uuid, this.videoObject.id) | ||
18 | } | ||
19 | |||
20 | async create (waitThumbnail = false) { | ||
21 | logger.debug('Adding remote video %s.', this.videoObject.id, this.lTags()) | ||
22 | |||
23 | const channelActor = await this.getOrCreateVideoChannelFromVideoObject() | ||
24 | const channel = channelActor.VideoChannel | ||
25 | |||
26 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) | ||
27 | const video = VideoModel.build(videoData) as MVideoThumbnail | ||
28 | |||
29 | const promiseThumbnail = this.tryToGenerateThumbnail(video) | ||
30 | |||
31 | let thumbnailModel: MThumbnail | ||
32 | if (waitThumbnail === true) { | ||
33 | thumbnailModel = await promiseThumbnail | ||
34 | } | ||
35 | |||
36 | const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { | ||
37 | try { | ||
38 | const videoCreated = await video.save({ transaction: t }) as MVideoFullLight | ||
39 | videoCreated.VideoChannel = channel | ||
40 | |||
41 | if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) | ||
42 | |||
43 | await this.setPreview(videoCreated, t) | ||
44 | await this.setWebTorrentFiles(videoCreated, t) | ||
45 | await this.setStreamingPlaylists(videoCreated, t) | ||
46 | await this.setTags(videoCreated, t) | ||
47 | await this.setTrackers(videoCreated, t) | ||
48 | await this.insertOrReplaceCaptions(videoCreated, t) | ||
49 | await this.insertOrReplaceLive(videoCreated, t) | ||
50 | |||
51 | // We added a video in this channel, set it as updated | ||
52 | await channel.setAsUpdated(t) | ||
53 | |||
54 | const autoBlacklisted = await autoBlacklistVideoIfNeeded({ | ||
55 | video: videoCreated, | ||
56 | user: undefined, | ||
57 | isRemote: true, | ||
58 | isNew: true, | ||
59 | transaction: t | ||
60 | }) | ||
61 | |||
62 | logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags()) | ||
63 | |||
64 | return { autoBlacklisted, videoCreated } | ||
65 | } catch (err) { | ||
66 | // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released | ||
67 | // Remove thumbnail | ||
68 | if (thumbnailModel) await thumbnailModel.removeThumbnail() | ||
69 | |||
70 | throw err | ||
71 | } | ||
72 | }) | ||
73 | |||
74 | if (waitThumbnail === false) { | ||
75 | // Error is already caught above | ||
76 | // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
77 | promiseThumbnail.then(thumbnailModel => { | ||
78 | if (!thumbnailModel) return | ||
79 | |||
80 | thumbnailModel = videoCreated.id | ||
81 | |||
82 | return thumbnailModel.save() | ||
83 | }) | ||
84 | } | ||
85 | |||
86 | return { autoBlacklisted, videoCreated } | ||
87 | } | ||
88 | } | ||
diff --git a/server/lib/activitypub/videos/shared/index.ts b/server/lib/activitypub/videos/shared/index.ts new file mode 100644 index 000000000..951403493 --- /dev/null +++ b/server/lib/activitypub/videos/shared/index.ts | |||
@@ -0,0 +1,6 @@ | |||
1 | export * from './abstract-builder' | ||
2 | export * from './creator' | ||
3 | export * from './object-to-model-attributes' | ||
4 | export * from './trackers' | ||
5 | export * from './url-to-object' | ||
6 | export * from './video-sync-attributes' | ||
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts new file mode 100644 index 000000000..85548428c --- /dev/null +++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts | |||
@@ -0,0 +1,256 @@ | |||
1 | import { maxBy, minBy } from 'lodash' | ||
2 | import * as magnetUtil from 'magnet-uri' | ||
3 | import { basename } from 'path' | ||
4 | import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos' | ||
5 | import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos' | ||
6 | import { logger } from '@server/helpers/logger' | ||
7 | import { getExtFromMimetype } from '@server/helpers/video' | ||
8 | import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants' | ||
9 | import { generateTorrentFileName } from '@server/lib/video-paths' | ||
10 | import { VideoFileModel } from '@server/models/video/video-file' | ||
11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | ||
12 | import { FilteredModelAttributes } from '@server/types' | ||
13 | import { MChannelId, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models' | ||
14 | import { | ||
15 | ActivityHashTagObject, | ||
16 | ActivityMagnetUrlObject, | ||
17 | ActivityPlaylistSegmentHashesObject, | ||
18 | ActivityPlaylistUrlObject, | ||
19 | ActivityTagObject, | ||
20 | ActivityUrlObject, | ||
21 | ActivityVideoUrlObject, | ||
22 | VideoObject, | ||
23 | VideoPrivacy, | ||
24 | VideoStreamingPlaylistType | ||
25 | } from '@shared/models' | ||
26 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
27 | |||
28 | function getThumbnailFromIcons (videoObject: VideoObject) { | ||
29 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) | ||
30 | // Fallback if there are not valid icons | ||
31 | if (validIcons.length === 0) validIcons = videoObject.icon | ||
32 | |||
33 | return minBy(validIcons, 'width') | ||
34 | } | ||
35 | |||
36 | function getPreviewFromIcons (videoObject: VideoObject) { | ||
37 | const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth) | ||
38 | |||
39 | return maxBy(validIcons, 'width') | ||
40 | } | ||
41 | |||
42 | function getTagsFromObject (videoObject: VideoObject) { | ||
43 | return videoObject.tag | ||
44 | .filter(isAPHashTagObject) | ||
45 | .map(t => t.name) | ||
46 | } | ||
47 | |||
48 | function getFileAttributesFromUrl ( | ||
49 | videoOrPlaylist: MVideo | MStreamingPlaylistVideo, | ||
50 | urls: (ActivityTagObject | ActivityUrlObject)[] | ||
51 | ) { | ||
52 | const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] | ||
53 | |||
54 | if (fileUrls.length === 0) return [] | ||
55 | |||
56 | const attributes: FilteredModelAttributes<VideoFileModel>[] = [] | ||
57 | for (const fileUrl of fileUrls) { | ||
58 | // Fetch associated magnet uri | ||
59 | const magnet = urls.filter(isAPMagnetUrlObject) | ||
60 | .find(u => u.height === fileUrl.height) | ||
61 | |||
62 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) | ||
63 | |||
64 | const parsed = magnetUtil.decode(magnet.href) | ||
65 | if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) { | ||
66 | throw new Error('Cannot parse magnet URI ' + magnet.href) | ||
67 | } | ||
68 | |||
69 | const torrentUrl = Array.isArray(parsed.xs) | ||
70 | ? parsed.xs[0] | ||
71 | : parsed.xs | ||
72 | |||
73 | // Fetch associated metadata url, if any | ||
74 | const metadata = urls.filter(isAPVideoFileUrlMetadataObject) | ||
75 | .find(u => { | ||
76 | return u.height === fileUrl.height && | ||
77 | u.fps === fileUrl.fps && | ||
78 | u.rel.includes(fileUrl.mediaType) | ||
79 | }) | ||
80 | |||
81 | const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType) | ||
82 | const resolution = fileUrl.height | ||
83 | const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id | ||
84 | const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null | ||
85 | |||
86 | const attribute = { | ||
87 | extname, | ||
88 | infoHash: parsed.infoHash, | ||
89 | resolution, | ||
90 | size: fileUrl.size, | ||
91 | fps: fileUrl.fps || -1, | ||
92 | metadataUrl: metadata?.href, | ||
93 | |||
94 | // Use the name of the remote file because we don't proxify video file requests | ||
95 | filename: basename(fileUrl.href), | ||
96 | fileUrl: fileUrl.href, | ||
97 | |||
98 | torrentUrl, | ||
99 | // Use our own torrent name since we proxify torrent requests | ||
100 | torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution), | ||
101 | |||
102 | // This is a video file owned by a video or by a streaming playlist | ||
103 | videoId, | ||
104 | videoStreamingPlaylistId | ||
105 | } | ||
106 | |||
107 | attributes.push(attribute) | ||
108 | } | ||
109 | |||
110 | return attributes | ||
111 | } | ||
112 | |||
113 | function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) { | ||
114 | const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] | ||
115 | if (playlistUrls.length === 0) return [] | ||
116 | |||
117 | const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = [] | ||
118 | for (const playlistUrlObject of playlistUrls) { | ||
119 | const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject) | ||
120 | |||
121 | let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] | ||
122 | |||
123 | // FIXME: backward compatibility introduced in v2.1.0 | ||
124 | if (files.length === 0) files = videoFiles | ||
125 | |||
126 | if (!segmentsSha256UrlObject) { | ||
127 | logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) | ||
128 | continue | ||
129 | } | ||
130 | |||
131 | const attribute = { | ||
132 | type: VideoStreamingPlaylistType.HLS, | ||
133 | playlistUrl: playlistUrlObject.href, | ||
134 | segmentsSha256Url: segmentsSha256UrlObject.href, | ||
135 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files), | ||
136 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | ||
137 | videoId: video.id, | ||
138 | |||
139 | tagAPObject: playlistUrlObject.tag | ||
140 | } | ||
141 | |||
142 | attributes.push(attribute) | ||
143 | } | ||
144 | |||
145 | return attributes | ||
146 | } | ||
147 | |||
148 | function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) { | ||
149 | return { | ||
150 | saveReplay: videoObject.liveSaveReplay, | ||
151 | permanentLive: videoObject.permanentLive, | ||
152 | videoId: video.id | ||
153 | } | ||
154 | } | ||
155 | |||
156 | function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) { | ||
157 | return videoObject.subtitleLanguage.map(c => ({ | ||
158 | videoId: video.id, | ||
159 | filename: VideoCaptionModel.generateCaptionName(c.identifier), | ||
160 | language: c.identifier, | ||
161 | fileUrl: c.url | ||
162 | })) | ||
163 | } | ||
164 | |||
165 | function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) { | ||
166 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | ||
167 | ? VideoPrivacy.PUBLIC | ||
168 | : VideoPrivacy.UNLISTED | ||
169 | |||
170 | const duration = videoObject.duration.replace(/[^\d]+/, '') | ||
171 | const language = videoObject.language?.identifier | ||
172 | |||
173 | const category = videoObject.category | ||
174 | ? parseInt(videoObject.category.identifier, 10) | ||
175 | : undefined | ||
176 | |||
177 | const licence = videoObject.licence | ||
178 | ? parseInt(videoObject.licence.identifier, 10) | ||
179 | : undefined | ||
180 | |||
181 | const description = videoObject.content || null | ||
182 | const support = videoObject.support || null | ||
183 | |||
184 | return { | ||
185 | name: videoObject.name, | ||
186 | uuid: videoObject.uuid, | ||
187 | url: videoObject.id, | ||
188 | category, | ||
189 | licence, | ||
190 | language, | ||
191 | description, | ||
192 | support, | ||
193 | nsfw: videoObject.sensitive, | ||
194 | commentsEnabled: videoObject.commentsEnabled, | ||
195 | downloadEnabled: videoObject.downloadEnabled, | ||
196 | waitTranscoding: videoObject.waitTranscoding, | ||
197 | isLive: videoObject.isLiveBroadcast, | ||
198 | state: videoObject.state, | ||
199 | channelId: videoChannel.id, | ||
200 | duration: parseInt(duration, 10), | ||
201 | createdAt: new Date(videoObject.published), | ||
202 | publishedAt: new Date(videoObject.published), | ||
203 | |||
204 | originallyPublishedAt: videoObject.originallyPublishedAt | ||
205 | ? new Date(videoObject.originallyPublishedAt) | ||
206 | : null, | ||
207 | |||
208 | updatedAt: new Date(videoObject.updated), | ||
209 | views: videoObject.views, | ||
210 | likes: 0, | ||
211 | dislikes: 0, | ||
212 | remote: true, | ||
213 | privacy | ||
214 | } | ||
215 | } | ||
216 | |||
217 | // --------------------------------------------------------------------------- | ||
218 | |||
219 | export { | ||
220 | getThumbnailFromIcons, | ||
221 | getPreviewFromIcons, | ||
222 | |||
223 | getTagsFromObject, | ||
224 | |||
225 | getFileAttributesFromUrl, | ||
226 | getStreamingPlaylistAttributesFromObject, | ||
227 | |||
228 | getLiveAttributesFromObject, | ||
229 | getCaptionAttributesFromObject, | ||
230 | |||
231 | getVideoAttributesFromObject | ||
232 | } | ||
233 | |||
234 | // --------------------------------------------------------------------------- | ||
235 | |||
236 | function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject { | ||
237 | const urlMediaType = url.mediaType | ||
238 | |||
239 | return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/') | ||
240 | } | ||
241 | |||
242 | function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject { | ||
243 | return url && url.mediaType === 'application/x-mpegURL' | ||
244 | } | ||
245 | |||
246 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | ||
247 | return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json' | ||
248 | } | ||
249 | |||
250 | function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject { | ||
251 | return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' | ||
252 | } | ||
253 | |||
254 | function isAPHashTagObject (url: any): url is ActivityHashTagObject { | ||
255 | return url && url.type === 'Hashtag' | ||
256 | } | ||
diff --git a/server/lib/activitypub/videos/shared/trackers.ts b/server/lib/activitypub/videos/shared/trackers.ts new file mode 100644 index 000000000..1c5fc4f84 --- /dev/null +++ b/server/lib/activitypub/videos/shared/trackers.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | ||
3 | import { isAPVideoTrackerUrlObject } from '@server/helpers/custom-validators/activitypub/videos' | ||
4 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
5 | import { REMOTE_SCHEME } from '@server/initializers/constants' | ||
6 | import { TrackerModel } from '@server/models/server/tracker' | ||
7 | import { MVideo, MVideoWithHost } from '@server/types/models' | ||
8 | import { ActivityTrackerUrlObject, VideoObject } from '@shared/models' | ||
9 | |||
10 | function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { | ||
11 | let wsFound = false | ||
12 | |||
13 | const trackers = object.url.filter(u => isAPVideoTrackerUrlObject(u)) | ||
14 | .map((u: ActivityTrackerUrlObject) => { | ||
15 | if (isArray(u.rel) && u.rel.includes('websocket')) wsFound = true | ||
16 | |||
17 | return u.href | ||
18 | }) | ||
19 | |||
20 | if (wsFound) return trackers | ||
21 | |||
22 | return [ | ||
23 | buildRemoteVideoBaseUrl(video, '/tracker/socket', REMOTE_SCHEME.WS), | ||
24 | buildRemoteVideoBaseUrl(video, '/tracker/announce') | ||
25 | ] | ||
26 | } | ||
27 | |||
28 | async function setVideoTrackers (options: { | ||
29 | video: MVideo | ||
30 | trackers: string[] | ||
31 | transaction: Transaction | ||
32 | }) { | ||
33 | const { video, trackers, transaction } = options | ||
34 | |||
35 | const trackerInstances = await TrackerModel.findOrCreateTrackers(trackers, transaction) | ||
36 | |||
37 | await video.$set('Trackers', trackerInstances, { transaction }) | ||
38 | } | ||
39 | |||
40 | export { | ||
41 | getTrackerUrls, | ||
42 | setVideoTrackers | ||
43 | } | ||
diff --git a/server/lib/activitypub/videos/shared/url-to-object.ts b/server/lib/activitypub/videos/shared/url-to-object.ts new file mode 100644 index 000000000..dba3e9480 --- /dev/null +++ b/server/lib/activitypub/videos/shared/url-to-object.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | ||
2 | import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
4 | import { doJSONRequest } from '@server/helpers/requests' | ||
5 | import { VideoObject } from '@shared/models' | ||
6 | |||
7 | const lTags = loggerTagsFactory('ap', 'video') | ||
8 | |||
9 | async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> { | ||
10 | logger.info('Fetching remote video %s.', videoUrl, lTags(videoUrl)) | ||
11 | |||
12 | const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true }) | ||
13 | |||
14 | if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { | ||
15 | logger.debug('Remote video JSON is not valid.', { body, ...lTags(videoUrl) }) | ||
16 | |||
17 | return { statusCode, videoObject: undefined } | ||
18 | } | ||
19 | |||
20 | return { statusCode, videoObject: body } | ||
21 | } | ||
22 | |||
23 | export { | ||
24 | fetchRemoteVideo | ||
25 | } | ||
diff --git a/server/lib/activitypub/videos/shared/video-sync-attributes.ts b/server/lib/activitypub/videos/shared/video-sync-attributes.ts new file mode 100644 index 000000000..c4e101005 --- /dev/null +++ b/server/lib/activitypub/videos/shared/video-sync-attributes.ts | |||
@@ -0,0 +1,94 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
2 | import { JobQueue } from '@server/lib/job-queue' | ||
3 | import { AccountVideoRateModel } from '@server/models/account/account-video-rate' | ||
4 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
5 | import { VideoShareModel } from '@server/models/video/video-share' | ||
6 | import { MVideo } from '@server/types/models' | ||
7 | import { ActivitypubHttpFetcherPayload, VideoObject } from '@shared/models' | ||
8 | import { crawlCollectionPage } from '../../crawl' | ||
9 | import { addVideoShares } from '../../share' | ||
10 | import { addVideoComments } from '../../video-comments' | ||
11 | import { createRates } from '../../video-rates' | ||
12 | |||
13 | const lTags = loggerTagsFactory('ap', 'video') | ||
14 | |||
15 | type SyncParam = { | ||
16 | likes: boolean | ||
17 | dislikes: boolean | ||
18 | shares: boolean | ||
19 | comments: boolean | ||
20 | thumbnail: boolean | ||
21 | refreshVideo?: boolean | ||
22 | } | ||
23 | |||
24 | async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoObject, syncParam: SyncParam) { | ||
25 | logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) | ||
26 | |||
27 | await syncRates('like', video, fetchedVideo, syncParam.likes) | ||
28 | await syncRates('dislike', video, fetchedVideo, syncParam.dislikes) | ||
29 | |||
30 | await syncShares(video, fetchedVideo, syncParam.shares) | ||
31 | |||
32 | await syncComments(video, fetchedVideo, syncParam.comments) | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | export { | ||
38 | SyncParam, | ||
39 | syncVideoExternalAttributes | ||
40 | } | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | function createJob (payload: ActivitypubHttpFetcherPayload) { | ||
45 | return JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) | ||
46 | } | ||
47 | |||
48 | function syncRates (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject, isSync: boolean) { | ||
49 | const uri = type === 'like' | ||
50 | ? fetchedVideo.likes | ||
51 | : fetchedVideo.dislikes | ||
52 | |||
53 | if (!isSync) { | ||
54 | const jobType = type === 'like' | ||
55 | ? 'video-likes' | ||
56 | : 'video-dislikes' | ||
57 | |||
58 | return createJob({ uri, videoId: video.id, type: jobType }) | ||
59 | } | ||
60 | |||
61 | const handler = items => createRates(items, video, type) | ||
62 | const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, type, crawlStartDate) | ||
63 | |||
64 | return crawlCollectionPage<string>(uri, handler, cleaner) | ||
65 | .catch(err => logger.error('Cannot add rate of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) })) | ||
66 | } | ||
67 | |||
68 | function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) { | ||
69 | const uri = fetchedVideo.shares | ||
70 | |||
71 | if (!isSync) { | ||
72 | return createJob({ uri, videoId: video.id, type: 'video-shares' }) | ||
73 | } | ||
74 | |||
75 | const handler = items => addVideoShares(items, video) | ||
76 | const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) | ||
77 | |||
78 | return crawlCollectionPage<string>(uri, handler, cleaner) | ||
79 | .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) })) | ||
80 | } | ||
81 | |||
82 | function syncComments (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) { | ||
83 | const uri = fetchedVideo.comments | ||
84 | |||
85 | if (!isSync) { | ||
86 | return createJob({ uri, videoId: video.id, type: 'video-comments' }) | ||
87 | } | ||
88 | |||
89 | const handler = items => addVideoComments(items) | ||
90 | const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) | ||
91 | |||
92 | return crawlCollectionPage<string>(uri, handler, cleaner) | ||
93 | .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) })) | ||
94 | } | ||
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts new file mode 100644 index 000000000..157569414 --- /dev/null +++ b/server/lib/activitypub/videos/updater.ts | |||
@@ -0,0 +1,166 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils' | ||
3 | import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' | ||
4 | import { Notifier } from '@server/lib/notifier' | ||
5 | import { PeerTubeSocket } from '@server/lib/peertube-socket' | ||
6 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | ||
7 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
8 | import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models' | ||
9 | import { VideoObject, VideoPrivacy } from '@shared/models' | ||
10 | import { APVideoAbstractBuilder, getVideoAttributesFromObject } from './shared' | ||
11 | |||
12 | export class APVideoUpdater extends APVideoAbstractBuilder { | ||
13 | private readonly wasPrivateVideo: boolean | ||
14 | private readonly wasUnlistedVideo: boolean | ||
15 | |||
16 | private readonly videoFieldsSave: any | ||
17 | |||
18 | private readonly oldVideoChannel: MChannelAccountLight | ||
19 | |||
20 | protected lTags: LoggerTagsFn | ||
21 | |||
22 | constructor ( | ||
23 | protected readonly videoObject: VideoObject, | ||
24 | private readonly video: MVideoAccountLightBlacklistAllFiles | ||
25 | ) { | ||
26 | super() | ||
27 | |||
28 | this.wasPrivateVideo = this.video.privacy === VideoPrivacy.PRIVATE | ||
29 | this.wasUnlistedVideo = this.video.privacy === VideoPrivacy.UNLISTED | ||
30 | |||
31 | this.oldVideoChannel = this.video.VideoChannel | ||
32 | |||
33 | this.videoFieldsSave = this.video.toJSON() | ||
34 | |||
35 | this.lTags = loggerTagsFactory('ap', 'video', 'update', video.uuid, video.url) | ||
36 | } | ||
37 | |||
38 | async update (overrideTo?: string[]) { | ||
39 | logger.debug( | ||
40 | 'Updating remote video "%s".', this.videoObject.uuid, | ||
41 | { videoObject: this.videoObject, ...this.lTags() } | ||
42 | ) | ||
43 | |||
44 | try { | ||
45 | const channelActor = await this.getOrCreateVideoChannelFromVideoObject() | ||
46 | |||
47 | const thumbnailModel = await this.tryToGenerateThumbnail(this.video) | ||
48 | |||
49 | this.checkChannelUpdateOrThrow(channelActor) | ||
50 | |||
51 | const videoUpdated = await this.updateVideo(channelActor.VideoChannel, undefined, overrideTo) | ||
52 | |||
53 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel) | ||
54 | |||
55 | await runInReadCommittedTransaction(async t => { | ||
56 | await this.setWebTorrentFiles(videoUpdated, t) | ||
57 | await this.setStreamingPlaylists(videoUpdated, t) | ||
58 | }) | ||
59 | |||
60 | await Promise.all([ | ||
61 | runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)), | ||
62 | runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)), | ||
63 | this.setOrDeleteLive(videoUpdated), | ||
64 | this.setPreview(videoUpdated) | ||
65 | ]) | ||
66 | |||
67 | await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t)) | ||
68 | |||
69 | await autoBlacklistVideoIfNeeded({ | ||
70 | video: videoUpdated, | ||
71 | user: undefined, | ||
72 | isRemote: true, | ||
73 | isNew: false, | ||
74 | transaction: undefined | ||
75 | }) | ||
76 | |||
77 | // Notify our users? | ||
78 | if (this.wasPrivateVideo || this.wasUnlistedVideo) { | ||
79 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) | ||
80 | } | ||
81 | |||
82 | if (videoUpdated.isLive) { | ||
83 | PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) | ||
84 | PeerTubeSocket.Instance.sendVideoViewsUpdate(videoUpdated) | ||
85 | } | ||
86 | |||
87 | logger.info('Remote video with uuid %s updated', this.videoObject.uuid, this.lTags()) | ||
88 | |||
89 | return videoUpdated | ||
90 | } catch (err) { | ||
91 | this.catchUpdateError(err) | ||
92 | } | ||
93 | } | ||
94 | |||
95 | // Check we can update the channel: we trust the remote server | ||
96 | private checkChannelUpdateOrThrow (newChannelActor: MActor) { | ||
97 | if (!this.oldVideoChannel.Actor.serverId || !newChannelActor.serverId) { | ||
98 | throw new Error('Cannot check old channel/new channel validity because `serverId` is null') | ||
99 | } | ||
100 | |||
101 | if (this.oldVideoChannel.Actor.serverId !== newChannelActor.serverId) { | ||
102 | throw new Error(`New channel ${newChannelActor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`) | ||
103 | } | ||
104 | } | ||
105 | |||
106 | private updateVideo (channel: MChannelId, transaction?: Transaction, overrideTo?: string[]) { | ||
107 | const to = overrideTo || this.videoObject.to | ||
108 | const videoData = getVideoAttributesFromObject(channel, this.videoObject, to) | ||
109 | this.video.name = videoData.name | ||
110 | this.video.uuid = videoData.uuid | ||
111 | this.video.url = videoData.url | ||
112 | this.video.category = videoData.category | ||
113 | this.video.licence = videoData.licence | ||
114 | this.video.language = videoData.language | ||
115 | this.video.description = videoData.description | ||
116 | this.video.support = videoData.support | ||
117 | this.video.nsfw = videoData.nsfw | ||
118 | this.video.commentsEnabled = videoData.commentsEnabled | ||
119 | this.video.downloadEnabled = videoData.downloadEnabled | ||
120 | this.video.waitTranscoding = videoData.waitTranscoding | ||
121 | this.video.state = videoData.state | ||
122 | this.video.duration = videoData.duration | ||
123 | this.video.createdAt = videoData.createdAt | ||
124 | this.video.publishedAt = videoData.publishedAt | ||
125 | this.video.originallyPublishedAt = videoData.originallyPublishedAt | ||
126 | this.video.privacy = videoData.privacy | ||
127 | this.video.channelId = videoData.channelId | ||
128 | this.video.views = videoData.views | ||
129 | this.video.isLive = videoData.isLive | ||
130 | |||
131 | // Ensures we update the updatedAt attribute, even if main attributes did not change | ||
132 | this.video.changed('updatedAt', true) | ||
133 | |||
134 | return this.video.save({ transaction }) as Promise<MVideoFullLight> | ||
135 | } | ||
136 | |||
137 | private async setCaptions (videoUpdated: MVideoFullLight, t: Transaction) { | ||
138 | await this.insertOrReplaceCaptions(videoUpdated, t) | ||
139 | } | ||
140 | |||
141 | private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction?: Transaction) { | ||
142 | if (!this.video.isLive) return | ||
143 | |||
144 | if (this.video.isLive) return this.insertOrReplaceLive(videoUpdated, transaction) | ||
145 | |||
146 | // Delete existing live if it exists | ||
147 | await VideoLiveModel.destroy({ | ||
148 | where: { | ||
149 | videoId: this.video.id | ||
150 | }, | ||
151 | transaction | ||
152 | }) | ||
153 | |||
154 | videoUpdated.VideoLive = null | ||
155 | } | ||
156 | |||
157 | private catchUpdateError (err: Error) { | ||
158 | if (this.video !== undefined && this.videoFieldsSave !== undefined) { | ||
159 | resetSequelizeInstance(this.video, this.videoFieldsSave) | ||
160 | } | ||
161 | |||
162 | // This is just a debug because we will retry the insert | ||
163 | logger.debug('Cannot update the remote video.', { err, ...this.lTags() }) | ||
164 | throw err | ||
165 | } | ||
166 | } | ||
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index b9c69eb2d..ae728d080 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
4 | import { ActorModel } from '@server/models/activitypub/actor' | 4 | import { ActorModel } from '@server/models/actor/actor' |
5 | import { MOAuthClient } from '@server/types/models' | 5 | import { MOAuthClient } from '@server/types/models' |
6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
7 | import { MUser } from '@server/types/models/user/user' | 7 | import { MUser } from '@server/types/models/user/user' |
@@ -9,7 +9,7 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model' | |||
9 | import { UserRole } from '@shared/models/users/user-role' | 9 | import { UserRole } from '@shared/models/users/user-role' |
10 | import { logger } from '../../helpers/logger' | 10 | import { logger } from '../../helpers/logger' |
11 | import { CONFIG } from '../../initializers/config' | 11 | import { CONFIG } from '../../initializers/config' |
12 | import { UserModel } from '../../models/account/user' | 12 | import { UserModel } from '../../models/user/user' |
13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
14 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' | 14 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
15 | import { createUserAccountAndChannelAndPlaylist } from '../user' | 15 | import { createUserAccountAndChannelAndPlaylist } from '../user' |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 203bd3893..72194416d 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -2,12 +2,14 @@ import * as express from 'express' | |||
2 | import { readFile } from 'fs-extra' | 2 | import { readFile } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import validator from 'validator' | 4 | import validator from 'validator' |
5 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
6 | import { HTMLServerConfig } from '@shared/models' | ||
5 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' | 7 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' |
6 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 8 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' |
7 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' | 9 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' |
8 | import { isTestInstance, sha256 } from '../helpers/core-utils' | 10 | import { isTestInstance, sha256 } from '../helpers/core-utils' |
9 | import { escapeHTML } from '@shared/core-utils/renderer' | ||
10 | import { logger } from '../helpers/logger' | 11 | import { logger } from '../helpers/logger' |
12 | import { mdToPlainText } from '../helpers/markdown' | ||
11 | import { CONFIG } from '../initializers/config' | 13 | import { CONFIG } from '../initializers/config' |
12 | import { | 14 | import { |
13 | ACCEPT_HEADERS, | 15 | ACCEPT_HEADERS, |
@@ -19,12 +21,13 @@ import { | |||
19 | WEBSERVER | 21 | WEBSERVER |
20 | } from '../initializers/constants' | 22 | } from '../initializers/constants' |
21 | import { AccountModel } from '../models/account/account' | 23 | import { AccountModel } from '../models/account/account' |
24 | import { getActivityStreamDuration } from '../models/video/formatter/video-format-utils' | ||
22 | import { VideoModel } from '../models/video/video' | 25 | import { VideoModel } from '../models/video/video' |
23 | import { VideoChannelModel } from '../models/video/video-channel' | 26 | import { VideoChannelModel } from '../models/video/video-channel' |
24 | import { getActivityStreamDuration } from '../models/video/video-format-utils' | ||
25 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 27 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
26 | import { MAccountActor, MChannelActor } from '../types/models' | 28 | import { MAccountActor, MChannelActor } from '../types/models' |
27 | import { mdToPlainText } from '../helpers/markdown' | 29 | import { ServerConfigManager } from './server-config-manager' |
30 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | ||
28 | 31 | ||
29 | type Tags = { | 32 | type Tags = { |
30 | ogType: string | 33 | ogType: string |
@@ -76,7 +79,9 @@ class ClientHtml { | |||
76 | return customHtml | 79 | return customHtml |
77 | } | 80 | } |
78 | 81 | ||
79 | static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) { | 82 | static async getWatchHTMLPage (videoIdArg: string, req: express.Request, res: express.Response) { |
83 | const videoId = toCompleteUUID(videoIdArg) | ||
84 | |||
80 | // Let Angular application handle errors | 85 | // Let Angular application handle errors |
81 | if (!validator.isInt(videoId) && !validator.isUUID(videoId, 4)) { | 86 | if (!validator.isInt(videoId) && !validator.isUUID(videoId, 4)) { |
82 | res.status(HttpStatusCode.NOT_FOUND_404) | 87 | res.status(HttpStatusCode.NOT_FOUND_404) |
@@ -134,7 +139,9 @@ class ClientHtml { | |||
134 | return customHtml | 139 | return customHtml |
135 | } | 140 | } |
136 | 141 | ||
137 | static async getWatchPlaylistHTMLPage (videoPlaylistId: string, req: express.Request, res: express.Response) { | 142 | static async getWatchPlaylistHTMLPage (videoPlaylistIdArg: string, req: express.Request, res: express.Response) { |
143 | const videoPlaylistId = toCompleteUUID(videoPlaylistIdArg) | ||
144 | |||
138 | // Let Angular application handle errors | 145 | // Let Angular application handle errors |
139 | if (!validator.isInt(videoPlaylistId) && !validator.isUUID(videoPlaylistId, 4)) { | 146 | if (!validator.isInt(videoPlaylistId) && !validator.isUUID(videoPlaylistId, 4)) { |
140 | res.status(HttpStatusCode.NOT_FOUND_404) | 147 | res.status(HttpStatusCode.NOT_FOUND_404) |
@@ -196,11 +203,22 @@ class ClientHtml { | |||
196 | } | 203 | } |
197 | 204 | ||
198 | static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { | 205 | static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { |
199 | return this.getAccountOrChannelHTMLPage(() => AccountModel.loadByNameWithHost(nameWithHost), req, res) | 206 | const accountModelPromise = AccountModel.loadByNameWithHost(nameWithHost) |
207 | return this.getAccountOrChannelHTMLPage(() => accountModelPromise, req, res) | ||
200 | } | 208 | } |
201 | 209 | ||
202 | static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { | 210 | static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { |
203 | return this.getAccountOrChannelHTMLPage(() => VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost), req, res) | 211 | const videoChannelModelPromise = VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost) |
212 | return this.getAccountOrChannelHTMLPage(() => videoChannelModelPromise, req, res) | ||
213 | } | ||
214 | |||
215 | static async getActorHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { | ||
216 | const [ account, channel ] = await Promise.all([ | ||
217 | AccountModel.loadByNameWithHost(nameWithHost), | ||
218 | VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost) | ||
219 | ]) | ||
220 | |||
221 | return this.getAccountOrChannelHTMLPage(() => Promise.resolve(account || channel), req, res) | ||
204 | } | 222 | } |
205 | 223 | ||
206 | static async getEmbedHTML () { | 224 | static async getEmbedHTML () { |
@@ -209,11 +227,14 @@ class ClientHtml { | |||
209 | if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] | 227 | if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] |
210 | 228 | ||
211 | const buffer = await readFile(path) | 229 | const buffer = await readFile(path) |
230 | const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig() | ||
212 | 231 | ||
213 | let html = buffer.toString() | 232 | let html = buffer.toString() |
214 | html = await ClientHtml.addAsyncPluginCSS(html) | 233 | html = await ClientHtml.addAsyncPluginCSS(html) |
215 | html = ClientHtml.addCustomCSS(html) | 234 | html = ClientHtml.addCustomCSS(html) |
216 | html = ClientHtml.addTitleTag(html) | 235 | html = ClientHtml.addTitleTag(html) |
236 | html = ClientHtml.addDescriptionTag(html) | ||
237 | html = ClientHtml.addServerConfig(html, serverConfig) | ||
217 | 238 | ||
218 | ClientHtml.htmlCache[path] = html | 239 | ClientHtml.htmlCache[path] = html |
219 | 240 | ||
@@ -275,6 +296,7 @@ class ClientHtml { | |||
275 | if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] | 296 | if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] |
276 | 297 | ||
277 | const buffer = await readFile(path) | 298 | const buffer = await readFile(path) |
299 | const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig() | ||
278 | 300 | ||
279 | let html = buffer.toString() | 301 | let html = buffer.toString() |
280 | 302 | ||
@@ -283,6 +305,7 @@ class ClientHtml { | |||
283 | html = ClientHtml.addFaviconContentHash(html) | 305 | html = ClientHtml.addFaviconContentHash(html) |
284 | html = ClientHtml.addLogoContentHash(html) | 306 | html = ClientHtml.addLogoContentHash(html) |
285 | html = ClientHtml.addCustomCSS(html) | 307 | html = ClientHtml.addCustomCSS(html) |
308 | html = ClientHtml.addServerConfig(html, serverConfig) | ||
286 | html = await ClientHtml.addAsyncPluginCSS(html) | 309 | html = await ClientHtml.addAsyncPluginCSS(html) |
287 | 310 | ||
288 | ClientHtml.htmlCache[path] = html | 311 | ClientHtml.htmlCache[path] = html |
@@ -355,6 +378,13 @@ class ClientHtml { | |||
355 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag) | 378 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag) |
356 | } | 379 | } |
357 | 380 | ||
381 | private static addServerConfig (htmlStringPage: string, serverConfig: HTMLServerConfig) { | ||
382 | const serverConfigString = JSON.stringify(serverConfig) | ||
383 | const configScriptTag = `<script type="application/javascript">window.PeerTubeServerConfig = '${serverConfigString}'</script>` | ||
384 | |||
385 | return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.SERVER_CONFIG, configScriptTag) | ||
386 | } | ||
387 | |||
358 | private static async addAsyncPluginCSS (htmlStringPage: string) { | 388 | private static async addAsyncPluginCSS (htmlStringPage: string) { |
359 | const globalCSSContent = await readFile(PLUGIN_GLOBAL_CSS_PATH) | 389 | const globalCSSContent = await readFile(PLUGIN_GLOBAL_CSS_PATH) |
360 | if (globalCSSContent.byteLength === 0) return htmlStringPage | 390 | if (globalCSSContent.byteLength === 0) return htmlStringPage |
@@ -524,11 +554,11 @@ async function serveIndexHTML (req: express.Request, res: express.Response) { | |||
524 | return | 554 | return |
525 | } catch (err) { | 555 | } catch (err) { |
526 | logger.error('Cannot generate HTML page.', err) | 556 | logger.error('Cannot generate HTML page.', err) |
527 | return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) | 557 | return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end() |
528 | } | 558 | } |
529 | } | 559 | } |
530 | 560 | ||
531 | return res.sendStatus(HttpStatusCode.NOT_ACCEPTABLE_406) | 561 | return res.status(HttpStatusCode.NOT_ACCEPTABLE_406).end() |
532 | } | 562 | } |
533 | 563 | ||
534 | // --------------------------------------------------------------------------- | 564 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/config.ts b/server/lib/config.ts deleted file mode 100644 index b4c4c9299..000000000 --- a/server/lib/config.ts +++ /dev/null | |||
@@ -1,255 +0,0 @@ | |||
1 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/helpers/signup' | ||
2 | import { getServerCommit } from '@server/helpers/utils' | ||
3 | import { CONFIG, isEmailEnabled } from '@server/initializers/config' | ||
4 | import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants' | ||
5 | import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models' | ||
6 | import { Hooks } from './plugins/hooks' | ||
7 | import { PluginManager } from './plugins/plugin-manager' | ||
8 | import { getThemeOrDefault } from './plugins/theme-utils' | ||
9 | import { getEnabledResolutions } from './video-transcoding' | ||
10 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | ||
11 | |||
12 | let serverCommit: string | ||
13 | |||
14 | async function getServerConfig (ip?: string): Promise<ServerConfig> { | ||
15 | if (serverCommit === undefined) serverCommit = await getServerCommit() | ||
16 | |||
17 | const { allowed } = await Hooks.wrapPromiseFun( | ||
18 | isSignupAllowed, | ||
19 | { | ||
20 | ip | ||
21 | }, | ||
22 | 'filter:api.user.signup.allowed.result' | ||
23 | ) | ||
24 | |||
25 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) | ||
26 | const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) | ||
27 | |||
28 | return { | ||
29 | instance: { | ||
30 | name: CONFIG.INSTANCE.NAME, | ||
31 | shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, | ||
32 | isNSFW: CONFIG.INSTANCE.IS_NSFW, | ||
33 | defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
34 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, | ||
35 | customizations: { | ||
36 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, | ||
37 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS | ||
38 | } | ||
39 | }, | ||
40 | search: { | ||
41 | remoteUri: { | ||
42 | users: CONFIG.SEARCH.REMOTE_URI.USERS, | ||
43 | anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS | ||
44 | }, | ||
45 | searchIndex: { | ||
46 | enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED, | ||
47 | url: CONFIG.SEARCH.SEARCH_INDEX.URL, | ||
48 | disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH, | ||
49 | isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH | ||
50 | } | ||
51 | }, | ||
52 | plugin: { | ||
53 | registered: getRegisteredPlugins(), | ||
54 | registeredExternalAuths: getExternalAuthsPlugins(), | ||
55 | registeredIdAndPassAuths: getIdAndPassAuthPlugins() | ||
56 | }, | ||
57 | theme: { | ||
58 | registered: getRegisteredThemes(), | ||
59 | default: defaultTheme | ||
60 | }, | ||
61 | email: { | ||
62 | enabled: isEmailEnabled() | ||
63 | }, | ||
64 | contactForm: { | ||
65 | enabled: CONFIG.CONTACT_FORM.ENABLED | ||
66 | }, | ||
67 | serverVersion: PEERTUBE_VERSION, | ||
68 | serverCommit, | ||
69 | signup: { | ||
70 | allowed, | ||
71 | allowedForCurrentIP, | ||
72 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | ||
73 | }, | ||
74 | transcoding: { | ||
75 | hls: { | ||
76 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
77 | }, | ||
78 | webtorrent: { | ||
79 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | ||
80 | }, | ||
81 | enabledResolutions: getEnabledResolutions('vod'), | ||
82 | profile: CONFIG.TRANSCODING.PROFILE, | ||
83 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod') | ||
84 | }, | ||
85 | live: { | ||
86 | enabled: CONFIG.LIVE.ENABLED, | ||
87 | |||
88 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, | ||
89 | maxDuration: CONFIG.LIVE.MAX_DURATION, | ||
90 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, | ||
91 | maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, | ||
92 | |||
93 | transcoding: { | ||
94 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, | ||
95 | enabledResolutions: getEnabledResolutions('live'), | ||
96 | profile: CONFIG.LIVE.TRANSCODING.PROFILE, | ||
97 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live') | ||
98 | }, | ||
99 | |||
100 | rtmp: { | ||
101 | port: CONFIG.LIVE.RTMP.PORT | ||
102 | } | ||
103 | }, | ||
104 | import: { | ||
105 | videos: { | ||
106 | http: { | ||
107 | enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED | ||
108 | }, | ||
109 | torrent: { | ||
110 | enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED | ||
111 | } | ||
112 | } | ||
113 | }, | ||
114 | autoBlacklist: { | ||
115 | videos: { | ||
116 | ofUsers: { | ||
117 | enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED | ||
118 | } | ||
119 | } | ||
120 | }, | ||
121 | avatar: { | ||
122 | file: { | ||
123 | size: { | ||
124 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
125 | }, | ||
126 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
127 | } | ||
128 | }, | ||
129 | banner: { | ||
130 | file: { | ||
131 | size: { | ||
132 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
133 | }, | ||
134 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
135 | } | ||
136 | }, | ||
137 | video: { | ||
138 | image: { | ||
139 | extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, | ||
140 | size: { | ||
141 | max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max | ||
142 | } | ||
143 | }, | ||
144 | file: { | ||
145 | extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME | ||
146 | } | ||
147 | }, | ||
148 | videoCaption: { | ||
149 | file: { | ||
150 | size: { | ||
151 | max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max | ||
152 | }, | ||
153 | extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME | ||
154 | } | ||
155 | }, | ||
156 | user: { | ||
157 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
158 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | ||
159 | }, | ||
160 | trending: { | ||
161 | videos: { | ||
162 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS, | ||
163 | algorithms: { | ||
164 | enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED, | ||
165 | default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT | ||
166 | } | ||
167 | } | ||
168 | }, | ||
169 | tracker: { | ||
170 | enabled: CONFIG.TRACKER.ENABLED | ||
171 | }, | ||
172 | |||
173 | followings: { | ||
174 | instance: { | ||
175 | autoFollowIndex: { | ||
176 | indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL | ||
177 | } | ||
178 | } | ||
179 | }, | ||
180 | |||
181 | broadcastMessage: { | ||
182 | enabled: CONFIG.BROADCAST_MESSAGE.ENABLED, | ||
183 | message: CONFIG.BROADCAST_MESSAGE.MESSAGE, | ||
184 | level: CONFIG.BROADCAST_MESSAGE.LEVEL, | ||
185 | dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE | ||
186 | } | ||
187 | } | ||
188 | } | ||
189 | |||
190 | function getRegisteredThemes () { | ||
191 | return PluginManager.Instance.getRegisteredThemes() | ||
192 | .map(t => ({ | ||
193 | name: t.name, | ||
194 | version: t.version, | ||
195 | description: t.description, | ||
196 | css: t.css, | ||
197 | clientScripts: t.clientScripts | ||
198 | })) | ||
199 | } | ||
200 | |||
201 | function getRegisteredPlugins () { | ||
202 | return PluginManager.Instance.getRegisteredPlugins() | ||
203 | .map(p => ({ | ||
204 | name: p.name, | ||
205 | version: p.version, | ||
206 | description: p.description, | ||
207 | clientScripts: p.clientScripts | ||
208 | })) | ||
209 | } | ||
210 | |||
211 | // --------------------------------------------------------------------------- | ||
212 | |||
213 | export { | ||
214 | getServerConfig, | ||
215 | getRegisteredThemes, | ||
216 | getRegisteredPlugins | ||
217 | } | ||
218 | |||
219 | // --------------------------------------------------------------------------- | ||
220 | |||
221 | function getIdAndPassAuthPlugins () { | ||
222 | const result: RegisteredIdAndPassAuthConfig[] = [] | ||
223 | |||
224 | for (const p of PluginManager.Instance.getIdAndPassAuths()) { | ||
225 | for (const auth of p.idAndPassAuths) { | ||
226 | result.push({ | ||
227 | npmName: p.npmName, | ||
228 | name: p.name, | ||
229 | version: p.version, | ||
230 | authName: auth.authName, | ||
231 | weight: auth.getWeight() | ||
232 | }) | ||
233 | } | ||
234 | } | ||
235 | |||
236 | return result | ||
237 | } | ||
238 | |||
239 | function getExternalAuthsPlugins () { | ||
240 | const result: RegisteredExternalAuthConfig[] = [] | ||
241 | |||
242 | for (const p of PluginManager.Instance.getExternalAuths()) { | ||
243 | for (const auth of p.externalAuths) { | ||
244 | result.push({ | ||
245 | npmName: p.npmName, | ||
246 | name: p.name, | ||
247 | version: p.version, | ||
248 | authName: auth.authName, | ||
249 | authDisplayName: auth.authDisplayName() | ||
250 | }) | ||
251 | } | ||
252 | } | ||
253 | |||
254 | return result | ||
255 | } | ||
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts index 82c95be80..f896d7af4 100644 --- a/server/lib/job-queue/handlers/activitypub-follow.ts +++ b/server/lib/job-queue/handlers/activitypub-follow.ts | |||
@@ -1,18 +1,17 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | 2 | import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url' |
3 | import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants' | 3 | import { ActivitypubFollowPayload } from '@shared/models' |
4 | import { sendFollow } from '../../activitypub/send' | ||
5 | import { sanitizeHost } from '../../../helpers/core-utils' | 4 | import { sanitizeHost } from '../../../helpers/core-utils' |
6 | import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' | ||
7 | import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor' | ||
8 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
9 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 6 | import { logger } from '../../../helpers/logger' |
10 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants' |
11 | import { Notifier } from '../../notifier' | ||
12 | import { sequelizeTypescript } from '../../../initializers/database' | 8 | import { sequelizeTypescript } from '../../../initializers/database' |
9 | import { ActorModel } from '../../../models/actor/actor' | ||
10 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | ||
13 | import { MActor, MActorFollowActors, MActorFull } from '../../../types/models' | 11 | import { MActor, MActorFollowActors, MActorFull } from '../../../types/models' |
14 | import { ActivitypubFollowPayload } from '@shared/models' | 12 | import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../activitypub/actors' |
15 | import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url' | 13 | import { sendFollow } from '../../activitypub/send' |
14 | import { Notifier } from '../../notifier' | ||
16 | 15 | ||
17 | async function processActivityPubFollow (job: Bull.Job) { | 16 | async function processActivityPubFollow (job: Bull.Job) { |
18 | const payload = job.data as ActivitypubFollowPayload | 17 | const payload = job.data as ActivitypubFollowPayload |
@@ -26,7 +25,7 @@ async function processActivityPubFollow (job: Bull.Job) { | |||
26 | } else { | 25 | } else { |
27 | const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) | 26 | const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) |
28 | const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost) | 27 | const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost) |
29 | targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') | 28 | targetActor = await getOrCreateAPActor(actorUrl, 'all') |
30 | } | 29 | } |
31 | 30 | ||
32 | if (payload.assertIsChannel && !targetActor.VideoChannel) { | 31 | if (payload.assertIsChannel && !targetActor.VideoChannel) { |
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts index c69ff9e83..d4b328635 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts | |||
@@ -3,7 +3,7 @@ import * as Bull from 'bull' | |||
3 | import { ActivitypubHttpBroadcastPayload } from '@shared/models' | 3 | import { ActivitypubHttpBroadcastPayload } from '@shared/models' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { doRequest } from '../../../helpers/requests' | 5 | import { doRequest } from '../../../helpers/requests' |
6 | import { BROADCAST_CONCURRENCY, REQUEST_TIMEOUT } from '../../../initializers/constants' | 6 | import { BROADCAST_CONCURRENCY } from '../../../initializers/constants' |
7 | import { ActorFollowScoreCache } from '../../files-cache' | 7 | import { ActorFollowScoreCache } from '../../files-cache' |
8 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' | 8 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' |
9 | 9 | ||
@@ -19,7 +19,6 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) { | |||
19 | method: 'POST' as 'POST', | 19 | method: 'POST' as 'POST', |
20 | json: body, | 20 | json: body, |
21 | httpSignature: httpSignatureOptions, | 21 | httpSignature: httpSignatureOptions, |
22 | timeout: REQUEST_TIMEOUT, | ||
23 | headers: buildGlobalHeaders(body) | 22 | headers: buildGlobalHeaders(body) |
24 | } | 23 | } |
25 | 24 | ||
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts index e210ac3ef..ab9675cae 100644 --- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts +++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts | |||
@@ -1,14 +1,13 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models' | 2 | import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { AccountModel } from '../../../models/account/account' | ||
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 4 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
6 | import { VideoModel } from '../../../models/video/video' | 5 | import { VideoModel } from '../../../models/video/video' |
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { VideoShareModel } from '../../../models/video/video-share' | 7 | import { VideoShareModel } from '../../../models/video/video-share' |
9 | import { MAccountDefault, MVideoFullLight } from '../../../types/models' | 8 | import { MVideoFullLight } from '../../../types/models' |
10 | import { crawlCollectionPage } from '../../activitypub/crawl' | 9 | import { crawlCollectionPage } from '../../activitypub/crawl' |
11 | import { createAccountPlaylists } from '../../activitypub/playlist' | 10 | import { createAccountPlaylists } from '../../activitypub/playlists' |
12 | import { processActivities } from '../../activitypub/process' | 11 | import { processActivities } from '../../activitypub/process' |
13 | import { addVideoShares } from '../../activitypub/share' | 12 | import { addVideoShares } from '../../activitypub/share' |
14 | import { addVideoComments } from '../../activitypub/video-comments' | 13 | import { addVideoComments } from '../../activitypub/video-comments' |
@@ -22,16 +21,13 @@ async function processActivityPubHttpFetcher (job: Bull.Job) { | |||
22 | let video: MVideoFullLight | 21 | let video: MVideoFullLight |
23 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) | 22 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) |
24 | 23 | ||
25 | let account: MAccountDefault | ||
26 | if (payload.accountId) account = await AccountModel.load(payload.accountId) | ||
27 | |||
28 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { | 24 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { |
29 | 'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }), | 25 | 'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }), |
30 | 'video-likes': items => createRates(items, video, 'like'), | 26 | 'video-likes': items => createRates(items, video, 'like'), |
31 | 'video-dislikes': items => createRates(items, video, 'dislike'), | 27 | 'video-dislikes': items => createRates(items, video, 'dislike'), |
32 | 'video-shares': items => addVideoShares(items, video), | 28 | 'video-shares': items => addVideoShares(items, video), |
33 | 'video-comments': items => addVideoComments(items), | 29 | 'video-comments': items => addVideoComments(items), |
34 | 'account-playlists': items => createAccountPlaylists(items, account) | 30 | 'account-playlists': items => createAccountPlaylists(items) |
35 | } | 31 | } |
36 | 32 | ||
37 | const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = { | 33 | const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = { |
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index 585dad671..9e561c6b7 100644 --- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts | |||
@@ -2,7 +2,6 @@ import * as Bull from 'bull' | |||
2 | import { ActivitypubHttpUnicastPayload } from '@shared/models' | 2 | import { ActivitypubHttpUnicastPayload } from '@shared/models' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { doRequest } from '../../../helpers/requests' | 4 | import { doRequest } from '../../../helpers/requests' |
5 | import { REQUEST_TIMEOUT } from '../../../initializers/constants' | ||
6 | import { ActorFollowScoreCache } from '../../files-cache' | 5 | import { ActorFollowScoreCache } from '../../files-cache' |
7 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' | 6 | import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' |
8 | 7 | ||
@@ -19,7 +18,6 @@ async function processActivityPubHttpUnicast (job: Bull.Job) { | |||
19 | method: 'POST' as 'POST', | 18 | method: 'POST' as 'POST', |
20 | json: body, | 19 | json: body, |
21 | httpSignature: httpSignatureOptions, | 20 | httpSignature: httpSignatureOptions, |
22 | timeout: REQUEST_TIMEOUT, | ||
23 | headers: buildGlobalHeaders(body) | 21 | headers: buildGlobalHeaders(body) |
24 | } | 22 | } |
25 | 23 | ||
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts index 666e56868..d97e50ebc 100644 --- a/server/lib/job-queue/handlers/activitypub-refresher.ts +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlists' | ||
3 | import { refreshVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
4 | import { loadVideoByUrl } from '@server/lib/model-loaders' | ||
5 | import { RefreshPayload } from '@shared/models' | ||
2 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
3 | import { fetchVideoByUrl } from '../../../helpers/video' | 7 | import { ActorModel } from '../../../models/actor/actor' |
4 | import { refreshActorIfNeeded } from '../../activitypub/actor' | ||
5 | import { refreshVideoIfNeeded } from '../../activitypub/videos' | ||
6 | import { ActorModel } from '../../../models/activitypub/actor' | ||
7 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' |
8 | import { RefreshPayload } from '@shared/models' | 9 | import { refreshActorIfNeeded } from '../../activitypub/actors' |
9 | import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist' | ||
10 | 10 | ||
11 | async function refreshAPObject (job: Bull.Job) { | 11 | async function refreshAPObject (job: Bull.Job) { |
12 | const payload = job.data as RefreshPayload | 12 | const payload = job.data as RefreshPayload |
@@ -30,7 +30,7 @@ async function refreshVideo (videoUrl: string) { | |||
30 | const fetchType = 'all' as 'all' | 30 | const fetchType = 'all' as 'all' |
31 | const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } | 31 | const syncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true } |
32 | 32 | ||
33 | const videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 33 | const videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType) |
34 | if (videoFromDatabase) { | 34 | if (videoFromDatabase) { |
35 | const refreshOptions = { | 35 | const refreshOptions = { |
36 | video: videoFromDatabase, | 36 | video: videoFromDatabase, |
@@ -47,7 +47,7 @@ async function refreshActor (actorUrl: string) { | |||
47 | const actor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorUrl) | 47 | const actor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorUrl) |
48 | 48 | ||
49 | if (actor) { | 49 | if (actor) { |
50 | await refreshActorIfNeeded(actor, fetchType) | 50 | await refreshActorIfNeeded({ actor, fetchedType: fetchType }) |
51 | } | 51 | } |
52 | } | 52 | } |
53 | 53 | ||
diff --git a/server/lib/job-queue/handlers/actor-keys.ts b/server/lib/job-queue/handlers/actor-keys.ts index 125307843..60ac61afd 100644 --- a/server/lib/job-queue/handlers/actor-keys.ts +++ b/server/lib/job-queue/handlers/actor-keys.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { generateAndSaveActorKeys } from '@server/lib/activitypub/actor' | 2 | import { generateAndSaveActorKeys } from '@server/lib/activitypub/actors' |
3 | import { ActorModel } from '@server/models/activitypub/actor' | 3 | import { ActorModel } from '@server/models/actor/actor' |
4 | import { ActorKeysPayload } from '@shared/models' | 4 | import { ActorKeysPayload } from '@shared/models' |
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | 6 | ||
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index e8a91450d..37e7c1fad 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { buildDigest } from '@server/helpers/peertube-crypto' | ||
2 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { ContextType } from '@shared/models/activitypub/context' | ||
1 | import { buildSignedActivity } from '../../../../helpers/activitypub' | 4 | import { buildSignedActivity } from '../../../../helpers/activitypub' |
2 | import { ActorModel } from '../../../../models/activitypub/actor' | ||
3 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' | 5 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' |
6 | import { ActorModel } from '../../../../models/actor/actor' | ||
4 | import { MActor } from '../../../../types/models' | 7 | import { MActor } from '../../../../types/models' |
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { buildDigest } from '@server/helpers/peertube-crypto' | ||
7 | import { ContextType } from '@shared/models/activitypub/context' | ||
8 | 8 | ||
9 | type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number } | 9 | type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number } |
10 | 10 | ||
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 71f2cafcd..187cb652e 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { copy, stat } from 'fs-extra' | 2 | import { copy, stat } from 'fs-extra' |
3 | import { extname } from 'path' | 3 | import { getLowercaseExtension } from '@server/helpers/core-utils' |
4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
5 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 5 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
6 | import { UserModel } from '@server/models/account/user' | 6 | import { UserModel } from '@server/models/user/user' |
7 | import { MVideoFullLight } from '@server/types/models' | 7 | import { MVideoFullLight } from '@server/types/models' |
8 | import { VideoFileImportPayload } from '@shared/models' | 8 | import { VideoFileImportPayload } from '@shared/models' |
9 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | 9 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' |
@@ -55,7 +55,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
55 | const { size } = await stat(inputFilePath) | 55 | const { size } = await stat(inputFilePath) |
56 | const fps = await getVideoFileFPS(inputFilePath) | 56 | const fps = await getVideoFileFPS(inputFilePath) |
57 | 57 | ||
58 | const fileExt = extname(inputFilePath) | 58 | const fileExt = getLowercaseExtension(inputFilePath) |
59 | 59 | ||
60 | const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === videoFileResolution) | 60 | const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === videoFileResolution) |
61 | 61 | ||
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index ed2c5eac0..55498003d 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -1,9 +1,11 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { move, remove, stat } from 'fs-extra' | 2 | import { move, remove, stat } from 'fs-extra' |
3 | import { extname } from 'path' | 3 | import { getLowercaseExtension } from '@server/helpers/core-utils' |
4 | import { retryTransactionWrapper } from '@server/helpers/database-utils' | 4 | import { retryTransactionWrapper } from '@server/helpers/database-utils' |
5 | import { YoutubeDL } from '@server/helpers/youtube-dl' | ||
5 | import { isPostImportVideoAccepted } from '@server/lib/moderation' | 6 | import { isPostImportVideoAccepted } from '@server/lib/moderation' |
6 | import { Hooks } from '@server/lib/plugins/hooks' | 7 | import { Hooks } from '@server/lib/plugins/hooks' |
8 | import { ServerConfigManager } from '@server/lib/server-config-manager' | ||
7 | import { isAbleToUploadVideo } from '@server/lib/user' | 9 | import { isAbleToUploadVideo } from '@server/lib/user' |
8 | import { addOptimizeOrMergeAudioJob } from '@server/lib/video' | 10 | import { addOptimizeOrMergeAudioJob } from '@server/lib/video' |
9 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 11 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
@@ -23,7 +25,6 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro | |||
23 | import { logger } from '../../../helpers/logger' | 25 | import { logger } from '../../../helpers/logger' |
24 | import { getSecureTorrentName } from '../../../helpers/utils' | 26 | import { getSecureTorrentName } from '../../../helpers/utils' |
25 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' | 27 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' |
26 | import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl' | ||
27 | import { CONFIG } from '../../../initializers/config' | 28 | import { CONFIG } from '../../../initializers/config' |
28 | import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' | 29 | import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' |
29 | import { sequelizeTypescript } from '../../../initializers/database' | 30 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -75,8 +76,10 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub | |||
75 | videoImportId: videoImport.id | 76 | videoImportId: videoImport.id |
76 | } | 77 | } |
77 | 78 | ||
79 | const youtubeDL = new YoutubeDL(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) | ||
80 | |||
78 | return processFile( | 81 | return processFile( |
79 | () => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), | 82 | () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT), |
80 | videoImport, | 83 | videoImport, |
81 | options | 84 | options |
82 | ) | 85 | ) |
@@ -116,7 +119,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
116 | const duration = await getDurationFromVideoFile(tempVideoPath) | 119 | const duration = await getDurationFromVideoFile(tempVideoPath) |
117 | 120 | ||
118 | // Prepare video file object for creation in database | 121 | // Prepare video file object for creation in database |
119 | const fileExt = extname(tempVideoPath) | 122 | const fileExt = getLowercaseExtension(tempVideoPath) |
120 | const videoFileData = { | 123 | const videoFileData = { |
121 | extname: fileExt, | 124 | extname: fileExt, |
122 | resolution: videoFileResolution, | 125 | resolution: videoFileResolution, |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index d57202ca5..9eba41bf8 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -3,16 +3,16 @@ import { pathExists, readdir, remove } from 'fs-extra' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { ffprobePromise, getAudioStream, getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 4 | import { ffprobePromise, getAudioStream, getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' |
5 | import { VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { VIDEO_LIVE } from '@server/initializers/constants' |
6 | import { LiveManager } from '@server/lib/live-manager' | 6 | import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' |
7 | import { generateVideoMiniature } from '@server/lib/thumbnail' | 7 | import { generateVideoMiniature } from '@server/lib/thumbnail' |
8 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' | ||
8 | import { publishAndFederateIfNeeded } from '@server/lib/video' | 9 | import { publishAndFederateIfNeeded } from '@server/lib/video' |
9 | import { getHLSDirectory } from '@server/lib/video-paths' | 10 | import { getHLSDirectory } from '@server/lib/video-paths' |
10 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/video-transcoding' | ||
11 | import { VideoModel } from '@server/models/video/video' | 11 | import { VideoModel } from '@server/models/video/video' |
12 | import { VideoFileModel } from '@server/models/video/video-file' | 12 | import { VideoFileModel } from '@server/models/video/video-file' |
13 | import { VideoLiveModel } from '@server/models/video/video-live' | 13 | import { VideoLiveModel } from '@server/models/video/video-live' |
14 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 14 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
15 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' | 15 | import { MVideo, MVideoLive } from '@server/types/models' |
16 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 16 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
17 | import { logger } from '../../../helpers/logger' | 17 | import { logger } from '../../../helpers/logger' |
18 | 18 | ||
@@ -37,7 +37,7 @@ async function processVideoLiveEnding (job: Bull.Job) { | |||
37 | return | 37 | return |
38 | } | 38 | } |
39 | 39 | ||
40 | LiveManager.Instance.cleanupShaSegments(video.uuid) | 40 | LiveSegmentShaStore.Instance.cleanupShaSegments(video.uuid) |
41 | 41 | ||
42 | if (live.saveReplay !== true) { | 42 | if (live.saveReplay !== true) { |
43 | return cleanupLive(video, streamingPlaylist) | 43 | return cleanupLive(video, streamingPlaylist) |
@@ -46,19 +46,10 @@ async function processVideoLiveEnding (job: Bull.Job) { | |||
46 | return saveLive(video, live) | 46 | return saveLive(video, live) |
47 | } | 47 | } |
48 | 48 | ||
49 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { | ||
50 | const hlsDirectory = getHLSDirectory(video) | ||
51 | |||
52 | await remove(hlsDirectory) | ||
53 | |||
54 | await streamingPlaylist.destroy() | ||
55 | } | ||
56 | |||
57 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
58 | 50 | ||
59 | export { | 51 | export { |
60 | processVideoLiveEnding, | 52 | processVideoLiveEnding |
61 | cleanupLive | ||
62 | } | 53 | } |
63 | 54 | ||
64 | // --------------------------------------------------------------------------- | 55 | // --------------------------------------------------------------------------- |
@@ -94,7 +85,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
94 | let durationDone = false | 85 | let durationDone = false |
95 | 86 | ||
96 | for (const playlistFile of playlistFiles) { | 87 | for (const playlistFile of playlistFiles) { |
97 | const concatenatedTsFile = LiveManager.Instance.buildConcatenatedName(playlistFile) | 88 | const concatenatedTsFile = buildConcatenatedName(playlistFile) |
98 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) | 89 | const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) |
99 | 90 | ||
100 | const probe = await ffprobePromise(concatenatedTsFilePath) | 91 | const probe = await ffprobePromise(concatenatedTsFilePath) |
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 010b95b05..f5ba6f435 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -2,7 +2,7 @@ import * as Bull from 'bull' | |||
2 | import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils' | 2 | import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils' |
3 | import { getTranscodingJobPriority, publishAndFederateIfNeeded } from '@server/lib/video' | 3 | import { getTranscodingJobPriority, publishAndFederateIfNeeded } from '@server/lib/video' |
4 | import { getVideoFilePath } from '@server/lib/video-paths' | 4 | import { getVideoFilePath } from '@server/lib/video-paths' |
5 | import { UserModel } from '@server/models/account/user' | 5 | import { UserModel } from '@server/models/user/user' |
6 | import { MUser, MUserId, MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' | 6 | import { MUser, MUserId, MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' |
7 | import { | 7 | import { |
8 | HLSTranscodingPayload, | 8 | HLSTranscodingPayload, |
@@ -15,7 +15,6 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' | |||
15 | import { computeResolutionsToTranscode } from '../../../helpers/ffprobe-utils' | 15 | import { computeResolutionsToTranscode } from '../../../helpers/ffprobe-utils' |
16 | import { logger } from '../../../helpers/logger' | 16 | import { logger } from '../../../helpers/logger' |
17 | import { CONFIG } from '../../../initializers/config' | 17 | import { CONFIG } from '../../../initializers/config' |
18 | import { sequelizeTypescript } from '../../../initializers/database' | ||
19 | import { VideoModel } from '../../../models/video/video' | 18 | import { VideoModel } from '../../../models/video/video' |
20 | import { federateVideoIfNeeded } from '../../activitypub/videos' | 19 | import { federateVideoIfNeeded } from '../../activitypub/videos' |
21 | import { Notifier } from '../../notifier' | 20 | import { Notifier } from '../../notifier' |
@@ -24,7 +23,7 @@ import { | |||
24 | mergeAudioVideofile, | 23 | mergeAudioVideofile, |
25 | optimizeOriginalVideofile, | 24 | optimizeOriginalVideofile, |
26 | transcodeNewWebTorrentResolution | 25 | transcodeNewWebTorrentResolution |
27 | } from '../../video-transcoding' | 26 | } from '../../transcoding/video-transcoding' |
28 | import { JobQueue } from '../job-queue' | 27 | import { JobQueue } from '../job-queue' |
29 | 28 | ||
30 | type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any> | 29 | type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any> |
@@ -151,35 +150,31 @@ async function onVideoFileOptimizer ( | |||
151 | // Outside the transaction (IO on disk) | 150 | // Outside the transaction (IO on disk) |
152 | const { videoFileResolution, isPortraitMode } = await videoArg.getMaxQualityResolution() | 151 | const { videoFileResolution, isPortraitMode } = await videoArg.getMaxQualityResolution() |
153 | 152 | ||
154 | const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { | 153 | // Maybe the video changed in database, refresh it |
155 | // Maybe the video changed in database, refresh it | 154 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid) |
156 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) | 155 | // Video does not exist anymore |
157 | // Video does not exist anymore | 156 | if (!videoDatabase) return undefined |
158 | if (!videoDatabase) return undefined | ||
159 | |||
160 | let videoPublished = false | ||
161 | |||
162 | // Generate HLS version of the original file | ||
163 | const originalFileHLSPayload = Object.assign({}, payload, { | ||
164 | isPortraitMode, | ||
165 | resolution: videoDatabase.getMaxQualityFile().resolution, | ||
166 | // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues | ||
167 | copyCodecs: transcodeType !== 'quick-transcode', | ||
168 | isMaxQuality: true | ||
169 | }) | ||
170 | const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) | ||
171 | |||
172 | const hasNewResolutions = await createLowerResolutionsJobs(videoDatabase, user, videoFileResolution, isPortraitMode, 'webtorrent') | ||
173 | |||
174 | if (!hasHls && !hasNewResolutions) { | ||
175 | // No transcoding to do, it's now published | ||
176 | videoPublished = await videoDatabase.publishIfNeededAndSave(t) | ||
177 | } | ||
178 | 157 | ||
179 | await federateVideoIfNeeded(videoDatabase, payload.isNewVideo, t) | 158 | let videoPublished = false |
180 | 159 | ||
181 | return { videoDatabase, videoPublished } | 160 | // Generate HLS version of the original file |
161 | const originalFileHLSPayload = Object.assign({}, payload, { | ||
162 | isPortraitMode, | ||
163 | resolution: videoDatabase.getMaxQualityFile().resolution, | ||
164 | // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues | ||
165 | copyCodecs: transcodeType !== 'quick-transcode', | ||
166 | isMaxQuality: true | ||
182 | }) | 167 | }) |
168 | const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) | ||
169 | |||
170 | const hasNewResolutions = await createLowerResolutionsJobs(videoDatabase, user, videoFileResolution, isPortraitMode, 'webtorrent') | ||
171 | |||
172 | if (!hasHls && !hasNewResolutions) { | ||
173 | // No transcoding to do, it's now published | ||
174 | videoPublished = await videoDatabase.publishIfNeededAndSave(undefined) | ||
175 | } | ||
176 | |||
177 | await federateVideoIfNeeded(videoDatabase, payload.isNewVideo) | ||
183 | 178 | ||
184 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase) | 179 | if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase) |
185 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) | 180 | if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase) |
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts index 897235ec0..86d0a271f 100644 --- a/server/lib/job-queue/handlers/video-views.ts +++ b/server/lib/job-queue/handlers/video-views.ts | |||
@@ -36,8 +36,8 @@ async function processVideosViews () { | |||
36 | } | 36 | } |
37 | 37 | ||
38 | await VideoViewModel.create({ | 38 | await VideoViewModel.create({ |
39 | startDate, | 39 | startDate: new Date(startDate), |
40 | endDate, | 40 | endDate: new Date(endDate), |
41 | views, | 41 | views, |
42 | videoId | 42 | videoId |
43 | }) | 43 | }) |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts deleted file mode 100644 index 66b5d119b..000000000 --- a/server/lib/live-manager.ts +++ /dev/null | |||
@@ -1,621 +0,0 @@ | |||
1 | |||
2 | import * as Bluebird from 'bluebird' | ||
3 | import * as chokidar from 'chokidar' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { appendFile, ensureDir, readFile, stat } from 'fs-extra' | ||
6 | import { createServer, Server } from 'net' | ||
7 | import { basename, join } from 'path' | ||
8 | import { isTestInstance } from '@server/helpers/core-utils' | ||
9 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' | ||
10 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | ||
11 | import { logger } from '@server/helpers/logger' | ||
12 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | ||
13 | import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' | ||
14 | import { UserModel } from '@server/models/account/user' | ||
15 | import { VideoModel } from '@server/models/video/video' | ||
16 | import { VideoFileModel } from '@server/models/video/video-file' | ||
17 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
18 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | ||
19 | import { MStreamingPlaylist, MStreamingPlaylistVideo, MUserId, MVideoLive, MVideoLiveVideo } from '@server/types/models' | ||
20 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' | ||
21 | import { federateVideoIfNeeded } from './activitypub/videos' | ||
22 | import { buildSha256Segment } from './hls' | ||
23 | import { JobQueue } from './job-queue' | ||
24 | import { cleanupLive } from './job-queue/handlers/video-live-ending' | ||
25 | import { PeerTubeSocket } from './peertube-socket' | ||
26 | import { isAbleToUploadVideo } from './user' | ||
27 | import { getHLSDirectory } from './video-paths' | ||
28 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | ||
29 | |||
30 | import memoizee = require('memoizee') | ||
31 | const NodeRtmpSession = require('node-media-server/node_rtmp_session') | ||
32 | const context = require('node-media-server/node_core_ctx') | ||
33 | const nodeMediaServerLogger = require('node-media-server/node_core_logger') | ||
34 | |||
35 | // Disable node media server logs | ||
36 | nodeMediaServerLogger.setLogType(0) | ||
37 | |||
38 | const config = { | ||
39 | rtmp: { | ||
40 | port: CONFIG.LIVE.RTMP.PORT, | ||
41 | chunk_size: VIDEO_LIVE.RTMP.CHUNK_SIZE, | ||
42 | gop_cache: VIDEO_LIVE.RTMP.GOP_CACHE, | ||
43 | ping: VIDEO_LIVE.RTMP.PING, | ||
44 | ping_timeout: VIDEO_LIVE.RTMP.PING_TIMEOUT | ||
45 | }, | ||
46 | transcoding: { | ||
47 | ffmpeg: 'ffmpeg' | ||
48 | } | ||
49 | } | ||
50 | |||
51 | class LiveManager { | ||
52 | |||
53 | private static instance: LiveManager | ||
54 | |||
55 | private readonly transSessions = new Map<string, FfmpegCommand>() | ||
56 | private readonly videoSessions = new Map<number, string>() | ||
57 | // Values are Date().getTime() | ||
58 | private readonly watchersPerVideo = new Map<number, number[]>() | ||
59 | private readonly segmentsSha256 = new Map<string, Map<string, string>>() | ||
60 | private readonly livesPerUser = new Map<number, { liveId: number, videoId: number, size: number }[]>() | ||
61 | |||
62 | private readonly isAbleToUploadVideoWithCache = memoizee((userId: number) => { | ||
63 | return isAbleToUploadVideo(userId, 1000) | ||
64 | }, { maxAge: MEMOIZE_TTL.LIVE_ABLE_TO_UPLOAD }) | ||
65 | |||
66 | private readonly hasClientSocketsInBadHealthWithCache = memoizee((sessionId: string) => { | ||
67 | return this.hasClientSocketsInBadHealth(sessionId) | ||
68 | }, { maxAge: MEMOIZE_TTL.LIVE_CHECK_SOCKET_HEALTH }) | ||
69 | |||
70 | private rtmpServer: Server | ||
71 | |||
72 | private constructor () { | ||
73 | } | ||
74 | |||
75 | init () { | ||
76 | const events = this.getContext().nodeEvent | ||
77 | events.on('postPublish', (sessionId: string, streamPath: string) => { | ||
78 | logger.debug('RTMP received stream', { id: sessionId, streamPath }) | ||
79 | |||
80 | const splittedPath = streamPath.split('/') | ||
81 | if (splittedPath.length !== 3 || splittedPath[1] !== VIDEO_LIVE.RTMP.BASE_PATH) { | ||
82 | logger.warn('Live path is incorrect.', { streamPath }) | ||
83 | return this.abortSession(sessionId) | ||
84 | } | ||
85 | |||
86 | this.handleSession(sessionId, streamPath, splittedPath[2]) | ||
87 | .catch(err => logger.error('Cannot handle sessions.', { err })) | ||
88 | }) | ||
89 | |||
90 | events.on('donePublish', sessionId => { | ||
91 | logger.info('Live session ended.', { sessionId }) | ||
92 | }) | ||
93 | |||
94 | registerConfigChangedHandler(() => { | ||
95 | if (!this.rtmpServer && CONFIG.LIVE.ENABLED === true) { | ||
96 | this.run() | ||
97 | return | ||
98 | } | ||
99 | |||
100 | if (this.rtmpServer && CONFIG.LIVE.ENABLED === false) { | ||
101 | this.stop() | ||
102 | } | ||
103 | }) | ||
104 | |||
105 | // Cleanup broken lives, that were terminated by a server restart for example | ||
106 | this.handleBrokenLives() | ||
107 | .catch(err => logger.error('Cannot handle broken lives.', { err })) | ||
108 | |||
109 | setInterval(() => this.updateLiveViews(), VIEW_LIFETIME.LIVE) | ||
110 | } | ||
111 | |||
112 | run () { | ||
113 | logger.info('Running RTMP server on port %d', config.rtmp.port) | ||
114 | |||
115 | this.rtmpServer = createServer(socket => { | ||
116 | const session = new NodeRtmpSession(config, socket) | ||
117 | |||
118 | session.run() | ||
119 | }) | ||
120 | |||
121 | this.rtmpServer.on('error', err => { | ||
122 | logger.error('Cannot run RTMP server.', { err }) | ||
123 | }) | ||
124 | |||
125 | this.rtmpServer.listen(CONFIG.LIVE.RTMP.PORT) | ||
126 | } | ||
127 | |||
128 | stop () { | ||
129 | logger.info('Stopping RTMP server.') | ||
130 | |||
131 | this.rtmpServer.close() | ||
132 | this.rtmpServer = undefined | ||
133 | |||
134 | // Sessions is an object | ||
135 | this.getContext().sessions.forEach((session: any) => { | ||
136 | if (session instanceof NodeRtmpSession) { | ||
137 | session.stop() | ||
138 | } | ||
139 | }) | ||
140 | } | ||
141 | |||
142 | isRunning () { | ||
143 | return !!this.rtmpServer | ||
144 | } | ||
145 | |||
146 | getSegmentsSha256 (videoUUID: string) { | ||
147 | return this.segmentsSha256.get(videoUUID) | ||
148 | } | ||
149 | |||
150 | stopSessionOf (videoId: number) { | ||
151 | const sessionId = this.videoSessions.get(videoId) | ||
152 | if (!sessionId) return | ||
153 | |||
154 | this.videoSessions.delete(videoId) | ||
155 | this.abortSession(sessionId) | ||
156 | } | ||
157 | |||
158 | getLiveQuotaUsedByUser (userId: number) { | ||
159 | const currentLives = this.livesPerUser.get(userId) | ||
160 | if (!currentLives) return 0 | ||
161 | |||
162 | return currentLives.reduce((sum, obj) => sum + obj.size, 0) | ||
163 | } | ||
164 | |||
165 | addViewTo (videoId: number) { | ||
166 | if (this.videoSessions.has(videoId) === false) return | ||
167 | |||
168 | let watchers = this.watchersPerVideo.get(videoId) | ||
169 | |||
170 | if (!watchers) { | ||
171 | watchers = [] | ||
172 | this.watchersPerVideo.set(videoId, watchers) | ||
173 | } | ||
174 | |||
175 | watchers.push(new Date().getTime()) | ||
176 | } | ||
177 | |||
178 | cleanupShaSegments (videoUUID: string) { | ||
179 | this.segmentsSha256.delete(videoUUID) | ||
180 | } | ||
181 | |||
182 | addSegmentToReplay (hlsVideoPath: string, segmentPath: string) { | ||
183 | const segmentName = basename(segmentPath) | ||
184 | const dest = join(hlsVideoPath, VIDEO_LIVE.REPLAY_DIRECTORY, this.buildConcatenatedName(segmentName)) | ||
185 | |||
186 | return readFile(segmentPath) | ||
187 | .then(data => appendFile(dest, data)) | ||
188 | .catch(err => logger.error('Cannot copy segment %s to repay directory.', segmentPath, { err })) | ||
189 | } | ||
190 | |||
191 | buildConcatenatedName (segmentOrPlaylistPath: string) { | ||
192 | const num = basename(segmentOrPlaylistPath).match(/^(\d+)(-|\.)/) | ||
193 | |||
194 | return 'concat-' + num[1] + '.ts' | ||
195 | } | ||
196 | |||
197 | private processSegments (hlsVideoPath: string, videoUUID: string, videoLive: MVideoLive, segmentPaths: string[]) { | ||
198 | Bluebird.mapSeries(segmentPaths, async previousSegment => { | ||
199 | // Add sha hash of previous segments, because ffmpeg should have finished generating them | ||
200 | await this.addSegmentSha(videoUUID, previousSegment) | ||
201 | |||
202 | if (videoLive.saveReplay) { | ||
203 | await this.addSegmentToReplay(hlsVideoPath, previousSegment) | ||
204 | } | ||
205 | }).catch(err => logger.error('Cannot process segments in %s', hlsVideoPath, { err })) | ||
206 | } | ||
207 | |||
208 | private getContext () { | ||
209 | return context | ||
210 | } | ||
211 | |||
212 | private abortSession (id: string) { | ||
213 | const session = this.getContext().sessions.get(id) | ||
214 | if (session) { | ||
215 | session.stop() | ||
216 | this.getContext().sessions.delete(id) | ||
217 | } | ||
218 | |||
219 | const transSession = this.transSessions.get(id) | ||
220 | if (transSession) { | ||
221 | transSession.kill('SIGINT') | ||
222 | this.transSessions.delete(id) | ||
223 | } | ||
224 | } | ||
225 | |||
226 | private async handleSession (sessionId: string, streamPath: string, streamKey: string) { | ||
227 | const videoLive = await VideoLiveModel.loadByStreamKey(streamKey) | ||
228 | if (!videoLive) { | ||
229 | logger.warn('Unknown live video with stream key %s.', streamKey) | ||
230 | return this.abortSession(sessionId) | ||
231 | } | ||
232 | |||
233 | const video = videoLive.Video | ||
234 | if (video.isBlacklisted()) { | ||
235 | logger.warn('Video is blacklisted. Refusing stream %s.', streamKey) | ||
236 | return this.abortSession(sessionId) | ||
237 | } | ||
238 | |||
239 | // Cleanup old potential live files (could happen with a permanent live) | ||
240 | this.cleanupShaSegments(video.uuid) | ||
241 | |||
242 | const oldStreamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) | ||
243 | if (oldStreamingPlaylist) { | ||
244 | await cleanupLive(video, oldStreamingPlaylist) | ||
245 | } | ||
246 | |||
247 | this.videoSessions.set(video.id, sessionId) | ||
248 | |||
249 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | ||
250 | |||
251 | const session = this.getContext().sessions.get(sessionId) | ||
252 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | ||
253 | |||
254 | const [ resolutionResult, fps ] = await Promise.all([ | ||
255 | getVideoFileResolution(rtmpUrl), | ||
256 | getVideoFileFPS(rtmpUrl) | ||
257 | ]) | ||
258 | |||
259 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED | ||
260 | ? computeResolutionsToTranscode(resolutionResult.videoFileResolution, 'live') | ||
261 | : [] | ||
262 | |||
263 | const allResolutions = resolutionsEnabled.concat([ session.videoHeight ]) | ||
264 | |||
265 | logger.info('Will mux/transcode live video of original resolution %d.', session.videoHeight, { allResolutions }) | ||
266 | |||
267 | const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({ | ||
268 | videoId: video.id, | ||
269 | playlistUrl, | ||
270 | segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), | ||
271 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, allResolutions), | ||
272 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | ||
273 | |||
274 | type: VideoStreamingPlaylistType.HLS | ||
275 | }, { returning: true }) as [ MStreamingPlaylist, boolean ] | ||
276 | |||
277 | return this.runMuxing({ | ||
278 | sessionId, | ||
279 | videoLive, | ||
280 | playlist: Object.assign(videoStreamingPlaylist, { Video: video }), | ||
281 | rtmpUrl, | ||
282 | fps, | ||
283 | allResolutions | ||
284 | }) | ||
285 | } | ||
286 | |||
287 | private async runMuxing (options: { | ||
288 | sessionId: string | ||
289 | videoLive: MVideoLiveVideo | ||
290 | playlist: MStreamingPlaylistVideo | ||
291 | rtmpUrl: string | ||
292 | fps: number | ||
293 | allResolutions: number[] | ||
294 | }) { | ||
295 | const { sessionId, videoLive, playlist, allResolutions, fps, rtmpUrl } = options | ||
296 | const startStreamDateTime = new Date().getTime() | ||
297 | |||
298 | const user = await UserModel.loadByLiveId(videoLive.id) | ||
299 | if (!this.livesPerUser.has(user.id)) { | ||
300 | this.livesPerUser.set(user.id, []) | ||
301 | } | ||
302 | |||
303 | const currentUserLive = { liveId: videoLive.id, videoId: videoLive.videoId, size: 0 } | ||
304 | const livesOfUser = this.livesPerUser.get(user.id) | ||
305 | livesOfUser.push(currentUserLive) | ||
306 | |||
307 | for (let i = 0; i < allResolutions.length; i++) { | ||
308 | const resolution = allResolutions[i] | ||
309 | |||
310 | const file = new VideoFileModel({ | ||
311 | resolution, | ||
312 | size: -1, | ||
313 | extname: '.ts', | ||
314 | infoHash: null, | ||
315 | fps, | ||
316 | videoStreamingPlaylistId: playlist.id | ||
317 | }) | ||
318 | |||
319 | VideoFileModel.customUpsert(file, 'streaming-playlist', null) | ||
320 | .catch(err => logger.error('Cannot create file for live streaming.', { err })) | ||
321 | } | ||
322 | |||
323 | const outPath = getHLSDirectory(videoLive.Video) | ||
324 | await ensureDir(outPath) | ||
325 | |||
326 | const replayDirectory = join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY) | ||
327 | |||
328 | if (videoLive.saveReplay === true) { | ||
329 | await ensureDir(replayDirectory) | ||
330 | } | ||
331 | |||
332 | const videoUUID = videoLive.Video.uuid | ||
333 | |||
334 | const ffmpegExec = CONFIG.LIVE.TRANSCODING.ENABLED | ||
335 | ? await getLiveTranscodingCommand({ | ||
336 | rtmpUrl, | ||
337 | outPath, | ||
338 | resolutions: allResolutions, | ||
339 | fps, | ||
340 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | ||
341 | profile: CONFIG.LIVE.TRANSCODING.PROFILE | ||
342 | }) | ||
343 | : getLiveMuxingCommand(rtmpUrl, outPath) | ||
344 | |||
345 | logger.info('Running live muxing/transcoding for %s.', videoUUID) | ||
346 | this.transSessions.set(sessionId, ffmpegExec) | ||
347 | |||
348 | const tsWatcher = chokidar.watch(outPath + '/*.ts') | ||
349 | |||
350 | const segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} | ||
351 | const playlistIdMatcher = /^([\d+])-/ | ||
352 | |||
353 | const addHandler = segmentPath => { | ||
354 | logger.debug('Live add handler of %s.', segmentPath) | ||
355 | |||
356 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] | ||
357 | |||
358 | const segmentsToProcess = segmentsToProcessPerPlaylist[playlistId] || [] | ||
359 | this.processSegments(outPath, videoUUID, videoLive, segmentsToProcess) | ||
360 | |||
361 | segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] | ||
362 | |||
363 | if (this.hasClientSocketsInBadHealthWithCache(sessionId)) { | ||
364 | logger.error( | ||
365 | 'Too much data in client socket stream (ffmpeg is too slow to transcode the video).' + | ||
366 | ' Stopping session of video %s.', videoUUID) | ||
367 | |||
368 | this.stopSessionOf(videoLive.videoId) | ||
369 | return | ||
370 | } | ||
371 | |||
372 | // Duration constraint check | ||
373 | if (this.isDurationConstraintValid(startStreamDateTime) !== true) { | ||
374 | logger.info('Stopping session of %s: max duration exceeded.', videoUUID) | ||
375 | |||
376 | this.stopSessionOf(videoLive.videoId) | ||
377 | return | ||
378 | } | ||
379 | |||
380 | // Check user quota if the user enabled replay saving | ||
381 | if (videoLive.saveReplay === true) { | ||
382 | stat(segmentPath) | ||
383 | .then(segmentStat => { | ||
384 | currentUserLive.size += segmentStat.size | ||
385 | }) | ||
386 | .then(() => this.isQuotaConstraintValid(user, videoLive)) | ||
387 | .then(quotaValid => { | ||
388 | if (quotaValid !== true) { | ||
389 | logger.info('Stopping session of %s: user quota exceeded.', videoUUID) | ||
390 | |||
391 | this.stopSessionOf(videoLive.videoId) | ||
392 | } | ||
393 | }) | ||
394 | .catch(err => logger.error('Cannot stat %s or check quota of %d.', segmentPath, user.id, { err })) | ||
395 | } | ||
396 | } | ||
397 | |||
398 | const deleteHandler = segmentPath => this.removeSegmentSha(videoUUID, segmentPath) | ||
399 | |||
400 | tsWatcher.on('add', p => addHandler(p)) | ||
401 | tsWatcher.on('unlink', p => deleteHandler(p)) | ||
402 | |||
403 | const masterWatcher = chokidar.watch(outPath + '/master.m3u8') | ||
404 | masterWatcher.on('add', async () => { | ||
405 | try { | ||
406 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoLive.videoId) | ||
407 | |||
408 | video.state = VideoState.PUBLISHED | ||
409 | await video.save() | ||
410 | videoLive.Video = video | ||
411 | |||
412 | setTimeout(() => { | ||
413 | federateVideoIfNeeded(video, false) | ||
414 | .catch(err => logger.error('Cannot federate live video %s.', video.url, { err })) | ||
415 | |||
416 | PeerTubeSocket.Instance.sendVideoLiveNewState(video) | ||
417 | }, VIDEO_LIVE.SEGMENT_TIME_SECONDS * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION) | ||
418 | |||
419 | } catch (err) { | ||
420 | logger.error('Cannot save/federate live video %d.', videoLive.videoId, { err }) | ||
421 | } finally { | ||
422 | masterWatcher.close() | ||
423 | .catch(err => logger.error('Cannot close master watcher of %s.', outPath, { err })) | ||
424 | } | ||
425 | }) | ||
426 | |||
427 | const onFFmpegEnded = () => { | ||
428 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', rtmpUrl) | ||
429 | |||
430 | this.transSessions.delete(sessionId) | ||
431 | |||
432 | this.watchersPerVideo.delete(videoLive.videoId) | ||
433 | this.videoSessions.delete(videoLive.videoId) | ||
434 | |||
435 | const newLivesPerUser = this.livesPerUser.get(user.id) | ||
436 | .filter(o => o.liveId !== videoLive.id) | ||
437 | this.livesPerUser.set(user.id, newLivesPerUser) | ||
438 | |||
439 | setTimeout(() => { | ||
440 | // Wait latest segments generation, and close watchers | ||
441 | |||
442 | Promise.all([ tsWatcher.close(), masterWatcher.close() ]) | ||
443 | .then(() => { | ||
444 | // Process remaining segments hash | ||
445 | for (const key of Object.keys(segmentsToProcessPerPlaylist)) { | ||
446 | this.processSegments(outPath, videoUUID, videoLive, segmentsToProcessPerPlaylist[key]) | ||
447 | } | ||
448 | }) | ||
449 | .catch(err => logger.error('Cannot close watchers of %s or process remaining hash segments.', outPath, { err })) | ||
450 | |||
451 | this.onEndTransmuxing(videoLive.Video.id) | ||
452 | .catch(err => logger.error('Error in closed transmuxing.', { err })) | ||
453 | }, 1000) | ||
454 | } | ||
455 | |||
456 | ffmpegExec.on('error', (err, stdout, stderr) => { | ||
457 | onFFmpegEnded() | ||
458 | |||
459 | // Don't care that we killed the ffmpeg process | ||
460 | if (err?.message?.includes('Exiting normally')) return | ||
461 | |||
462 | logger.error('Live transcoding error.', { err, stdout, stderr }) | ||
463 | |||
464 | this.abortSession(sessionId) | ||
465 | }) | ||
466 | |||
467 | ffmpegExec.on('end', () => onFFmpegEnded()) | ||
468 | |||
469 | ffmpegExec.run() | ||
470 | } | ||
471 | |||
472 | private async onEndTransmuxing (videoId: number, cleanupNow = false) { | ||
473 | try { | ||
474 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
475 | if (!fullVideo) return | ||
476 | |||
477 | const live = await VideoLiveModel.loadByVideoId(videoId) | ||
478 | |||
479 | if (!live.permanentLive) { | ||
480 | JobQueue.Instance.createJob({ | ||
481 | type: 'video-live-ending', | ||
482 | payload: { | ||
483 | videoId: fullVideo.id | ||
484 | } | ||
485 | }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY }) | ||
486 | |||
487 | fullVideo.state = VideoState.LIVE_ENDED | ||
488 | } else { | ||
489 | fullVideo.state = VideoState.WAITING_FOR_LIVE | ||
490 | } | ||
491 | |||
492 | await fullVideo.save() | ||
493 | |||
494 | PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo) | ||
495 | |||
496 | await federateVideoIfNeeded(fullVideo, false) | ||
497 | } catch (err) { | ||
498 | logger.error('Cannot save/federate new video state of live streaming of video id %d.', videoId, { err }) | ||
499 | } | ||
500 | } | ||
501 | |||
502 | private async addSegmentSha (videoUUID: string, segmentPath: string) { | ||
503 | const segmentName = basename(segmentPath) | ||
504 | logger.debug('Adding live sha segment %s.', segmentPath) | ||
505 | |||
506 | const shaResult = await buildSha256Segment(segmentPath) | ||
507 | |||
508 | if (!this.segmentsSha256.has(videoUUID)) { | ||
509 | this.segmentsSha256.set(videoUUID, new Map()) | ||
510 | } | ||
511 | |||
512 | const filesMap = this.segmentsSha256.get(videoUUID) | ||
513 | filesMap.set(segmentName, shaResult) | ||
514 | } | ||
515 | |||
516 | private removeSegmentSha (videoUUID: string, segmentPath: string) { | ||
517 | const segmentName = basename(segmentPath) | ||
518 | |||
519 | logger.debug('Removing live sha segment %s.', segmentPath) | ||
520 | |||
521 | const filesMap = this.segmentsSha256.get(videoUUID) | ||
522 | if (!filesMap) { | ||
523 | logger.warn('Unknown files map to remove sha for %s.', videoUUID) | ||
524 | return | ||
525 | } | ||
526 | |||
527 | if (!filesMap.has(segmentName)) { | ||
528 | logger.warn('Unknown segment in files map for video %s and segment %s.', videoUUID, segmentPath) | ||
529 | return | ||
530 | } | ||
531 | |||
532 | filesMap.delete(segmentName) | ||
533 | } | ||
534 | |||
535 | private isDurationConstraintValid (streamingStartTime: number) { | ||
536 | const maxDuration = CONFIG.LIVE.MAX_DURATION | ||
537 | // No limit | ||
538 | if (maxDuration < 0) return true | ||
539 | |||
540 | const now = new Date().getTime() | ||
541 | const max = streamingStartTime + maxDuration | ||
542 | |||
543 | return now <= max | ||
544 | } | ||
545 | |||
546 | private hasClientSocketsInBadHealth (sessionId: string) { | ||
547 | const rtmpSession = this.getContext().sessions.get(sessionId) | ||
548 | |||
549 | if (!rtmpSession) { | ||
550 | logger.warn('Cannot get session %s to check players socket health.', sessionId) | ||
551 | return | ||
552 | } | ||
553 | |||
554 | for (const playerSessionId of rtmpSession.players) { | ||
555 | const playerSession = this.getContext().sessions.get(playerSessionId) | ||
556 | |||
557 | if (!playerSession) { | ||
558 | logger.error('Cannot get player session %s to check socket health.', playerSession) | ||
559 | continue | ||
560 | } | ||
561 | |||
562 | if (playerSession.socket.writableLength > VIDEO_LIVE.MAX_SOCKET_WAITING_DATA) { | ||
563 | return true | ||
564 | } | ||
565 | } | ||
566 | |||
567 | return false | ||
568 | } | ||
569 | |||
570 | private async isQuotaConstraintValid (user: MUserId, live: MVideoLive) { | ||
571 | if (live.saveReplay !== true) return true | ||
572 | |||
573 | return this.isAbleToUploadVideoWithCache(user.id) | ||
574 | } | ||
575 | |||
576 | private async updateLiveViews () { | ||
577 | if (!this.isRunning()) return | ||
578 | |||
579 | if (!isTestInstance()) logger.info('Updating live video views.') | ||
580 | |||
581 | for (const videoId of this.watchersPerVideo.keys()) { | ||
582 | const notBefore = new Date().getTime() - VIEW_LIFETIME.LIVE | ||
583 | |||
584 | const watchers = this.watchersPerVideo.get(videoId) | ||
585 | |||
586 | const numWatchers = watchers.length | ||
587 | |||
588 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
589 | video.views = numWatchers | ||
590 | await video.save() | ||
591 | |||
592 | await federateVideoIfNeeded(video, false) | ||
593 | |||
594 | PeerTubeSocket.Instance.sendVideoViewsUpdate(video) | ||
595 | |||
596 | // Only keep not expired watchers | ||
597 | const newWatchers = watchers.filter(w => w > notBefore) | ||
598 | this.watchersPerVideo.set(videoId, newWatchers) | ||
599 | |||
600 | logger.debug('New live video views for %s is %d.', video.url, numWatchers) | ||
601 | } | ||
602 | } | ||
603 | |||
604 | private async handleBrokenLives () { | ||
605 | const videoIds = await VideoModel.listPublishedLiveIds() | ||
606 | |||
607 | for (const id of videoIds) { | ||
608 | await this.onEndTransmuxing(id, true) | ||
609 | } | ||
610 | } | ||
611 | |||
612 | static get Instance () { | ||
613 | return this.instance || (this.instance = new this()) | ||
614 | } | ||
615 | } | ||
616 | |||
617 | // --------------------------------------------------------------------------- | ||
618 | |||
619 | export { | ||
620 | LiveManager | ||
621 | } | ||
diff --git a/server/lib/live/index.ts b/server/lib/live/index.ts new file mode 100644 index 000000000..8b46800da --- /dev/null +++ b/server/lib/live/index.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | export * from './live-manager' | ||
2 | export * from './live-quota-store' | ||
3 | export * from './live-segment-sha-store' | ||
4 | export * from './live-utils' | ||
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts new file mode 100644 index 000000000..014cd3fcf --- /dev/null +++ b/server/lib/live/live-manager.ts | |||
@@ -0,0 +1,419 @@ | |||
1 | |||
2 | import { createServer, Server } from 'net' | ||
3 | import { isTestInstance } from '@server/helpers/core-utils' | ||
4 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | ||
5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
6 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | ||
7 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' | ||
8 | import { UserModel } from '@server/models/user/user' | ||
9 | import { VideoModel } from '@server/models/video/video' | ||
10 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | ||
12 | import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models' | ||
13 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' | ||
14 | import { federateVideoIfNeeded } from '../activitypub/videos' | ||
15 | import { JobQueue } from '../job-queue' | ||
16 | import { PeerTubeSocket } from '../peertube-socket' | ||
17 | import { LiveQuotaStore } from './live-quota-store' | ||
18 | import { LiveSegmentShaStore } from './live-segment-sha-store' | ||
19 | import { cleanupLive } from './live-utils' | ||
20 | import { MuxingSession } from './shared' | ||
21 | |||
22 | const NodeRtmpSession = require('node-media-server/node_rtmp_session') | ||
23 | const context = require('node-media-server/node_core_ctx') | ||
24 | const nodeMediaServerLogger = require('node-media-server/node_core_logger') | ||
25 | |||
26 | // Disable node media server logs | ||
27 | nodeMediaServerLogger.setLogType(0) | ||
28 | |||
29 | const config = { | ||
30 | rtmp: { | ||
31 | port: CONFIG.LIVE.RTMP.PORT, | ||
32 | chunk_size: VIDEO_LIVE.RTMP.CHUNK_SIZE, | ||
33 | gop_cache: VIDEO_LIVE.RTMP.GOP_CACHE, | ||
34 | ping: VIDEO_LIVE.RTMP.PING, | ||
35 | ping_timeout: VIDEO_LIVE.RTMP.PING_TIMEOUT | ||
36 | }, | ||
37 | transcoding: { | ||
38 | ffmpeg: 'ffmpeg' | ||
39 | } | ||
40 | } | ||
41 | |||
42 | const lTags = loggerTagsFactory('live') | ||
43 | |||
44 | class LiveManager { | ||
45 | |||
46 | private static instance: LiveManager | ||
47 | |||
48 | private readonly muxingSessions = new Map<string, MuxingSession>() | ||
49 | private readonly videoSessions = new Map<number, string>() | ||
50 | // Values are Date().getTime() | ||
51 | private readonly watchersPerVideo = new Map<number, number[]>() | ||
52 | |||
53 | private rtmpServer: Server | ||
54 | |||
55 | private constructor () { | ||
56 | } | ||
57 | |||
58 | init () { | ||
59 | const events = this.getContext().nodeEvent | ||
60 | events.on('postPublish', (sessionId: string, streamPath: string) => { | ||
61 | logger.debug('RTMP received stream', { id: sessionId, streamPath, ...lTags(sessionId) }) | ||
62 | |||
63 | const splittedPath = streamPath.split('/') | ||
64 | if (splittedPath.length !== 3 || splittedPath[1] !== VIDEO_LIVE.RTMP.BASE_PATH) { | ||
65 | logger.warn('Live path is incorrect.', { streamPath, ...lTags(sessionId) }) | ||
66 | return this.abortSession(sessionId) | ||
67 | } | ||
68 | |||
69 | this.handleSession(sessionId, streamPath, splittedPath[2]) | ||
70 | .catch(err => logger.error('Cannot handle sessions.', { err, ...lTags(sessionId) })) | ||
71 | }) | ||
72 | |||
73 | events.on('donePublish', sessionId => { | ||
74 | logger.info('Live session ended.', { sessionId, ...lTags(sessionId) }) | ||
75 | }) | ||
76 | |||
77 | registerConfigChangedHandler(() => { | ||
78 | if (!this.rtmpServer && CONFIG.LIVE.ENABLED === true) { | ||
79 | this.run() | ||
80 | return | ||
81 | } | ||
82 | |||
83 | if (this.rtmpServer && CONFIG.LIVE.ENABLED === false) { | ||
84 | this.stop() | ||
85 | } | ||
86 | }) | ||
87 | |||
88 | // Cleanup broken lives, that were terminated by a server restart for example | ||
89 | this.handleBrokenLives() | ||
90 | .catch(err => logger.error('Cannot handle broken lives.', { err, ...lTags() })) | ||
91 | |||
92 | setInterval(() => this.updateLiveViews(), VIEW_LIFETIME.LIVE) | ||
93 | } | ||
94 | |||
95 | run () { | ||
96 | logger.info('Running RTMP server on port %d', config.rtmp.port, lTags()) | ||
97 | |||
98 | this.rtmpServer = createServer(socket => { | ||
99 | const session = new NodeRtmpSession(config, socket) | ||
100 | |||
101 | session.run() | ||
102 | }) | ||
103 | |||
104 | this.rtmpServer.on('error', err => { | ||
105 | logger.error('Cannot run RTMP server.', { err, ...lTags() }) | ||
106 | }) | ||
107 | |||
108 | this.rtmpServer.listen(CONFIG.LIVE.RTMP.PORT) | ||
109 | } | ||
110 | |||
111 | stop () { | ||
112 | logger.info('Stopping RTMP server.', lTags()) | ||
113 | |||
114 | this.rtmpServer.close() | ||
115 | this.rtmpServer = undefined | ||
116 | |||
117 | // Sessions is an object | ||
118 | this.getContext().sessions.forEach((session: any) => { | ||
119 | if (session instanceof NodeRtmpSession) { | ||
120 | session.stop() | ||
121 | } | ||
122 | }) | ||
123 | } | ||
124 | |||
125 | isRunning () { | ||
126 | return !!this.rtmpServer | ||
127 | } | ||
128 | |||
129 | stopSessionOf (videoId: number) { | ||
130 | const sessionId = this.videoSessions.get(videoId) | ||
131 | if (!sessionId) return | ||
132 | |||
133 | this.videoSessions.delete(videoId) | ||
134 | this.abortSession(sessionId) | ||
135 | } | ||
136 | |||
137 | addViewTo (videoId: number) { | ||
138 | if (this.videoSessions.has(videoId) === false) return | ||
139 | |||
140 | let watchers = this.watchersPerVideo.get(videoId) | ||
141 | |||
142 | if (!watchers) { | ||
143 | watchers = [] | ||
144 | this.watchersPerVideo.set(videoId, watchers) | ||
145 | } | ||
146 | |||
147 | watchers.push(new Date().getTime()) | ||
148 | } | ||
149 | |||
150 | private getContext () { | ||
151 | return context | ||
152 | } | ||
153 | |||
154 | private abortSession (sessionId: string) { | ||
155 | const session = this.getContext().sessions.get(sessionId) | ||
156 | if (session) { | ||
157 | session.stop() | ||
158 | this.getContext().sessions.delete(sessionId) | ||
159 | } | ||
160 | |||
161 | const muxingSession = this.muxingSessions.get(sessionId) | ||
162 | if (muxingSession) { | ||
163 | // Muxing session will fire and event so we correctly cleanup the session | ||
164 | muxingSession.abort() | ||
165 | |||
166 | this.muxingSessions.delete(sessionId) | ||
167 | } | ||
168 | } | ||
169 | |||
170 | private async handleSession (sessionId: string, streamPath: string, streamKey: string) { | ||
171 | const videoLive = await VideoLiveModel.loadByStreamKey(streamKey) | ||
172 | if (!videoLive) { | ||
173 | logger.warn('Unknown live video with stream key %s.', streamKey, lTags(sessionId)) | ||
174 | return this.abortSession(sessionId) | ||
175 | } | ||
176 | |||
177 | const video = videoLive.Video | ||
178 | if (video.isBlacklisted()) { | ||
179 | logger.warn('Video is blacklisted. Refusing stream %s.', streamKey, lTags(sessionId, video.uuid)) | ||
180 | return this.abortSession(sessionId) | ||
181 | } | ||
182 | |||
183 | // Cleanup old potential live files (could happen with a permanent live) | ||
184 | LiveSegmentShaStore.Instance.cleanupShaSegments(video.uuid) | ||
185 | |||
186 | const oldStreamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) | ||
187 | if (oldStreamingPlaylist) { | ||
188 | await cleanupLive(video, oldStreamingPlaylist) | ||
189 | } | ||
190 | |||
191 | this.videoSessions.set(video.id, sessionId) | ||
192 | |||
193 | const rtmpUrl = 'rtmp://127.0.0.1:' + config.rtmp.port + streamPath | ||
194 | |||
195 | const [ { videoFileResolution }, fps ] = await Promise.all([ | ||
196 | getVideoFileResolution(rtmpUrl), | ||
197 | getVideoFileFPS(rtmpUrl) | ||
198 | ]) | ||
199 | |||
200 | const allResolutions = this.buildAllResolutionsToTranscode(videoFileResolution) | ||
201 | |||
202 | logger.info( | ||
203 | 'Will mux/transcode live video of original resolution %d.', videoFileResolution, | ||
204 | { allResolutions, ...lTags(sessionId, video.uuid) } | ||
205 | ) | ||
206 | |||
207 | const streamingPlaylist = await this.createLivePlaylist(video, allResolutions) | ||
208 | |||
209 | return this.runMuxingSession({ | ||
210 | sessionId, | ||
211 | videoLive, | ||
212 | streamingPlaylist, | ||
213 | rtmpUrl, | ||
214 | fps, | ||
215 | allResolutions | ||
216 | }) | ||
217 | } | ||
218 | |||
219 | private async runMuxingSession (options: { | ||
220 | sessionId: string | ||
221 | videoLive: MVideoLiveVideo | ||
222 | streamingPlaylist: MStreamingPlaylistVideo | ||
223 | rtmpUrl: string | ||
224 | fps: number | ||
225 | allResolutions: number[] | ||
226 | }) { | ||
227 | const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, rtmpUrl } = options | ||
228 | const videoUUID = videoLive.Video.uuid | ||
229 | const localLTags = lTags(sessionId, videoUUID) | ||
230 | |||
231 | const user = await UserModel.loadByLiveId(videoLive.id) | ||
232 | LiveQuotaStore.Instance.addNewLive(user.id, videoLive.id) | ||
233 | |||
234 | const muxingSession = new MuxingSession({ | ||
235 | context: this.getContext(), | ||
236 | user, | ||
237 | sessionId, | ||
238 | videoLive, | ||
239 | streamingPlaylist, | ||
240 | rtmpUrl, | ||
241 | fps, | ||
242 | allResolutions | ||
243 | }) | ||
244 | |||
245 | muxingSession.on('master-playlist-created', () => this.publishAndFederateLive(videoLive, localLTags)) | ||
246 | |||
247 | muxingSession.on('bad-socket-health', ({ videoId }) => { | ||
248 | logger.error( | ||
249 | 'Too much data in client socket stream (ffmpeg is too slow to transcode the video).' + | ||
250 | ' Stopping session of video %s.', videoUUID, | ||
251 | localLTags | ||
252 | ) | ||
253 | |||
254 | this.stopSessionOf(videoId) | ||
255 | }) | ||
256 | |||
257 | muxingSession.on('duration-exceeded', ({ videoId }) => { | ||
258 | logger.info('Stopping session of %s: max duration exceeded.', videoUUID, localLTags) | ||
259 | |||
260 | this.stopSessionOf(videoId) | ||
261 | }) | ||
262 | |||
263 | muxingSession.on('quota-exceeded', ({ videoId }) => { | ||
264 | logger.info('Stopping session of %s: user quota exceeded.', videoUUID, localLTags) | ||
265 | |||
266 | this.stopSessionOf(videoId) | ||
267 | }) | ||
268 | |||
269 | muxingSession.on('ffmpeg-error', ({ sessionId }) => this.abortSession(sessionId)) | ||
270 | muxingSession.on('ffmpeg-end', ({ videoId }) => { | ||
271 | this.onMuxingFFmpegEnd(videoId) | ||
272 | }) | ||
273 | |||
274 | muxingSession.on('after-cleanup', ({ videoId }) => { | ||
275 | this.muxingSessions.delete(sessionId) | ||
276 | |||
277 | muxingSession.destroy() | ||
278 | |||
279 | return this.onAfterMuxingCleanup(videoId) | ||
280 | .catch(err => logger.error('Error in end transmuxing.', { err, ...localLTags })) | ||
281 | }) | ||
282 | |||
283 | this.muxingSessions.set(sessionId, muxingSession) | ||
284 | |||
285 | muxingSession.runMuxing() | ||
286 | .catch(err => { | ||
287 | logger.error('Cannot run muxing.', { err, ...localLTags }) | ||
288 | this.abortSession(sessionId) | ||
289 | }) | ||
290 | } | ||
291 | |||
292 | private async publishAndFederateLive (live: MVideoLiveVideo, localLTags: { tags: string[] }) { | ||
293 | const videoId = live.videoId | ||
294 | |||
295 | try { | ||
296 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
297 | |||
298 | logger.info('Will publish and federate live %s.', video.url, localLTags) | ||
299 | |||
300 | video.state = VideoState.PUBLISHED | ||
301 | await video.save() | ||
302 | |||
303 | live.Video = video | ||
304 | |||
305 | setTimeout(() => { | ||
306 | federateVideoIfNeeded(video, false) | ||
307 | .catch(err => logger.error('Cannot federate live video %s.', video.url, { err, ...localLTags })) | ||
308 | |||
309 | PeerTubeSocket.Instance.sendVideoLiveNewState(video) | ||
310 | }, VIDEO_LIVE.SEGMENT_TIME_SECONDS * 1000 * VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION) | ||
311 | } catch (err) { | ||
312 | logger.error('Cannot save/federate live video %d.', videoId, { err, ...localLTags }) | ||
313 | } | ||
314 | } | ||
315 | |||
316 | private onMuxingFFmpegEnd (videoId: number) { | ||
317 | this.watchersPerVideo.delete(videoId) | ||
318 | this.videoSessions.delete(videoId) | ||
319 | } | ||
320 | |||
321 | private async onAfterMuxingCleanup (videoUUID: string, cleanupNow = false) { | ||
322 | try { | ||
323 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoUUID) | ||
324 | if (!fullVideo) return | ||
325 | |||
326 | const live = await VideoLiveModel.loadByVideoId(fullVideo.id) | ||
327 | |||
328 | if (!live.permanentLive) { | ||
329 | JobQueue.Instance.createJob({ | ||
330 | type: 'video-live-ending', | ||
331 | payload: { | ||
332 | videoId: fullVideo.id | ||
333 | } | ||
334 | }, { delay: cleanupNow ? 0 : VIDEO_LIVE.CLEANUP_DELAY }) | ||
335 | |||
336 | fullVideo.state = VideoState.LIVE_ENDED | ||
337 | } else { | ||
338 | fullVideo.state = VideoState.WAITING_FOR_LIVE | ||
339 | } | ||
340 | |||
341 | await fullVideo.save() | ||
342 | |||
343 | PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo) | ||
344 | |||
345 | await federateVideoIfNeeded(fullVideo, false) | ||
346 | } catch (err) { | ||
347 | logger.error('Cannot save/federate new video state of live streaming of video %d.', videoUUID, { err, ...lTags(videoUUID) }) | ||
348 | } | ||
349 | } | ||
350 | |||
351 | private async updateLiveViews () { | ||
352 | if (!this.isRunning()) return | ||
353 | |||
354 | if (!isTestInstance()) logger.info('Updating live video views.', lTags()) | ||
355 | |||
356 | for (const videoId of this.watchersPerVideo.keys()) { | ||
357 | const notBefore = new Date().getTime() - VIEW_LIFETIME.LIVE | ||
358 | |||
359 | const watchers = this.watchersPerVideo.get(videoId) | ||
360 | |||
361 | const numWatchers = watchers.length | ||
362 | |||
363 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
364 | video.views = numWatchers | ||
365 | await video.save() | ||
366 | |||
367 | await federateVideoIfNeeded(video, false) | ||
368 | |||
369 | PeerTubeSocket.Instance.sendVideoViewsUpdate(video) | ||
370 | |||
371 | // Only keep not expired watchers | ||
372 | const newWatchers = watchers.filter(w => w > notBefore) | ||
373 | this.watchersPerVideo.set(videoId, newWatchers) | ||
374 | |||
375 | logger.debug('New live video views for %s is %d.', video.url, numWatchers, lTags()) | ||
376 | } | ||
377 | } | ||
378 | |||
379 | private async handleBrokenLives () { | ||
380 | const videoUUIDs = await VideoModel.listPublishedLiveUUIDs() | ||
381 | |||
382 | for (const uuid of videoUUIDs) { | ||
383 | await this.onAfterMuxingCleanup(uuid, true) | ||
384 | } | ||
385 | } | ||
386 | |||
387 | private buildAllResolutionsToTranscode (originResolution: number) { | ||
388 | const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED | ||
389 | ? computeResolutionsToTranscode(originResolution, 'live') | ||
390 | : [] | ||
391 | |||
392 | return resolutionsEnabled.concat([ originResolution ]) | ||
393 | } | ||
394 | |||
395 | private async createLivePlaylist (video: MVideo, allResolutions: number[]) { | ||
396 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | ||
397 | const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({ | ||
398 | videoId: video.id, | ||
399 | playlistUrl, | ||
400 | segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), | ||
401 | p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, allResolutions), | ||
402 | p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, | ||
403 | |||
404 | type: VideoStreamingPlaylistType.HLS | ||
405 | }, { returning: true }) as [ MStreamingPlaylist, boolean ] | ||
406 | |||
407 | return Object.assign(videoStreamingPlaylist, { Video: video }) | ||
408 | } | ||
409 | |||
410 | static get Instance () { | ||
411 | return this.instance || (this.instance = new this()) | ||
412 | } | ||
413 | } | ||
414 | |||
415 | // --------------------------------------------------------------------------- | ||
416 | |||
417 | export { | ||
418 | LiveManager | ||
419 | } | ||
diff --git a/server/lib/live/live-quota-store.ts b/server/lib/live/live-quota-store.ts new file mode 100644 index 000000000..8ceccde98 --- /dev/null +++ b/server/lib/live/live-quota-store.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | class LiveQuotaStore { | ||
2 | |||
3 | private static instance: LiveQuotaStore | ||
4 | |||
5 | private readonly livesPerUser = new Map<number, { liveId: number, size: number }[]>() | ||
6 | |||
7 | private constructor () { | ||
8 | } | ||
9 | |||
10 | addNewLive (userId: number, liveId: number) { | ||
11 | if (!this.livesPerUser.has(userId)) { | ||
12 | this.livesPerUser.set(userId, []) | ||
13 | } | ||
14 | |||
15 | const currentUserLive = { liveId, size: 0 } | ||
16 | const livesOfUser = this.livesPerUser.get(userId) | ||
17 | livesOfUser.push(currentUserLive) | ||
18 | } | ||
19 | |||
20 | removeLive (userId: number, liveId: number) { | ||
21 | const newLivesPerUser = this.livesPerUser.get(userId) | ||
22 | .filter(o => o.liveId !== liveId) | ||
23 | |||
24 | this.livesPerUser.set(userId, newLivesPerUser) | ||
25 | } | ||
26 | |||
27 | addQuotaTo (userId: number, liveId: number, size: number) { | ||
28 | const lives = this.livesPerUser.get(userId) | ||
29 | const live = lives.find(l => l.liveId === liveId) | ||
30 | |||
31 | live.size += size | ||
32 | } | ||
33 | |||
34 | getLiveQuotaOf (userId: number) { | ||
35 | const currentLives = this.livesPerUser.get(userId) | ||
36 | if (!currentLives) return 0 | ||
37 | |||
38 | return currentLives.reduce((sum, obj) => sum + obj.size, 0) | ||
39 | } | ||
40 | |||
41 | static get Instance () { | ||
42 | return this.instance || (this.instance = new this()) | ||
43 | } | ||
44 | } | ||
45 | |||
46 | export { | ||
47 | LiveQuotaStore | ||
48 | } | ||
diff --git a/server/lib/live/live-segment-sha-store.ts b/server/lib/live/live-segment-sha-store.ts new file mode 100644 index 000000000..4af6f3ebf --- /dev/null +++ b/server/lib/live/live-segment-sha-store.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { basename } from 'path' | ||
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
3 | import { buildSha256Segment } from '../hls' | ||
4 | |||
5 | const lTags = loggerTagsFactory('live') | ||
6 | |||
7 | class LiveSegmentShaStore { | ||
8 | |||
9 | private static instance: LiveSegmentShaStore | ||
10 | |||
11 | private readonly segmentsSha256 = new Map<string, Map<string, string>>() | ||
12 | |||
13 | private constructor () { | ||
14 | } | ||
15 | |||
16 | getSegmentsSha256 (videoUUID: string) { | ||
17 | return this.segmentsSha256.get(videoUUID) | ||
18 | } | ||
19 | |||
20 | async addSegmentSha (videoUUID: string, segmentPath: string) { | ||
21 | const segmentName = basename(segmentPath) | ||
22 | logger.debug('Adding live sha segment %s.', segmentPath, lTags(videoUUID)) | ||
23 | |||
24 | const shaResult = await buildSha256Segment(segmentPath) | ||
25 | |||
26 | if (!this.segmentsSha256.has(videoUUID)) { | ||
27 | this.segmentsSha256.set(videoUUID, new Map()) | ||
28 | } | ||
29 | |||
30 | const filesMap = this.segmentsSha256.get(videoUUID) | ||
31 | filesMap.set(segmentName, shaResult) | ||
32 | } | ||
33 | |||
34 | removeSegmentSha (videoUUID: string, segmentPath: string) { | ||
35 | const segmentName = basename(segmentPath) | ||
36 | |||
37 | logger.debug('Removing live sha segment %s.', segmentPath, lTags(videoUUID)) | ||
38 | |||
39 | const filesMap = this.segmentsSha256.get(videoUUID) | ||
40 | if (!filesMap) { | ||
41 | logger.warn('Unknown files map to remove sha for %s.', videoUUID, lTags(videoUUID)) | ||
42 | return | ||
43 | } | ||
44 | |||
45 | if (!filesMap.has(segmentName)) { | ||
46 | logger.warn('Unknown segment in files map for video %s and segment %s.', videoUUID, segmentPath, lTags(videoUUID)) | ||
47 | return | ||
48 | } | ||
49 | |||
50 | filesMap.delete(segmentName) | ||
51 | } | ||
52 | |||
53 | cleanupShaSegments (videoUUID: string) { | ||
54 | this.segmentsSha256.delete(videoUUID) | ||
55 | } | ||
56 | |||
57 | static get Instance () { | ||
58 | return this.instance || (this.instance = new this()) | ||
59 | } | ||
60 | } | ||
61 | |||
62 | export { | ||
63 | LiveSegmentShaStore | ||
64 | } | ||
diff --git a/server/lib/live/live-utils.ts b/server/lib/live/live-utils.ts new file mode 100644 index 000000000..e4526c7a5 --- /dev/null +++ b/server/lib/live/live-utils.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { basename } from 'path' | ||
3 | import { MStreamingPlaylist, MVideo } from '@server/types/models' | ||
4 | import { getHLSDirectory } from '../video-paths' | ||
5 | |||
6 | function buildConcatenatedName (segmentOrPlaylistPath: string) { | ||
7 | const num = basename(segmentOrPlaylistPath).match(/^(\d+)(-|\.)/) | ||
8 | |||
9 | return 'concat-' + num[1] + '.ts' | ||
10 | } | ||
11 | |||
12 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { | ||
13 | const hlsDirectory = getHLSDirectory(video) | ||
14 | |||
15 | await remove(hlsDirectory) | ||
16 | |||
17 | await streamingPlaylist.destroy() | ||
18 | } | ||
19 | |||
20 | export { | ||
21 | cleanupLive, | ||
22 | buildConcatenatedName | ||
23 | } | ||
diff --git a/server/lib/live/shared/index.ts b/server/lib/live/shared/index.ts new file mode 100644 index 000000000..c4d1b59ec --- /dev/null +++ b/server/lib/live/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './muxing-session' | |||
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts new file mode 100644 index 000000000..26467f060 --- /dev/null +++ b/server/lib/live/shared/muxing-session.ts | |||
@@ -0,0 +1,346 @@ | |||
1 | |||
2 | import * as Bluebird from 'bluebird' | ||
3 | import * as chokidar from 'chokidar' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { appendFile, ensureDir, readFile, stat } from 'fs-extra' | ||
6 | import { basename, join } from 'path' | ||
7 | import { EventEmitter } from 'stream' | ||
8 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' | ||
9 | import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' | ||
10 | import { CONFIG } from '@server/initializers/config' | ||
11 | import { MEMOIZE_TTL, VIDEO_LIVE } from '@server/initializers/constants' | ||
12 | import { VideoFileModel } from '@server/models/video/video-file' | ||
13 | import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models' | ||
14 | import { VideoTranscodingProfilesManager } from '../../transcoding/video-transcoding-profiles' | ||
15 | import { isAbleToUploadVideo } from '../../user' | ||
16 | import { getHLSDirectory } from '../../video-paths' | ||
17 | import { LiveQuotaStore } from '../live-quota-store' | ||
18 | import { LiveSegmentShaStore } from '../live-segment-sha-store' | ||
19 | import { buildConcatenatedName } from '../live-utils' | ||
20 | |||
21 | import memoizee = require('memoizee') | ||
22 | |||
23 | interface MuxingSessionEvents { | ||
24 | 'master-playlist-created': ({ videoId: number }) => void | ||
25 | |||
26 | 'bad-socket-health': ({ videoId: number }) => void | ||
27 | 'duration-exceeded': ({ videoId: number }) => void | ||
28 | 'quota-exceeded': ({ videoId: number }) => void | ||
29 | |||
30 | 'ffmpeg-end': ({ videoId: number }) => void | ||
31 | 'ffmpeg-error': ({ sessionId: string }) => void | ||
32 | |||
33 | 'after-cleanup': ({ videoId: number }) => void | ||
34 | } | ||
35 | |||
36 | declare interface MuxingSession { | ||
37 | on<U extends keyof MuxingSessionEvents>( | ||
38 | event: U, listener: MuxingSessionEvents[U] | ||
39 | ): this | ||
40 | |||
41 | emit<U extends keyof MuxingSessionEvents>( | ||
42 | event: U, ...args: Parameters<MuxingSessionEvents[U]> | ||
43 | ): boolean | ||
44 | } | ||
45 | |||
46 | class MuxingSession extends EventEmitter { | ||
47 | |||
48 | private ffmpegCommand: FfmpegCommand | ||
49 | |||
50 | private readonly context: any | ||
51 | private readonly user: MUserId | ||
52 | private readonly sessionId: string | ||
53 | private readonly videoLive: MVideoLiveVideo | ||
54 | private readonly streamingPlaylist: MStreamingPlaylistVideo | ||
55 | private readonly rtmpUrl: string | ||
56 | private readonly fps: number | ||
57 | private readonly allResolutions: number[] | ||
58 | |||
59 | private readonly videoId: number | ||
60 | private readonly videoUUID: string | ||
61 | private readonly saveReplay: boolean | ||
62 | |||
63 | private readonly lTags: LoggerTagsFn | ||
64 | |||
65 | private segmentsToProcessPerPlaylist: { [playlistId: string]: string[] } = {} | ||
66 | |||
67 | private tsWatcher: chokidar.FSWatcher | ||
68 | private masterWatcher: chokidar.FSWatcher | ||
69 | |||
70 | private readonly isAbleToUploadVideoWithCache = memoizee((userId: number) => { | ||
71 | return isAbleToUploadVideo(userId, 1000) | ||
72 | }, { maxAge: MEMOIZE_TTL.LIVE_ABLE_TO_UPLOAD }) | ||
73 | |||
74 | private readonly hasClientSocketInBadHealthWithCache = memoizee((sessionId: string) => { | ||
75 | return this.hasClientSocketInBadHealth(sessionId) | ||
76 | }, { maxAge: MEMOIZE_TTL.LIVE_CHECK_SOCKET_HEALTH }) | ||
77 | |||
78 | constructor (options: { | ||
79 | context: any | ||
80 | user: MUserId | ||
81 | sessionId: string | ||
82 | videoLive: MVideoLiveVideo | ||
83 | streamingPlaylist: MStreamingPlaylistVideo | ||
84 | rtmpUrl: string | ||
85 | fps: number | ||
86 | allResolutions: number[] | ||
87 | }) { | ||
88 | super() | ||
89 | |||
90 | this.context = options.context | ||
91 | this.user = options.user | ||
92 | this.sessionId = options.sessionId | ||
93 | this.videoLive = options.videoLive | ||
94 | this.streamingPlaylist = options.streamingPlaylist | ||
95 | this.rtmpUrl = options.rtmpUrl | ||
96 | this.fps = options.fps | ||
97 | this.allResolutions = options.allResolutions | ||
98 | |||
99 | this.videoId = this.videoLive.Video.id | ||
100 | this.videoUUID = this.videoLive.Video.uuid | ||
101 | |||
102 | this.saveReplay = this.videoLive.saveReplay | ||
103 | |||
104 | this.lTags = loggerTagsFactory('live', this.sessionId, this.videoUUID) | ||
105 | } | ||
106 | |||
107 | async runMuxing () { | ||
108 | this.createFiles() | ||
109 | |||
110 | const outPath = await this.prepareDirectories() | ||
111 | |||
112 | this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED | ||
113 | ? await getLiveTranscodingCommand({ | ||
114 | rtmpUrl: this.rtmpUrl, | ||
115 | outPath, | ||
116 | resolutions: this.allResolutions, | ||
117 | fps: this.fps, | ||
118 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | ||
119 | profile: CONFIG.LIVE.TRANSCODING.PROFILE | ||
120 | }) | ||
121 | : getLiveMuxingCommand(this.rtmpUrl, outPath) | ||
122 | |||
123 | logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags) | ||
124 | |||
125 | this.watchTSFiles(outPath) | ||
126 | this.watchMasterFile(outPath) | ||
127 | |||
128 | this.ffmpegCommand.on('error', (err, stdout, stderr) => { | ||
129 | this.onFFmpegError(err, stdout, stderr, outPath) | ||
130 | }) | ||
131 | |||
132 | this.ffmpegCommand.on('end', () => this.onFFmpegEnded(outPath)) | ||
133 | |||
134 | this.ffmpegCommand.run() | ||
135 | } | ||
136 | |||
137 | abort () { | ||
138 | if (!this.ffmpegCommand) return | ||
139 | |||
140 | this.ffmpegCommand.kill('SIGINT') | ||
141 | } | ||
142 | |||
143 | destroy () { | ||
144 | this.removeAllListeners() | ||
145 | this.isAbleToUploadVideoWithCache.clear() | ||
146 | this.hasClientSocketInBadHealthWithCache.clear() | ||
147 | } | ||
148 | |||
149 | private onFFmpegError (err: any, stdout: string, stderr: string, outPath: string) { | ||
150 | this.onFFmpegEnded(outPath) | ||
151 | |||
152 | // Don't care that we killed the ffmpeg process | ||
153 | if (err?.message?.includes('Exiting normally')) return | ||
154 | |||
155 | logger.error('Live transcoding error.', { err, stdout, stderr, ...this.lTags }) | ||
156 | |||
157 | this.emit('ffmpeg-error', ({ sessionId: this.sessionId })) | ||
158 | } | ||
159 | |||
160 | private onFFmpegEnded (outPath: string) { | ||
161 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.rtmpUrl, this.lTags) | ||
162 | |||
163 | setTimeout(() => { | ||
164 | // Wait latest segments generation, and close watchers | ||
165 | |||
166 | Promise.all([ this.tsWatcher.close(), this.masterWatcher.close() ]) | ||
167 | .then(() => { | ||
168 | // Process remaining segments hash | ||
169 | for (const key of Object.keys(this.segmentsToProcessPerPlaylist)) { | ||
170 | this.processSegments(outPath, this.segmentsToProcessPerPlaylist[key]) | ||
171 | } | ||
172 | }) | ||
173 | .catch(err => { | ||
174 | logger.error( | ||
175 | 'Cannot close watchers of %s or process remaining hash segments.', outPath, | ||
176 | { err, ...this.lTags } | ||
177 | ) | ||
178 | }) | ||
179 | |||
180 | this.emit('after-cleanup', { videoId: this.videoId }) | ||
181 | }, 1000) | ||
182 | } | ||
183 | |||
184 | private watchMasterFile (outPath: string) { | ||
185 | this.masterWatcher = chokidar.watch(outPath + '/master.m3u8') | ||
186 | |||
187 | this.masterWatcher.on('add', async () => { | ||
188 | this.emit('master-playlist-created', { videoId: this.videoId }) | ||
189 | |||
190 | this.masterWatcher.close() | ||
191 | .catch(err => logger.error('Cannot close master watcher of %s.', outPath, { err, ...this.lTags })) | ||
192 | }) | ||
193 | } | ||
194 | |||
195 | private watchTSFiles (outPath: string) { | ||
196 | const startStreamDateTime = new Date().getTime() | ||
197 | |||
198 | this.tsWatcher = chokidar.watch(outPath + '/*.ts') | ||
199 | |||
200 | const playlistIdMatcher = /^([\d+])-/ | ||
201 | |||
202 | const addHandler = async segmentPath => { | ||
203 | logger.debug('Live add handler of %s.', segmentPath, this.lTags) | ||
204 | |||
205 | const playlistId = basename(segmentPath).match(playlistIdMatcher)[0] | ||
206 | |||
207 | const segmentsToProcess = this.segmentsToProcessPerPlaylist[playlistId] || [] | ||
208 | this.processSegments(outPath, segmentsToProcess) | ||
209 | |||
210 | this.segmentsToProcessPerPlaylist[playlistId] = [ segmentPath ] | ||
211 | |||
212 | if (this.hasClientSocketInBadHealthWithCache(this.sessionId)) { | ||
213 | this.emit('bad-socket-health', { videoId: this.videoId }) | ||
214 | return | ||
215 | } | ||
216 | |||
217 | // Duration constraint check | ||
218 | if (this.isDurationConstraintValid(startStreamDateTime) !== true) { | ||
219 | this.emit('duration-exceeded', { videoId: this.videoId }) | ||
220 | return | ||
221 | } | ||
222 | |||
223 | // Check user quota if the user enabled replay saving | ||
224 | if (await this.isQuotaExceeded(segmentPath) === true) { | ||
225 | this.emit('quota-exceeded', { videoId: this.videoId }) | ||
226 | } | ||
227 | } | ||
228 | |||
229 | const deleteHandler = segmentPath => LiveSegmentShaStore.Instance.removeSegmentSha(this.videoUUID, segmentPath) | ||
230 | |||
231 | this.tsWatcher.on('add', p => addHandler(p)) | ||
232 | this.tsWatcher.on('unlink', p => deleteHandler(p)) | ||
233 | } | ||
234 | |||
235 | private async isQuotaExceeded (segmentPath: string) { | ||
236 | if (this.saveReplay !== true) return false | ||
237 | |||
238 | try { | ||
239 | const segmentStat = await stat(segmentPath) | ||
240 | |||
241 | LiveQuotaStore.Instance.addQuotaTo(this.user.id, this.videoLive.id, segmentStat.size) | ||
242 | |||
243 | const canUpload = await this.isAbleToUploadVideoWithCache(this.user.id) | ||
244 | |||
245 | return canUpload !== true | ||
246 | } catch (err) { | ||
247 | logger.error('Cannot stat %s or check quota of %d.', segmentPath, this.user.id, { err, ...this.lTags }) | ||
248 | } | ||
249 | } | ||
250 | |||
251 | private createFiles () { | ||
252 | for (let i = 0; i < this.allResolutions.length; i++) { | ||
253 | const resolution = this.allResolutions[i] | ||
254 | |||
255 | const file = new VideoFileModel({ | ||
256 | resolution, | ||
257 | size: -1, | ||
258 | extname: '.ts', | ||
259 | infoHash: null, | ||
260 | fps: this.fps, | ||
261 | videoStreamingPlaylistId: this.streamingPlaylist.id | ||
262 | }) | ||
263 | |||
264 | VideoFileModel.customUpsert(file, 'streaming-playlist', null) | ||
265 | .catch(err => logger.error('Cannot create file for live streaming.', { err, ...this.lTags })) | ||
266 | } | ||
267 | } | ||
268 | |||
269 | private async prepareDirectories () { | ||
270 | const outPath = getHLSDirectory(this.videoLive.Video) | ||
271 | await ensureDir(outPath) | ||
272 | |||
273 | const replayDirectory = join(outPath, VIDEO_LIVE.REPLAY_DIRECTORY) | ||
274 | |||
275 | if (this.videoLive.saveReplay === true) { | ||
276 | await ensureDir(replayDirectory) | ||
277 | } | ||
278 | |||
279 | return outPath | ||
280 | } | ||
281 | |||
282 | private isDurationConstraintValid (streamingStartTime: number) { | ||
283 | const maxDuration = CONFIG.LIVE.MAX_DURATION | ||
284 | // No limit | ||
285 | if (maxDuration < 0) return true | ||
286 | |||
287 | const now = new Date().getTime() | ||
288 | const max = streamingStartTime + maxDuration | ||
289 | |||
290 | return now <= max | ||
291 | } | ||
292 | |||
293 | private processSegments (hlsVideoPath: string, segmentPaths: string[]) { | ||
294 | Bluebird.mapSeries(segmentPaths, async previousSegment => { | ||
295 | // Add sha hash of previous segments, because ffmpeg should have finished generating them | ||
296 | await LiveSegmentShaStore.Instance.addSegmentSha(this.videoUUID, previousSegment) | ||
297 | |||
298 | if (this.saveReplay) { | ||
299 | await this.addSegmentToReplay(hlsVideoPath, previousSegment) | ||
300 | } | ||
301 | }).catch(err => logger.error('Cannot process segments in %s', hlsVideoPath, { err, ...this.lTags })) | ||
302 | } | ||
303 | |||
304 | private hasClientSocketInBadHealth (sessionId: string) { | ||
305 | const rtmpSession = this.context.sessions.get(sessionId) | ||
306 | |||
307 | if (!rtmpSession) { | ||
308 | logger.warn('Cannot get session %s to check players socket health.', sessionId, this.lTags) | ||
309 | return | ||
310 | } | ||
311 | |||
312 | for (const playerSessionId of rtmpSession.players) { | ||
313 | const playerSession = this.context.sessions.get(playerSessionId) | ||
314 | |||
315 | if (!playerSession) { | ||
316 | logger.error('Cannot get player session %s to check socket health.', playerSession, this.lTags) | ||
317 | continue | ||
318 | } | ||
319 | |||
320 | if (playerSession.socket.writableLength > VIDEO_LIVE.MAX_SOCKET_WAITING_DATA) { | ||
321 | return true | ||
322 | } | ||
323 | } | ||
324 | |||
325 | return false | ||
326 | } | ||
327 | |||
328 | private async addSegmentToReplay (hlsVideoPath: string, segmentPath: string) { | ||
329 | const segmentName = basename(segmentPath) | ||
330 | const dest = join(hlsVideoPath, VIDEO_LIVE.REPLAY_DIRECTORY, buildConcatenatedName(segmentName)) | ||
331 | |||
332 | try { | ||
333 | const data = await readFile(segmentPath) | ||
334 | |||
335 | await appendFile(dest, data) | ||
336 | } catch (err) { | ||
337 | logger.error('Cannot copy segment %s to replay directory.', segmentPath, { err, ...this.lTags }) | ||
338 | } | ||
339 | } | ||
340 | } | ||
341 | |||
342 | // --------------------------------------------------------------------------- | ||
343 | |||
344 | export { | ||
345 | MuxingSession | ||
346 | } | ||
diff --git a/server/lib/actor-image.ts b/server/lib/local-actor.ts index f271f0b5b..77667f6b0 100644 --- a/server/lib/actor-image.ts +++ b/server/lib/local-actor.ts | |||
@@ -1,19 +1,38 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import { queue } from 'async' | 2 | import { queue } from 'async' |
3 | import * as LRUCache from 'lru-cache' | 3 | import * as LRUCache from 'lru-cache' |
4 | import { extname, join } from 'path' | 4 | import { join } from 'path' |
5 | import { v4 as uuidv4 } from 'uuid' | 5 | import { getLowercaseExtension } from '@server/helpers/core-utils' |
6 | import { ActorImageType } from '@shared/models' | 6 | import { buildUUID } from '@server/helpers/uuid' |
7 | import { ActorModel } from '@server/models/actor/actor' | ||
8 | import { ActivityPubActorType, ActorImageType } from '@shared/models' | ||
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | 9 | import { retryTransactionWrapper } from '../helpers/database-utils' |
8 | import { processImage } from '../helpers/image-utils' | 10 | import { processImage } from '../helpers/image-utils' |
9 | import { downloadImage } from '../helpers/requests' | 11 | import { downloadImage } from '../helpers/requests' |
10 | import { CONFIG } from '../initializers/config' | 12 | import { CONFIG } from '../initializers/config' |
11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | 13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' |
12 | import { sequelizeTypescript } from '../initializers/database' | 14 | import { sequelizeTypescript } from '../initializers/database' |
13 | import { MAccountDefault, MChannelDefault } from '../types/models' | 15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' |
14 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor' | 16 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actors' |
15 | import { sendUpdateActor } from './activitypub/send' | 17 | import { sendUpdateActor } from './activitypub/send' |
16 | 18 | ||
19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { | ||
20 | return new ActorModel({ | ||
21 | type, | ||
22 | url, | ||
23 | preferredUsername, | ||
24 | publicKey: null, | ||
25 | privateKey: null, | ||
26 | followersCount: 0, | ||
27 | followingCount: 0, | ||
28 | inboxUrl: url + '/inbox', | ||
29 | outboxUrl: url + '/outbox', | ||
30 | sharedInboxUrl: WEBSERVER.URL + '/inbox', | ||
31 | followersUrl: url + '/followers', | ||
32 | followingUrl: url + '/following' | ||
33 | }) as MActor | ||
34 | } | ||
35 | |||
17 | async function updateLocalActorImageFile ( | 36 | async function updateLocalActorImageFile ( |
18 | accountOrChannel: MAccountDefault | MChannelDefault, | 37 | accountOrChannel: MAccountDefault | MChannelDefault, |
19 | imagePhysicalFile: Express.Multer.File, | 38 | imagePhysicalFile: Express.Multer.File, |
@@ -23,9 +42,9 @@ async function updateLocalActorImageFile ( | |||
23 | ? ACTOR_IMAGES_SIZE.AVATARS | 42 | ? ACTOR_IMAGES_SIZE.AVATARS |
24 | : ACTOR_IMAGES_SIZE.BANNERS | 43 | : ACTOR_IMAGES_SIZE.BANNERS |
25 | 44 | ||
26 | const extension = extname(imagePhysicalFile.filename) | 45 | const extension = getLowercaseExtension(imagePhysicalFile.filename) |
27 | 46 | ||
28 | const imageName = uuidv4() + extension | 47 | const imageName = buildUUID() + extension |
29 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | 48 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) |
30 | await processImage(imagePhysicalFile.path, destination, imageSize) | 49 | await processImage(imagePhysicalFile.path, destination, imageSize) |
31 | 50 | ||
@@ -93,5 +112,6 @@ export { | |||
93 | actorImagePathUnsafeCache, | 112 | actorImagePathUnsafeCache, |
94 | updateLocalActorImageFile, | 113 | updateLocalActorImageFile, |
95 | deleteLocalActorImageFile, | 114 | deleteLocalActorImageFile, |
96 | pushActorImageProcessInQueue | 115 | pushActorImageProcessInQueue, |
116 | buildActorInstance | ||
97 | } | 117 | } |
diff --git a/server/lib/model-loaders/actor.ts b/server/lib/model-loaders/actor.ts new file mode 100644 index 000000000..1355d8ee2 --- /dev/null +++ b/server/lib/model-loaders/actor.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | |||
2 | import { ActorModel } from '../../models/actor/actor' | ||
3 | import { MActorAccountChannelId, MActorFull } from '../../types/models' | ||
4 | |||
5 | type ActorLoadByUrlType = 'all' | 'association-ids' | ||
6 | |||
7 | function loadActorByUrl (url: string, fetchType: ActorLoadByUrlType): Promise<MActorFull | MActorAccountChannelId> { | ||
8 | if (fetchType === 'all') return ActorModel.loadByUrlAndPopulateAccountAndChannel(url) | ||
9 | |||
10 | if (fetchType === 'association-ids') return ActorModel.loadByUrl(url) | ||
11 | } | ||
12 | |||
13 | export { | ||
14 | ActorLoadByUrlType, | ||
15 | |||
16 | loadActorByUrl | ||
17 | } | ||
diff --git a/server/lib/model-loaders/index.ts b/server/lib/model-loaders/index.ts new file mode 100644 index 000000000..9e5152cb2 --- /dev/null +++ b/server/lib/model-loaders/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './actor' | ||
2 | export * from './video' | ||
diff --git a/server/lib/model-loaders/video.ts b/server/lib/model-loaders/video.ts new file mode 100644 index 000000000..0a3c15ad8 --- /dev/null +++ b/server/lib/model-loaders/video.ts | |||
@@ -0,0 +1,73 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | ||
2 | import { | ||
3 | MVideoAccountLightBlacklistAllFiles, | ||
4 | MVideoFormattableDetails, | ||
5 | MVideoFullLight, | ||
6 | MVideoId, | ||
7 | MVideoImmutable, | ||
8 | MVideoThumbnail | ||
9 | } from '@server/types/models' | ||
10 | import { Hooks } from '../plugins/hooks' | ||
11 | |||
12 | type VideoLoadType = 'for-api' | 'all' | 'only-video' | 'id' | 'none' | 'only-immutable-attributes' | ||
13 | |||
14 | function loadVideo (id: number | string, fetchType: 'for-api', userId?: number): Promise<MVideoFormattableDetails> | ||
15 | function loadVideo (id: number | string, fetchType: 'all', userId?: number): Promise<MVideoFullLight> | ||
16 | function loadVideo (id: number | string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable> | ||
17 | function loadVideo (id: number | string, fetchType: 'only-video', userId?: number): Promise<MVideoThumbnail> | ||
18 | function loadVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Promise<MVideoId> | ||
19 | function loadVideo ( | ||
20 | id: number | string, | ||
21 | fetchType: VideoLoadType, | ||
22 | userId?: number | ||
23 | ): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable> | ||
24 | function loadVideo ( | ||
25 | id: number | string, | ||
26 | fetchType: VideoLoadType, | ||
27 | userId?: number | ||
28 | ): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable> { | ||
29 | |||
30 | if (fetchType === 'for-api') { | ||
31 | return Hooks.wrapPromiseFun( | ||
32 | VideoModel.loadForGetAPI, | ||
33 | { id, userId }, | ||
34 | 'filter:api.video.get.result' | ||
35 | ) | ||
36 | } | ||
37 | |||
38 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) | ||
39 | |||
40 | if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id) | ||
41 | |||
42 | if (fetchType === 'only-video') return VideoModel.load(id) | ||
43 | |||
44 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) | ||
45 | } | ||
46 | |||
47 | type VideoLoadByUrlType = 'all' | 'only-video' | 'only-immutable-attributes' | ||
48 | |||
49 | function loadVideoByUrl (url: string, fetchType: 'all'): Promise<MVideoAccountLightBlacklistAllFiles> | ||
50 | function loadVideoByUrl (url: string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable> | ||
51 | function loadVideoByUrl (url: string, fetchType: 'only-video'): Promise<MVideoThumbnail> | ||
52 | function loadVideoByUrl ( | ||
53 | url: string, | ||
54 | fetchType: VideoLoadByUrlType | ||
55 | ): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> | ||
56 | function loadVideoByUrl ( | ||
57 | url: string, | ||
58 | fetchType: VideoLoadByUrlType | ||
59 | ): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { | ||
60 | if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url) | ||
61 | |||
62 | if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url) | ||
63 | |||
64 | if (fetchType === 'only-video') return VideoModel.loadByUrl(url) | ||
65 | } | ||
66 | |||
67 | export { | ||
68 | VideoLoadType, | ||
69 | VideoLoadByUrlType, | ||
70 | |||
71 | loadVideo, | ||
72 | loadVideoByUrl | ||
73 | } | ||
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index 925d64902..14e00518e 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts | |||
@@ -23,9 +23,9 @@ import { ActivityCreate } from '../../shared/models/activitypub' | |||
23 | import { VideoObject } from '../../shared/models/activitypub/objects' | 23 | import { VideoObject } from '../../shared/models/activitypub/objects' |
24 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' | 24 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' |
25 | import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' | 25 | import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' |
26 | import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' | 26 | import { VideoCommentCreate } from '../../shared/models/videos/comment/video-comment.model' |
27 | import { UserModel } from '../models/account/user' | 27 | import { ActorModel } from '../models/actor/actor' |
28 | import { ActorModel } from '../models/activitypub/actor' | 28 | import { UserModel } from '../models/user/user' |
29 | import { VideoModel } from '../models/video/video' | 29 | import { VideoModel } from '../models/video/video' |
30 | import { VideoCommentModel } from '../models/video/video-comment' | 30 | import { VideoCommentModel } from '../models/video/video-comment' |
31 | import { sendAbuse } from './activitypub/send/send-flag' | 31 | import { sendAbuse } from './activitypub/send/send-flag' |
@@ -221,7 +221,7 @@ async function createAbuse (options: { | |||
221 | const { isOwned } = await associateFun(abuseInstance) | 221 | const { isOwned } = await associateFun(abuseInstance) |
222 | 222 | ||
223 | if (isOwned === false) { | 223 | if (isOwned === false) { |
224 | await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction) | 224 | sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction) |
225 | } | 225 | } |
226 | 226 | ||
227 | const abuseJSON = abuseInstance.toFormattedAdminJSON() | 227 | const abuseJSON = abuseInstance.toFormattedAdminJSON() |
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts index da7f7cc05..1f9ff16df 100644 --- a/server/lib/notifier.ts +++ b/server/lib/notifier.ts | |||
@@ -17,8 +17,8 @@ import { VideoPrivacy, VideoState } from '../../shared/models/videos' | |||
17 | import { logger } from '../helpers/logger' | 17 | import { logger } from '../helpers/logger' |
18 | import { CONFIG } from '../initializers/config' | 18 | import { CONFIG } from '../initializers/config' |
19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
20 | import { UserModel } from '../models/account/user' | 20 | import { UserModel } from '../models/user/user' |
21 | import { UserNotificationModel } from '../models/account/user-notification' | 21 | import { UserNotificationModel } from '../models/user/user-notification' |
22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' | 22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' |
23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' | 23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' |
24 | import { isBlockedByServerOrAccount } from './blocklist' | 24 | import { isBlockedByServerOrAccount } from './blocklist' |
diff --git a/server/lib/plugins/hooks.ts b/server/lib/plugins/hooks.ts index aa92f03cc..5e97b52a0 100644 --- a/server/lib/plugins/hooks.ts +++ b/server/lib/plugins/hooks.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models/plugins/server-hook.model' | ||
2 | import { PluginManager } from './plugin-manager' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import { PluginManager } from './plugin-manager' | ||
5 | 5 | ||
6 | type PromiseFunction <U, T> = (params: U) => Promise<T> | Bluebird<T> | 6 | type PromiseFunction <U, T> = (params: U) => Promise<T> | Bluebird<T> |
7 | type RawFunction <U, T> = (params: U) => T | 7 | type RawFunction <U, T> = (params: U) => T |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index f1bc24d8b..8487672ba 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -15,9 +15,9 @@ import { MPlugin } from '@server/types/models' | |||
15 | import { PeerTubeHelpers } from '@server/types/plugins' | 15 | import { PeerTubeHelpers } from '@server/types/plugins' |
16 | import { VideoBlacklistCreate } from '@shared/models' | 16 | import { VideoBlacklistCreate } from '@shared/models' |
17 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' | 17 | import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist' |
18 | import { getServerConfig } from '../config' | 18 | import { ServerConfigManager } from '../server-config-manager' |
19 | import { blacklistVideo, unblacklistVideo } from '../video-blacklist' | 19 | import { blacklistVideo, unblacklistVideo } from '../video-blacklist' |
20 | import { UserModel } from '@server/models/account/user' | 20 | import { UserModel } from '@server/models/user/user' |
21 | 21 | ||
22 | function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { | 22 | function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers { |
23 | const logger = buildPluginLogger(npmName) | 23 | const logger = buildPluginLogger(npmName) |
@@ -147,7 +147,7 @@ function buildConfigHelpers () { | |||
147 | }, | 147 | }, |
148 | 148 | ||
149 | getServerConfig () { | 149 | getServerConfig () { |
150 | return getServerConfig() | 150 | return ServerConfigManager.Instance.getServerConfig() |
151 | } | 151 | } |
152 | } | 152 | } |
153 | } | 153 | } |
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts index 165bc91b3..119cee8e0 100644 --- a/server/lib/plugins/plugin-index.ts +++ b/server/lib/plugins/plugin-index.ts | |||
@@ -1,16 +1,16 @@ | |||
1 | import { sanitizeUrl } from '@server/helpers/core-utils' | 1 | import { sanitizeUrl } from '@server/helpers/core-utils' |
2 | import { ResultList } from '../../../shared/models' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' | 3 | import { doJSONRequest } from '@server/helpers/requests' |
4 | import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' | 4 | import { CONFIG } from '@server/initializers/config' |
5 | import { PEERTUBE_VERSION } from '@server/initializers/constants' | ||
6 | import { PluginModel } from '@server/models/server/plugin' | ||
5 | import { | 7 | import { |
8 | PeerTubePluginIndex, | ||
9 | PeertubePluginIndexList, | ||
6 | PeertubePluginLatestVersionRequest, | 10 | PeertubePluginLatestVersionRequest, |
7 | PeertubePluginLatestVersionResponse | 11 | PeertubePluginLatestVersionResponse, |
8 | } from '../../../shared/models/plugins/peertube-plugin-latest-version.model' | 12 | ResultList |
9 | import { logger } from '../../helpers/logger' | 13 | } from '@shared/models' |
10 | import { doJSONRequest } from '../../helpers/requests' | ||
11 | import { CONFIG } from '../../initializers/config' | ||
12 | import { PEERTUBE_VERSION } from '../../initializers/constants' | ||
13 | import { PluginModel } from '../../models/server/plugin' | ||
14 | import { PluginManager } from './plugin-manager' | 14 | import { PluginManager } from './plugin-manager' |
15 | 15 | ||
16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { | 16 | async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { |
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index ba9814383..6599bccca 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -4,16 +4,11 @@ import { createReadStream, createWriteStream } from 'fs' | |||
4 | import { ensureDir, outputFile, readJSON } from 'fs-extra' | 4 | import { ensureDir, outputFile, readJSON } from 'fs-extra' |
5 | import { basename, join } from 'path' | 5 | import { basename, join } from 'path' |
6 | import { MOAuthTokenUser, MUser } from '@server/types/models' | 6 | import { MOAuthTokenUser, MUser } from '@server/types/models' |
7 | import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' | 7 | import { getCompleteLocale } from '@shared/core-utils' |
8 | import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models' | ||
8 | import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' | 9 | import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' |
9 | import { | ||
10 | ClientScript, | ||
11 | PluginPackageJson, | ||
12 | PluginTranslationPaths as PackagePluginTranslations | ||
13 | } from '../../../shared/models/plugins/plugin-package-json.model' | ||
14 | import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' | ||
15 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 10 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
16 | import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model' | 11 | import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server/server-hook.model' |
17 | import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' | 12 | import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' |
18 | import { logger } from '../../helpers/logger' | 13 | import { logger } from '../../helpers/logger' |
19 | import { CONFIG } from '../../initializers/config' | 14 | import { CONFIG } from '../../initializers/config' |
@@ -23,7 +18,6 @@ import { PluginLibrary, RegisterServerAuthExternalOptions, RegisterServerAuthPas | |||
23 | import { ClientHtml } from '../client-html' | 18 | import { ClientHtml } from '../client-html' |
24 | import { RegisterHelpers } from './register-helpers' | 19 | import { RegisterHelpers } from './register-helpers' |
25 | import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' | 20 | import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' |
26 | import { getCompleteLocale } from '@shared/core-utils' | ||
27 | 21 | ||
28 | export interface RegisteredPlugin { | 22 | export interface RegisteredPlugin { |
29 | npmName: string | 23 | npmName: string |
@@ -310,22 +304,28 @@ export class PluginManager implements ServerHook { | |||
310 | uninstalled: false, | 304 | uninstalled: false, |
311 | peertubeEngine: packageJSON.engine.peertube | 305 | peertubeEngine: packageJSON.engine.peertube |
312 | }, { returning: true }) | 306 | }, { returning: true }) |
313 | } catch (err) { | 307 | |
314 | logger.error('Cannot install plugin %s, removing it...', toInstall, { err }) | 308 | logger.info('Successful installation of plugin %s.', toInstall) |
309 | |||
310 | await this.registerPluginOrTheme(plugin) | ||
311 | } catch (rootErr) { | ||
312 | logger.error('Cannot install plugin %s, removing it...', toInstall, { err: rootErr }) | ||
315 | 313 | ||
316 | try { | 314 | try { |
317 | await removeNpmPlugin(npmName) | 315 | await this.uninstall(npmName) |
318 | } catch (err) { | 316 | } catch (err) { |
319 | logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err }) | 317 | logger.error('Cannot uninstall plugin %s after failed installation.', toInstall, { err }) |
318 | |||
319 | try { | ||
320 | await removeNpmPlugin(npmName) | ||
321 | } catch (err) { | ||
322 | logger.error('Cannot remove plugin %s after failed installation.', toInstall, { err }) | ||
323 | } | ||
320 | } | 324 | } |
321 | 325 | ||
322 | throw err | 326 | throw rootErr |
323 | } | 327 | } |
324 | 328 | ||
325 | logger.info('Successful installation of plugin %s.', toInstall) | ||
326 | |||
327 | await this.registerPluginOrTheme(plugin) | ||
328 | |||
329 | return plugin | 329 | return plugin |
330 | } | 330 | } |
331 | 331 | ||
@@ -431,8 +431,7 @@ export class PluginManager implements ServerHook { | |||
431 | 431 | ||
432 | await ensureDir(registerOptions.peertubeHelpers.plugin.getDataDirectoryPath()) | 432 | await ensureDir(registerOptions.peertubeHelpers.plugin.getDataDirectoryPath()) |
433 | 433 | ||
434 | library.register(registerOptions) | 434 | await library.register(registerOptions) |
435 | .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) | ||
436 | 435 | ||
437 | logger.info('Add plugin %s CSS to global file.', npmName) | 436 | logger.info('Add plugin %s CSS to global file.', npmName) |
438 | 437 | ||
@@ -443,7 +442,7 @@ export class PluginManager implements ServerHook { | |||
443 | 442 | ||
444 | // ###################### Translations ###################### | 443 | // ###################### Translations ###################### |
445 | 444 | ||
446 | private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) { | 445 | private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PluginTranslationPaths) { |
447 | for (const locale of Object.keys(translationPaths)) { | 446 | for (const locale of Object.keys(translationPaths)) { |
448 | const path = translationPaths[locale] | 447 | const path = translationPaths[locale] |
449 | const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path)) | 448 | const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path)) |
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts index aa69ca2a2..09275f9ba 100644 --- a/server/lib/plugins/register-helpers.ts +++ b/server/lib/plugins/register-helpers.ts | |||
@@ -26,10 +26,10 @@ import { | |||
26 | PluginVideoLicenceManager, | 26 | PluginVideoLicenceManager, |
27 | PluginVideoPrivacyManager, | 27 | PluginVideoPrivacyManager, |
28 | RegisterServerHookOptions, | 28 | RegisterServerHookOptions, |
29 | RegisterServerSettingOptions | 29 | RegisterServerSettingOptions, |
30 | serverHookObject | ||
30 | } from '@shared/models' | 31 | } from '@shared/models' |
31 | import { serverHookObject } from '@shared/models/plugins/server-hook.model' | 32 | import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles' |
32 | import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles' | ||
33 | import { buildPluginHelpers } from './plugin-helpers-builder' | 33 | import { buildPluginHelpers } from './plugin-helpers-builder' |
34 | 34 | ||
35 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' | 35 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' |
@@ -37,18 +37,20 @@ type VideoConstant = { [key in number | string]: string } | |||
37 | 37 | ||
38 | type UpdatedVideoConstant = { | 38 | type UpdatedVideoConstant = { |
39 | [name in AlterableVideoConstant]: { | 39 | [name in AlterableVideoConstant]: { |
40 | added: { key: number | string, label: string }[] | 40 | [ npmName: string]: { |
41 | deleted: { key: number | string, label: string }[] | 41 | added: { key: number | string, label: string }[] |
42 | deleted: { key: number | string, label: string }[] | ||
43 | } | ||
42 | } | 44 | } |
43 | } | 45 | } |
44 | 46 | ||
45 | export class RegisterHelpers { | 47 | export class RegisterHelpers { |
46 | private readonly updatedVideoConstants: UpdatedVideoConstant = { | 48 | private readonly updatedVideoConstants: UpdatedVideoConstant = { |
47 | playlistPrivacy: { added: [], deleted: [] }, | 49 | playlistPrivacy: { }, |
48 | privacy: { added: [], deleted: [] }, | 50 | privacy: { }, |
49 | language: { added: [], deleted: [] }, | 51 | language: { }, |
50 | licence: { added: [], deleted: [] }, | 52 | licence: { }, |
51 | category: { added: [], deleted: [] } | 53 | category: { } |
52 | } | 54 | } |
53 | 55 | ||
54 | private readonly transcodingProfiles: { | 56 | private readonly transcodingProfiles: { |
@@ -377,7 +379,7 @@ export class RegisterHelpers { | |||
377 | const { npmName, type, obj, key } = parameters | 379 | const { npmName, type, obj, key } = parameters |
378 | 380 | ||
379 | if (!obj[key]) { | 381 | if (!obj[key]) { |
380 | logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key) | 382 | logger.warn('Cannot delete %s by plugin %s: key %s does not exist.', type, npmName, key) |
381 | return false | 383 | return false |
382 | } | 384 | } |
383 | 385 | ||
@@ -388,7 +390,15 @@ export class RegisterHelpers { | |||
388 | } | 390 | } |
389 | } | 391 | } |
390 | 392 | ||
391 | this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] }) | 393 | const updatedConstants = this.updatedVideoConstants[type][npmName] |
394 | |||
395 | const alreadyAdded = updatedConstants.added.find(a => a.key === key) | ||
396 | if (alreadyAdded) { | ||
397 | updatedConstants.added.filter(a => a.key !== key) | ||
398 | } else if (obj[key]) { | ||
399 | updatedConstants.deleted.push({ key, label: obj[key] }) | ||
400 | } | ||
401 | |||
392 | delete obj[key] | 402 | delete obj[key] |
393 | 403 | ||
394 | return true | 404 | return true |
diff --git a/server/lib/redundancy.ts b/server/lib/redundancy.ts index da620b607..2a9241249 100644 --- a/server/lib/redundancy.ts +++ b/server/lib/redundancy.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | ||
2 | import { sendUndoCacheFile } from './activitypub/send' | ||
3 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
4 | import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models' | ||
5 | import { CONFIG } from '@server/initializers/config' | ||
6 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
7 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 3 | import { CONFIG } from '@server/initializers/config' |
8 | import { Activity } from '@shared/models' | 4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
9 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models' | ||
7 | import { Activity } from '@shared/models' | ||
8 | import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | ||
9 | import { sendUndoCacheFile } from './activitypub/send' | ||
10 | 10 | ||
11 | async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) { | 11 | async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) { |
12 | const serverActor = await getServerActor() | 12 | const serverActor = await getServerActor() |
diff --git a/server/lib/schedulers/actor-follow-scheduler.ts b/server/lib/schedulers/actor-follow-scheduler.ts index 598c0211f..1b80316e9 100644 --- a/server/lib/schedulers/actor-follow-scheduler.ts +++ b/server/lib/schedulers/actor-follow-scheduler.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { isTestInstance } from '../../helpers/core-utils' | 1 | import { isTestInstance } from '../../helpers/core-utils' |
2 | import { logger } from '../../helpers/logger' | 2 | import { logger } from '../../helpers/logger' |
3 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | ||
4 | import { AbstractScheduler } from './abstract-scheduler' | ||
5 | import { ACTOR_FOLLOW_SCORE, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 3 | import { ACTOR_FOLLOW_SCORE, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
4 | import { ActorFollowModel } from '../../models/actor/actor-follow' | ||
6 | import { ActorFollowScoreCache } from '../files-cache' | 5 | import { ActorFollowScoreCache } from '../files-cache' |
6 | import { AbstractScheduler } from './abstract-scheduler' | ||
7 | 7 | ||
8 | export class ActorFollowScheduler extends AbstractScheduler { | 8 | export class ActorFollowScheduler extends AbstractScheduler { |
9 | 9 | ||
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts index 0b8cd1389..aaa5feed5 100644 --- a/server/lib/schedulers/auto-follow-index-instances.ts +++ b/server/lib/schedulers/auto-follow-index-instances.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { chunk } from 'lodash' | 1 | import { chunk } from 'lodash' |
2 | import { doJSONRequest } from '@server/helpers/requests' | 2 | import { doJSONRequest } from '@server/helpers/requests' |
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
7 | import { CONFIG } from '../../initializers/config' | 7 | import { CONFIG } from '../../initializers/config' |
diff --git a/server/lib/schedulers/remove-old-history-scheduler.ts b/server/lib/schedulers/remove-old-history-scheduler.ts index 17a42b2c4..225669ea2 100644 --- a/server/lib/schedulers/remove-old-history-scheduler.ts +++ b/server/lib/schedulers/remove-old-history-scheduler.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { logger } from '../../helpers/logger' | 1 | import { logger } from '../../helpers/logger' |
2 | import { AbstractScheduler } from './abstract-scheduler' | 2 | import { AbstractScheduler } from './abstract-scheduler' |
3 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 3 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
4 | import { UserVideoHistoryModel } from '../../models/account/user-video-history' | 4 | import { UserVideoHistoryModel } from '../../models/user/user-video-history' |
5 | import { CONFIG } from '../../initializers/config' | 5 | import { CONFIG } from '../../initializers/config' |
6 | 6 | ||
7 | export class RemoveOldHistoryScheduler extends AbstractScheduler { | 7 | export class RemoveOldHistoryScheduler extends AbstractScheduler { |
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 3e75babcb..af69bda89 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { VideoModel } from '@server/models/video/video' | ||
2 | import { MVideoFullLight } from '@server/types/models' | ||
1 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
2 | import { AbstractScheduler } from './abstract-scheduler' | 4 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
5 | import { sequelizeTypescript } from '../../initializers/database' | ||
3 | import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' | 6 | import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' |
4 | import { retryTransactionWrapper } from '../../helpers/database-utils' | ||
5 | import { federateVideoIfNeeded } from '../activitypub/videos' | 7 | import { federateVideoIfNeeded } from '../activitypub/videos' |
6 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | ||
7 | import { Notifier } from '../notifier' | 8 | import { Notifier } from '../notifier' |
8 | import { sequelizeTypescript } from '../../initializers/database' | 9 | import { AbstractScheduler } from './abstract-scheduler' |
9 | import { MVideoFullLight } from '@server/types/models' | ||
10 | 10 | ||
11 | export class UpdateVideosScheduler extends AbstractScheduler { | 11 | export class UpdateVideosScheduler extends AbstractScheduler { |
12 | 12 | ||
@@ -19,18 +19,19 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
19 | } | 19 | } |
20 | 20 | ||
21 | protected async internalExecute () { | 21 | protected async internalExecute () { |
22 | return retryTransactionWrapper(this.updateVideos.bind(this)) | 22 | return this.updateVideos() |
23 | } | 23 | } |
24 | 24 | ||
25 | private async updateVideos () { | 25 | private async updateVideos () { |
26 | if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined | 26 | if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined |
27 | 27 | ||
28 | const publishedVideos = await sequelizeTypescript.transaction(async t => { | 28 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate() |
29 | const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) | 29 | const publishedVideos: MVideoFullLight[] = [] |
30 | const publishedVideos: MVideoFullLight[] = [] | 30 | |
31 | for (const schedule of schedules) { | ||
32 | await sequelizeTypescript.transaction(async t => { | ||
33 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(schedule.videoId, t) | ||
31 | 34 | ||
32 | for (const schedule of schedules) { | ||
33 | const video = schedule.Video | ||
34 | logger.info('Executing scheduled video update on %s.', video.uuid) | 35 | logger.info('Executing scheduled video update on %s.', video.uuid) |
35 | 36 | ||
36 | if (schedule.privacy) { | 37 | if (schedule.privacy) { |
@@ -42,16 +43,13 @@ export class UpdateVideosScheduler extends AbstractScheduler { | |||
42 | await federateVideoIfNeeded(video, isNewVideo, t) | 43 | await federateVideoIfNeeded(video, isNewVideo, t) |
43 | 44 | ||
44 | if (wasConfidentialVideo) { | 45 | if (wasConfidentialVideo) { |
45 | const videoToPublish: MVideoFullLight = Object.assign(video, { ScheduleVideoUpdate: schedule, UserVideoHistories: [] }) | 46 | publishedVideos.push(video) |
46 | publishedVideos.push(videoToPublish) | ||
47 | } | 47 | } |
48 | } | 48 | } |
49 | 49 | ||
50 | await schedule.destroy({ transaction: t }) | 50 | await schedule.destroy({ transaction: t }) |
51 | } | 51 | }) |
52 | 52 | } | |
53 | return publishedVideos | ||
54 | }) | ||
55 | 53 | ||
56 | for (const v of publishedVideos) { | 54 | for (const v of publishedVideos) { |
57 | Notifier.Instance.notifyOnNewVideoIfNeeded(v) | 55 | Notifier.Instance.notifyOnNewVideoIfNeeded(v) |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index 59b55cccc..b5a5eb697 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -23,7 +23,7 @@ import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../. | |||
23 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 23 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
24 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' | 24 | import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' |
25 | import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' | 25 | import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' |
26 | import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos' | 26 | import { getOrCreateAPVideo } from '../activitypub/videos' |
27 | import { downloadPlaylistSegments } from '../hls' | 27 | import { downloadPlaylistSegments } from '../hls' |
28 | import { removeVideoRedundancy } from '../redundancy' | 28 | import { removeVideoRedundancy } from '../redundancy' |
29 | import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-paths' | 29 | import { generateHLSRedundancyUrl, generateWebTorrentRedundancyUrl } from '../video-paths' |
@@ -351,7 +351,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
351 | syncParam: { likes: false, dislikes: false, shares: false, comments: false, thumbnail: false, refreshVideo: true }, | 351 | syncParam: { likes: false, dislikes: false, shares: false, comments: false, thumbnail: false, refreshVideo: true }, |
352 | fetchType: 'all' as 'all' | 352 | fetchType: 'all' as 'all' |
353 | } | 353 | } |
354 | const { video } = await getOrCreateVideoAndAccountAndChannel(getVideoOptions) | 354 | const { video } = await getOrCreateAPVideo(getVideoOptions) |
355 | 355 | ||
356 | return video | 356 | return video |
357 | } | 357 | } |
diff --git a/server/lib/schedulers/youtube-dl-update-scheduler.ts b/server/lib/schedulers/youtube-dl-update-scheduler.ts index aefe6aba4..898691c13 100644 --- a/server/lib/schedulers/youtube-dl-update-scheduler.ts +++ b/server/lib/schedulers/youtube-dl-update-scheduler.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { AbstractScheduler } from './abstract-scheduler' | 1 | import { YoutubeDL } from '@server/helpers/youtube-dl' |
2 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | 2 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' |
3 | import { updateYoutubeDLBinary } from '../../helpers/youtube-dl' | 3 | import { AbstractScheduler } from './abstract-scheduler' |
4 | 4 | ||
5 | export class YoutubeDlUpdateScheduler extends AbstractScheduler { | 5 | export class YoutubeDlUpdateScheduler extends AbstractScheduler { |
6 | 6 | ||
@@ -13,7 +13,7 @@ export class YoutubeDlUpdateScheduler extends AbstractScheduler { | |||
13 | } | 13 | } |
14 | 14 | ||
15 | protected internalExecute () { | 15 | protected internalExecute () { |
16 | return updateYoutubeDLBinary() | 16 | return YoutubeDL.updateYoutubeDLBinary() |
17 | } | 17 | } |
18 | 18 | ||
19 | static get Instance () { | 19 | static get Instance () { |
diff --git a/server/lib/search.ts b/server/lib/search.ts new file mode 100644 index 000000000..b643a4055 --- /dev/null +++ b/server/lib/search.ts | |||
@@ -0,0 +1,50 @@ | |||
1 | import * as express from 'express' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | ||
4 | import { getServerActor } from '@server/models/application/application' | ||
5 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
6 | import { SearchTargetQuery } from '@shared/models' | ||
7 | |||
8 | function isSearchIndexSearch (query: SearchTargetQuery) { | ||
9 | if (query.searchTarget === 'search-index') return true | ||
10 | |||
11 | const searchIndexConfig = CONFIG.SEARCH.SEARCH_INDEX | ||
12 | |||
13 | if (searchIndexConfig.ENABLED !== true) return false | ||
14 | |||
15 | if (searchIndexConfig.DISABLE_LOCAL_SEARCH) return true | ||
16 | if (searchIndexConfig.IS_DEFAULT_SEARCH && !query.searchTarget) return true | ||
17 | |||
18 | return false | ||
19 | } | ||
20 | |||
21 | async function buildMutedForSearchIndex (res: express.Response) { | ||
22 | const serverActor = await getServerActor() | ||
23 | const accountIds = [ serverActor.Account.id ] | ||
24 | |||
25 | if (res.locals.oauth) { | ||
26 | accountIds.push(res.locals.oauth.token.User.Account.id) | ||
27 | } | ||
28 | |||
29 | const [ blockedHosts, blockedAccounts ] = await Promise.all([ | ||
30 | ServerBlocklistModel.listHostsBlockedBy(accountIds), | ||
31 | AccountBlocklistModel.listHandlesBlockedBy(accountIds) | ||
32 | ]) | ||
33 | |||
34 | return { | ||
35 | blockedHosts, | ||
36 | blockedAccounts | ||
37 | } | ||
38 | } | ||
39 | |||
40 | function isURISearch (search: string) { | ||
41 | if (!search) return false | ||
42 | |||
43 | return search.startsWith('http://') || search.startsWith('https://') | ||
44 | } | ||
45 | |||
46 | export { | ||
47 | isSearchIndexSearch, | ||
48 | buildMutedForSearchIndex, | ||
49 | isURISearch | ||
50 | } | ||
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts new file mode 100644 index 000000000..80d87a9d3 --- /dev/null +++ b/server/lib/server-config-manager.ts | |||
@@ -0,0 +1,304 @@ | |||
1 | import { getServerCommit } from '@server/helpers/utils' | ||
2 | import { CONFIG, isEmailEnabled } from '@server/initializers/config' | ||
3 | import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants' | ||
4 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '@server/lib/signup' | ||
5 | import { ActorCustomPageModel } from '@server/models/account/actor-custom-page' | ||
6 | import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models' | ||
7 | import { Hooks } from './plugins/hooks' | ||
8 | import { PluginManager } from './plugins/plugin-manager' | ||
9 | import { getThemeOrDefault } from './plugins/theme-utils' | ||
10 | import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles' | ||
11 | |||
12 | /** | ||
13 | * | ||
14 | * Used to send the server config to clients (using REST/API or plugins API) | ||
15 | * We need a singleton class to manage config state depending on external events (to build menu entries etc) | ||
16 | * | ||
17 | */ | ||
18 | |||
19 | class ServerConfigManager { | ||
20 | |||
21 | private static instance: ServerConfigManager | ||
22 | |||
23 | private serverCommit: string | ||
24 | |||
25 | private homepageEnabled = false | ||
26 | |||
27 | private constructor () {} | ||
28 | |||
29 | async init () { | ||
30 | const instanceHomepage = await ActorCustomPageModel.loadInstanceHomepage() | ||
31 | |||
32 | this.updateHomepageState(instanceHomepage?.content) | ||
33 | } | ||
34 | |||
35 | updateHomepageState (content: string) { | ||
36 | this.homepageEnabled = !!content | ||
37 | } | ||
38 | |||
39 | async getHTMLServerConfig (): Promise<HTMLServerConfig> { | ||
40 | if (this.serverCommit === undefined) this.serverCommit = await getServerCommit() | ||
41 | |||
42 | const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) | ||
43 | |||
44 | return { | ||
45 | instance: { | ||
46 | name: CONFIG.INSTANCE.NAME, | ||
47 | shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, | ||
48 | isNSFW: CONFIG.INSTANCE.IS_NSFW, | ||
49 | defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
50 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, | ||
51 | customizations: { | ||
52 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, | ||
53 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS | ||
54 | } | ||
55 | }, | ||
56 | search: { | ||
57 | remoteUri: { | ||
58 | users: CONFIG.SEARCH.REMOTE_URI.USERS, | ||
59 | anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS | ||
60 | }, | ||
61 | searchIndex: { | ||
62 | enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED, | ||
63 | url: CONFIG.SEARCH.SEARCH_INDEX.URL, | ||
64 | disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH, | ||
65 | isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH | ||
66 | } | ||
67 | }, | ||
68 | plugin: { | ||
69 | registered: this.getRegisteredPlugins(), | ||
70 | registeredExternalAuths: this.getExternalAuthsPlugins(), | ||
71 | registeredIdAndPassAuths: this.getIdAndPassAuthPlugins() | ||
72 | }, | ||
73 | theme: { | ||
74 | registered: this.getRegisteredThemes(), | ||
75 | default: defaultTheme | ||
76 | }, | ||
77 | email: { | ||
78 | enabled: isEmailEnabled() | ||
79 | }, | ||
80 | contactForm: { | ||
81 | enabled: CONFIG.CONTACT_FORM.ENABLED | ||
82 | }, | ||
83 | serverVersion: PEERTUBE_VERSION, | ||
84 | serverCommit: this.serverCommit, | ||
85 | transcoding: { | ||
86 | hls: { | ||
87 | enabled: CONFIG.TRANSCODING.HLS.ENABLED | ||
88 | }, | ||
89 | webtorrent: { | ||
90 | enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED | ||
91 | }, | ||
92 | enabledResolutions: this.getEnabledResolutions('vod'), | ||
93 | profile: CONFIG.TRANSCODING.PROFILE, | ||
94 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('vod') | ||
95 | }, | ||
96 | live: { | ||
97 | enabled: CONFIG.LIVE.ENABLED, | ||
98 | |||
99 | allowReplay: CONFIG.LIVE.ALLOW_REPLAY, | ||
100 | maxDuration: CONFIG.LIVE.MAX_DURATION, | ||
101 | maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, | ||
102 | maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, | ||
103 | |||
104 | transcoding: { | ||
105 | enabled: CONFIG.LIVE.TRANSCODING.ENABLED, | ||
106 | enabledResolutions: this.getEnabledResolutions('live'), | ||
107 | profile: CONFIG.LIVE.TRANSCODING.PROFILE, | ||
108 | availableProfiles: VideoTranscodingProfilesManager.Instance.getAvailableProfiles('live') | ||
109 | }, | ||
110 | |||
111 | rtmp: { | ||
112 | port: CONFIG.LIVE.RTMP.PORT | ||
113 | } | ||
114 | }, | ||
115 | import: { | ||
116 | videos: { | ||
117 | http: { | ||
118 | enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED | ||
119 | }, | ||
120 | torrent: { | ||
121 | enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED | ||
122 | } | ||
123 | } | ||
124 | }, | ||
125 | autoBlacklist: { | ||
126 | videos: { | ||
127 | ofUsers: { | ||
128 | enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED | ||
129 | } | ||
130 | } | ||
131 | }, | ||
132 | avatar: { | ||
133 | file: { | ||
134 | size: { | ||
135 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
136 | }, | ||
137 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
138 | } | ||
139 | }, | ||
140 | banner: { | ||
141 | file: { | ||
142 | size: { | ||
143 | max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max | ||
144 | }, | ||
145 | extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME | ||
146 | } | ||
147 | }, | ||
148 | video: { | ||
149 | image: { | ||
150 | extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, | ||
151 | size: { | ||
152 | max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max | ||
153 | } | ||
154 | }, | ||
155 | file: { | ||
156 | extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME | ||
157 | } | ||
158 | }, | ||
159 | videoCaption: { | ||
160 | file: { | ||
161 | size: { | ||
162 | max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max | ||
163 | }, | ||
164 | extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME | ||
165 | } | ||
166 | }, | ||
167 | user: { | ||
168 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
169 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | ||
170 | }, | ||
171 | trending: { | ||
172 | videos: { | ||
173 | intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS, | ||
174 | algorithms: { | ||
175 | enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED, | ||
176 | default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT | ||
177 | } | ||
178 | } | ||
179 | }, | ||
180 | tracker: { | ||
181 | enabled: CONFIG.TRACKER.ENABLED | ||
182 | }, | ||
183 | |||
184 | followings: { | ||
185 | instance: { | ||
186 | autoFollowIndex: { | ||
187 | indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL | ||
188 | } | ||
189 | } | ||
190 | }, | ||
191 | |||
192 | broadcastMessage: { | ||
193 | enabled: CONFIG.BROADCAST_MESSAGE.ENABLED, | ||
194 | message: CONFIG.BROADCAST_MESSAGE.MESSAGE, | ||
195 | level: CONFIG.BROADCAST_MESSAGE.LEVEL, | ||
196 | dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE | ||
197 | }, | ||
198 | |||
199 | homepage: { | ||
200 | enabled: this.homepageEnabled | ||
201 | } | ||
202 | } | ||
203 | } | ||
204 | |||
205 | async getServerConfig (ip?: string): Promise<ServerConfig> { | ||
206 | const { allowed } = await Hooks.wrapPromiseFun( | ||
207 | isSignupAllowed, | ||
208 | { | ||
209 | ip | ||
210 | }, | ||
211 | 'filter:api.user.signup.allowed.result' | ||
212 | ) | ||
213 | |||
214 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) | ||
215 | |||
216 | const signup = { | ||
217 | allowed, | ||
218 | allowedForCurrentIP, | ||
219 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE, | ||
220 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | ||
221 | } | ||
222 | |||
223 | const htmlConfig = await this.getHTMLServerConfig() | ||
224 | |||
225 | return { ...htmlConfig, signup } | ||
226 | } | ||
227 | |||
228 | getRegisteredThemes () { | ||
229 | return PluginManager.Instance.getRegisteredThemes() | ||
230 | .map(t => ({ | ||
231 | name: t.name, | ||
232 | version: t.version, | ||
233 | description: t.description, | ||
234 | css: t.css, | ||
235 | clientScripts: t.clientScripts | ||
236 | })) | ||
237 | } | ||
238 | |||
239 | getRegisteredPlugins () { | ||
240 | return PluginManager.Instance.getRegisteredPlugins() | ||
241 | .map(p => ({ | ||
242 | name: p.name, | ||
243 | version: p.version, | ||
244 | description: p.description, | ||
245 | clientScripts: p.clientScripts | ||
246 | })) | ||
247 | } | ||
248 | |||
249 | getEnabledResolutions (type: 'vod' | 'live') { | ||
250 | const transcoding = type === 'vod' | ||
251 | ? CONFIG.TRANSCODING | ||
252 | : CONFIG.LIVE.TRANSCODING | ||
253 | |||
254 | return Object.keys(transcoding.RESOLUTIONS) | ||
255 | .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true) | ||
256 | .map(r => parseInt(r, 10)) | ||
257 | } | ||
258 | |||
259 | private getIdAndPassAuthPlugins () { | ||
260 | const result: RegisteredIdAndPassAuthConfig[] = [] | ||
261 | |||
262 | for (const p of PluginManager.Instance.getIdAndPassAuths()) { | ||
263 | for (const auth of p.idAndPassAuths) { | ||
264 | result.push({ | ||
265 | npmName: p.npmName, | ||
266 | name: p.name, | ||
267 | version: p.version, | ||
268 | authName: auth.authName, | ||
269 | weight: auth.getWeight() | ||
270 | }) | ||
271 | } | ||
272 | } | ||
273 | |||
274 | return result | ||
275 | } | ||
276 | |||
277 | private getExternalAuthsPlugins () { | ||
278 | const result: RegisteredExternalAuthConfig[] = [] | ||
279 | |||
280 | for (const p of PluginManager.Instance.getExternalAuths()) { | ||
281 | for (const auth of p.externalAuths) { | ||
282 | result.push({ | ||
283 | npmName: p.npmName, | ||
284 | name: p.name, | ||
285 | version: p.version, | ||
286 | authName: auth.authName, | ||
287 | authDisplayName: auth.authDisplayName() | ||
288 | }) | ||
289 | } | ||
290 | } | ||
291 | |||
292 | return result | ||
293 | } | ||
294 | |||
295 | static get Instance () { | ||
296 | return this.instance || (this.instance = new this()) | ||
297 | } | ||
298 | } | ||
299 | |||
300 | // --------------------------------------------------------------------------- | ||
301 | |||
302 | export { | ||
303 | ServerConfigManager | ||
304 | } | ||
diff --git a/server/helpers/signup.ts b/server/lib/signup.ts index ed872539b..8fa81e601 100644 --- a/server/helpers/signup.ts +++ b/server/lib/signup.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { UserModel } from '../models/account/user' | 1 | import { UserModel } from '../models/user/user' |
2 | import * as ipaddr from 'ipaddr.js' | 2 | import * as ipaddr from 'ipaddr.js' |
3 | import { CONFIG } from '../initializers/config' | 3 | import { CONFIG } from '../initializers/config' |
4 | 4 | ||
diff --git a/server/lib/stat-manager.ts b/server/lib/stat-manager.ts index 5d703f610..3c5e0a93e 100644 --- a/server/lib/stat-manager.ts +++ b/server/lib/stat-manager.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { CONFIG } from '@server/initializers/config' | 1 | import { CONFIG } from '@server/initializers/config' |
2 | import { UserModel } from '@server/models/account/user' | 2 | import { UserModel } from '@server/models/user/user' |
3 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
4 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' | 4 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' |
5 | import { VideoModel } from '@server/models/video/video' | 5 | import { VideoModel } from '@server/models/video/video' |
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | 6 | import { VideoChannelModel } from '@server/models/video/video-channel' |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index cfee69cfc..c08523988 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -14,7 +14,7 @@ import { getVideoFilePath } from './video-paths' | |||
14 | 14 | ||
15 | type ImageSize = { height?: number, width?: number } | 15 | type ImageSize = { height?: number, width?: number } |
16 | 16 | ||
17 | function createPlaylistMiniatureFromExisting (options: { | 17 | function updatePlaylistMiniatureFromExisting (options: { |
18 | inputPath: string | 18 | inputPath: string |
19 | playlist: MVideoPlaylistThumbnail | 19 | playlist: MVideoPlaylistThumbnail |
20 | automaticallyGenerated: boolean | 20 | automaticallyGenerated: boolean |
@@ -26,7 +26,7 @@ function createPlaylistMiniatureFromExisting (options: { | |||
26 | const type = ThumbnailType.MINIATURE | 26 | const type = ThumbnailType.MINIATURE |
27 | 27 | ||
28 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) | 28 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) |
29 | return createThumbnailFromFunction({ | 29 | return updateThumbnailFromFunction({ |
30 | thumbnailCreator, | 30 | thumbnailCreator, |
31 | filename, | 31 | filename, |
32 | height, | 32 | height, |
@@ -37,7 +37,7 @@ function createPlaylistMiniatureFromExisting (options: { | |||
37 | }) | 37 | }) |
38 | } | 38 | } |
39 | 39 | ||
40 | function createPlaylistMiniatureFromUrl (options: { | 40 | function updatePlaylistMiniatureFromUrl (options: { |
41 | downloadUrl: string | 41 | downloadUrl: string |
42 | playlist: MVideoPlaylistThumbnail | 42 | playlist: MVideoPlaylistThumbnail |
43 | size?: ImageSize | 43 | size?: ImageSize |
@@ -52,10 +52,10 @@ function createPlaylistMiniatureFromUrl (options: { | |||
52 | : downloadUrl | 52 | : downloadUrl |
53 | 53 | ||
54 | const thumbnailCreator = () => downloadImage(downloadUrl, basePath, filename, { width, height }) | 54 | const thumbnailCreator = () => downloadImage(downloadUrl, basePath, filename, { width, height }) |
55 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | 55 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) |
56 | } | 56 | } |
57 | 57 | ||
58 | function createVideoMiniatureFromUrl (options: { | 58 | function updateVideoMiniatureFromUrl (options: { |
59 | downloadUrl: string | 59 | downloadUrl: string |
60 | video: MVideoThumbnail | 60 | video: MVideoThumbnail |
61 | type: ThumbnailType | 61 | type: ThumbnailType |
@@ -82,10 +82,10 @@ function createVideoMiniatureFromUrl (options: { | |||
82 | return Promise.resolve() | 82 | return Promise.resolve() |
83 | } | 83 | } |
84 | 84 | ||
85 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) | 85 | return updateThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) |
86 | } | 86 | } |
87 | 87 | ||
88 | function createVideoMiniatureFromExisting (options: { | 88 | function updateVideoMiniatureFromExisting (options: { |
89 | inputPath: string | 89 | inputPath: string |
90 | video: MVideoThumbnail | 90 | video: MVideoThumbnail |
91 | type: ThumbnailType | 91 | type: ThumbnailType |
@@ -98,7 +98,7 @@ function createVideoMiniatureFromExisting (options: { | |||
98 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | 98 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) |
99 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) | 99 | const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal) |
100 | 100 | ||
101 | return createThumbnailFromFunction({ | 101 | return updateThumbnailFromFunction({ |
102 | thumbnailCreator, | 102 | thumbnailCreator, |
103 | filename, | 103 | filename, |
104 | height, | 104 | height, |
@@ -123,7 +123,7 @@ function generateVideoMiniature (options: { | |||
123 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) | 123 | ? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true) |
124 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | 124 | : () => generateImageFromVideoFile(input, basePath, filename, { height, width }) |
125 | 125 | ||
126 | return createThumbnailFromFunction({ | 126 | return updateThumbnailFromFunction({ |
127 | thumbnailCreator, | 127 | thumbnailCreator, |
128 | filename, | 128 | filename, |
129 | height, | 129 | height, |
@@ -134,7 +134,7 @@ function generateVideoMiniature (options: { | |||
134 | }) | 134 | }) |
135 | } | 135 | } |
136 | 136 | ||
137 | function createPlaceholderThumbnail (options: { | 137 | function updatePlaceholderThumbnail (options: { |
138 | fileUrl: string | 138 | fileUrl: string |
139 | video: MVideoThumbnail | 139 | video: MVideoThumbnail |
140 | type: ThumbnailType | 140 | type: ThumbnailType |
@@ -165,11 +165,11 @@ function createPlaceholderThumbnail (options: { | |||
165 | 165 | ||
166 | export { | 166 | export { |
167 | generateVideoMiniature, | 167 | generateVideoMiniature, |
168 | createVideoMiniatureFromUrl, | 168 | updateVideoMiniatureFromUrl, |
169 | createVideoMiniatureFromExisting, | 169 | updateVideoMiniatureFromExisting, |
170 | createPlaceholderThumbnail, | 170 | updatePlaceholderThumbnail, |
171 | createPlaylistMiniatureFromUrl, | 171 | updatePlaylistMiniatureFromUrl, |
172 | createPlaylistMiniatureFromExisting | 172 | updatePlaylistMiniatureFromExisting |
173 | } | 173 | } |
174 | 174 | ||
175 | function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) { | 175 | function hasThumbnailUrlChanged (existingThumbnail: MThumbnail, downloadUrl: string, video: MVideoUUID) { |
@@ -231,7 +231,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si | |||
231 | return undefined | 231 | return undefined |
232 | } | 232 | } |
233 | 233 | ||
234 | async function createThumbnailFromFunction (parameters: { | 234 | async function updateThumbnailFromFunction (parameters: { |
235 | thumbnailCreator: () => Promise<any> | 235 | thumbnailCreator: () => Promise<any> |
236 | filename: string | 236 | filename: string |
237 | height: number | 237 | height: number |
diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/transcoding/video-transcoding-profiles.ts index 81f5e1962..c5ea72a5f 100644 --- a/server/lib/video-transcoding-profiles.ts +++ b/server/lib/transcoding/video-transcoding-profiles.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { logger } from '@server/helpers/logger' | 1 | import { logger } from '@server/helpers/logger' |
2 | import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos' | 2 | import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos' |
3 | import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils' | 3 | import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils' |
4 | import { | 4 | import { |
5 | canDoQuickAudioTranscode, | 5 | canDoQuickAudioTranscode, |
6 | ffprobePromise, | 6 | ffprobePromise, |
@@ -8,8 +8,8 @@ import { | |||
8 | getMaxAudioBitrate, | 8 | getMaxAudioBitrate, |
9 | getVideoFileBitrate, | 9 | getVideoFileBitrate, |
10 | getVideoStreamFromFile | 10 | getVideoStreamFromFile |
11 | } from '../helpers/ffprobe-utils' | 11 | } from '../../helpers/ffprobe-utils' |
12 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 12 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' |
13 | 13 | ||
14 | /** | 14 | /** |
15 | * | 15 | * |
diff --git a/server/lib/video-transcoding.ts b/server/lib/transcoding/video-transcoding.ts index c949dca2e..1ad63baf3 100644 --- a/server/lib/video-transcoding.ts +++ b/server/lib/transcoding/video-transcoding.ts | |||
@@ -1,19 +1,20 @@ | |||
1 | import { Job } from 'bull' | 1 | import { Job } from 'bull' |
2 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' | 2 | import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' |
3 | import { basename, extname as extnameUtil, join } from 'path' | 3 | import { basename, extname as extnameUtil, join } from 'path' |
4 | import { toEven } from '@server/helpers/core-utils' | ||
4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
5 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 6 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
6 | import { VideoResolution } from '../../shared/models/videos' | 7 | import { VideoResolution } from '../../../shared/models/videos' |
7 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | 8 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
8 | import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' | 9 | import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils' |
9 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils' | 10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' |
10 | import { logger } from '../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
11 | import { CONFIG } from '../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
12 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' | 13 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants' |
13 | import { VideoFileModel } from '../models/video/video-file' | 14 | import { VideoFileModel } from '../../models/video/video-file' |
14 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' |
15 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls' | 16 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' |
16 | import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from './video-paths' | 17 | import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from '../video-paths' |
17 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | 18 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' |
18 | 19 | ||
19 | /** | 20 | /** |
@@ -35,6 +36,8 @@ async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile | |||
35 | ? 'quick-transcode' | 36 | ? 'quick-transcode' |
36 | : 'video' | 37 | : 'video' |
37 | 38 | ||
39 | const resolution = toEven(inputVideoFile.resolution) | ||
40 | |||
38 | const transcodeOptions: TranscodeOptions = { | 41 | const transcodeOptions: TranscodeOptions = { |
39 | type: transcodeType, | 42 | type: transcodeType, |
40 | 43 | ||
@@ -44,7 +47,7 @@ async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile | |||
44 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 47 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
45 | profile: CONFIG.TRANSCODING.PROFILE, | 48 | profile: CONFIG.TRANSCODING.PROFILE, |
46 | 49 | ||
47 | resolution: inputVideoFile.resolution, | 50 | resolution, |
48 | 51 | ||
49 | job | 52 | job |
50 | } | 53 | } |
@@ -57,7 +60,7 @@ async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile | |||
57 | 60 | ||
58 | // Important to do this before getVideoFilename() to take in account the new filename | 61 | // Important to do this before getVideoFilename() to take in account the new filename |
59 | inputVideoFile.extname = newExtname | 62 | inputVideoFile.extname = newExtname |
60 | inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname) | 63 | inputVideoFile.filename = generateVideoFilename(video, false, resolution, newExtname) |
61 | 64 | ||
62 | const videoOutputPath = getVideoFilePath(video, inputVideoFile) | 65 | const videoOutputPath = getVideoFilePath(video, inputVideoFile) |
63 | 66 | ||
@@ -215,16 +218,6 @@ function generateHlsPlaylistResolution (options: { | |||
215 | }) | 218 | }) |
216 | } | 219 | } |
217 | 220 | ||
218 | function getEnabledResolutions (type: 'vod' | 'live') { | ||
219 | const transcoding = type === 'vod' | ||
220 | ? CONFIG.TRANSCODING | ||
221 | : CONFIG.LIVE.TRANSCODING | ||
222 | |||
223 | return Object.keys(transcoding.RESOLUTIONS) | ||
224 | .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true) | ||
225 | .map(r => parseInt(r, 10)) | ||
226 | } | ||
227 | |||
228 | // --------------------------------------------------------------------------- | 221 | // --------------------------------------------------------------------------- |
229 | 222 | ||
230 | export { | 223 | export { |
@@ -232,8 +225,7 @@ export { | |||
232 | generateHlsPlaylistResolutionFromTS, | 225 | generateHlsPlaylistResolutionFromTS, |
233 | optimizeOriginalVideofile, | 226 | optimizeOriginalVideofile, |
234 | transcodeNewWebTorrentResolution, | 227 | transcodeNewWebTorrentResolution, |
235 | mergeAudioVideofile, | 228 | mergeAudioVideofile |
236 | getEnabledResolutions | ||
237 | } | 229 | } |
238 | 230 | ||
239 | // --------------------------------------------------------------------------- | 231 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/user.ts b/server/lib/user.ts index 9b0a0a2f1..936403692 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -1,19 +1,21 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { Transaction } from 'sequelize/types' |
2 | import { v4 as uuidv4 } from 'uuid' | 2 | import { buildUUID } from '@server/helpers/uuid' |
3 | import { UserModel } from '@server/models/account/user' | 3 | import { UserModel } from '@server/models/user/user' |
4 | import { MActorDefault } from '@server/types/models/actor' | ||
4 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 5 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
5 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' | 6 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' |
6 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' | 7 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' |
7 | import { sequelizeTypescript } from '../initializers/database' | 8 | import { sequelizeTypescript } from '../initializers/database' |
8 | import { AccountModel } from '../models/account/account' | 9 | import { AccountModel } from '../models/account/account' |
9 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | 10 | import { ActorModel } from '../models/actor/actor' |
10 | import { ActorModel } from '../models/activitypub/actor' | 11 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' |
11 | import { MAccountDefault, MActorDefault, MChannelActor } from '../types/models' | 12 | import { MAccountDefault, MChannelActor } from '../types/models' |
12 | import { MUser, MUserDefault, MUserId } from '../types/models/user' | 13 | import { MUser, MUserDefault, MUserId } from '../types/models/user' |
13 | import { buildActorInstance, generateAndSaveActorKeys } from './activitypub/actor' | 14 | import { generateAndSaveActorKeys } from './activitypub/actors' |
14 | import { getLocalAccountActivityPubUrl } from './activitypub/url' | 15 | import { getLocalAccountActivityPubUrl } from './activitypub/url' |
15 | import { Emailer } from './emailer' | 16 | import { Emailer } from './emailer' |
16 | import { LiveManager } from './live-manager' | 17 | import { LiveQuotaStore } from './live/live-quota-store' |
18 | import { buildActorInstance } from './local-actor' | ||
17 | import { Redis } from './redis' | 19 | import { Redis } from './redis' |
18 | import { createLocalVideoChannel } from './video-channel' | 20 | import { createLocalVideoChannel } from './video-channel' |
19 | import { createWatchLaterPlaylist } from './video-playlist' | 21 | import { createWatchLaterPlaylist } from './video-playlist' |
@@ -42,11 +44,11 @@ async function createUserAccountAndChannelAndPlaylist (parameters: { | |||
42 | displayName: userDisplayName, | 44 | displayName: userDisplayName, |
43 | userId: userCreated.id, | 45 | userId: userCreated.id, |
44 | applicationId: null, | 46 | applicationId: null, |
45 | t: t | 47 | t |
46 | }) | 48 | }) |
47 | userCreated.Account = accountCreated | 49 | userCreated.Account = accountCreated |
48 | 50 | ||
49 | const channelAttributes = await buildChannelAttributes(userCreated, channelNames) | 51 | const channelAttributes = await buildChannelAttributes(userCreated, t, channelNames) |
50 | const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t) | 52 | const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t) |
51 | 53 | ||
52 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) | 54 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) |
@@ -127,7 +129,7 @@ async function getOriginalVideoFileTotalFromUser (user: MUserId) { | |||
127 | 129 | ||
128 | const base = await UserModel.getTotalRawQuery(query, user.id) | 130 | const base = await UserModel.getTotalRawQuery(query, user.id) |
129 | 131 | ||
130 | return base + LiveManager.Instance.getLiveQuotaUsedByUser(user.id) | 132 | return base + LiveQuotaStore.Instance.getLiveQuotaOf(user.id) |
131 | } | 133 | } |
132 | 134 | ||
133 | // Returns cumulative size of all video files uploaded in the last 24 hours. | 135 | // Returns cumulative size of all video files uploaded in the last 24 hours. |
@@ -141,10 +143,10 @@ async function getOriginalVideoFileTotalDailyFromUser (user: MUserId) { | |||
141 | 143 | ||
142 | const base = await UserModel.getTotalRawQuery(query, user.id) | 144 | const base = await UserModel.getTotalRawQuery(query, user.id) |
143 | 145 | ||
144 | return base + LiveManager.Instance.getLiveQuotaUsedByUser(user.id) | 146 | return base + LiveQuotaStore.Instance.getLiveQuotaOf(user.id) |
145 | } | 147 | } |
146 | 148 | ||
147 | async function isAbleToUploadVideo (userId: number, size: number) { | 149 | async function isAbleToUploadVideo (userId: number, newVideoSize: number) { |
148 | const user = await UserModel.loadById(userId) | 150 | const user = await UserModel.loadById(userId) |
149 | 151 | ||
150 | if (user.videoQuota === -1 && user.videoQuotaDaily === -1) return Promise.resolve(true) | 152 | if (user.videoQuota === -1 && user.videoQuotaDaily === -1) return Promise.resolve(true) |
@@ -154,8 +156,8 @@ async function isAbleToUploadVideo (userId: number, size: number) { | |||
154 | getOriginalVideoFileTotalDailyFromUser(user) | 156 | getOriginalVideoFileTotalDailyFromUser(user) |
155 | ]) | 157 | ]) |
156 | 158 | ||
157 | const uploadedTotal = size + totalBytes | 159 | const uploadedTotal = newVideoSize + totalBytes |
158 | const uploadedDaily = size + totalBytesDaily | 160 | const uploadedDaily = newVideoSize + totalBytesDaily |
159 | 161 | ||
160 | if (user.videoQuotaDaily === -1) return uploadedTotal < user.videoQuota | 162 | if (user.videoQuotaDaily === -1) return uploadedTotal < user.videoQuota |
161 | if (user.videoQuota === -1) return uploadedDaily < user.videoQuotaDaily | 163 | if (user.videoQuota === -1) return uploadedDaily < user.videoQuotaDaily |
@@ -201,14 +203,14 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
201 | return UserNotificationSettingModel.create(values, { transaction: t }) | 203 | return UserNotificationSettingModel.create(values, { transaction: t }) |
202 | } | 204 | } |
203 | 205 | ||
204 | async function buildChannelAttributes (user: MUser, channelNames?: ChannelNames) { | 206 | async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) { |
205 | if (channelNames) return channelNames | 207 | if (channelNames) return channelNames |
206 | 208 | ||
207 | let channelName = user.username + '_channel' | 209 | let channelName = user.username + '_channel' |
208 | 210 | ||
209 | // Conflict, generate uuid instead | 211 | // Conflict, generate uuid instead |
210 | const actor = await ActorModel.loadLocalByName(channelName) | 212 | const actor = await ActorModel.loadLocalByName(channelName, transaction) |
211 | if (actor) channelName = uuidv4() | 213 | if (actor) channelName = buildUUID() |
212 | 214 | ||
213 | const videoChannelDisplayName = `Main ${user.username} channel` | 215 | const videoChannelDisplayName = `Main ${user.username} channel` |
214 | 216 | ||
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts index 37c43c3b0..0984c0d7a 100644 --- a/server/lib/video-blacklist.ts +++ b/server/lib/video-blacklist.ts | |||
@@ -16,7 +16,7 @@ import { CONFIG } from '../initializers/config' | |||
16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | 16 | import { VideoBlacklistModel } from '../models/video/video-blacklist' |
17 | import { sendDeleteVideo } from './activitypub/send' | 17 | import { sendDeleteVideo } from './activitypub/send' |
18 | import { federateVideoIfNeeded } from './activitypub/videos' | 18 | import { federateVideoIfNeeded } from './activitypub/videos' |
19 | import { LiveManager } from './live-manager' | 19 | import { LiveManager } from './live/live-manager' |
20 | import { Notifier } from './notifier' | 20 | import { Notifier } from './notifier' |
21 | import { Hooks } from './plugins/hooks' | 21 | import { Hooks } from './plugins/hooks' |
22 | 22 | ||
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 0476cb2d5..2fd63a8c4 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts | |||
@@ -1,17 +1,15 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { v4 as uuidv4 } from 'uuid' | ||
3 | import { VideoChannelCreate } from '../../shared/models' | 2 | import { VideoChannelCreate } from '../../shared/models' |
4 | import { VideoModel } from '../models/video/video' | 3 | import { VideoModel } from '../models/video/video' |
5 | import { VideoChannelModel } from '../models/video/video-channel' | 4 | import { VideoChannelModel } from '../models/video/video-channel' |
6 | import { MAccountId, MChannelId } from '../types/models' | 5 | import { MAccountId, MChannelId } from '../types/models' |
7 | import { buildActorInstance } from './activitypub/actor' | ||
8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' | 6 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' |
9 | import { federateVideoIfNeeded } from './activitypub/videos' | 7 | import { federateVideoIfNeeded } from './activitypub/videos' |
8 | import { buildActorInstance } from './local-actor' | ||
10 | 9 | ||
11 | async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) { | 10 | async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) { |
12 | const uuid = uuidv4() | ||
13 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) | 11 | const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) |
14 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) | 12 | const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name) |
15 | 13 | ||
16 | const actorInstanceCreated = await actorInstance.save({ transaction: t }) | 14 | const actorInstanceCreated = await actorInstance.save({ transaction: t }) |
17 | 15 | ||
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index 736ebb2f8..c76570a5d 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts | |||
@@ -3,7 +3,7 @@ import * as Sequelize from 'sequelize' | |||
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | import { sequelizeTypescript } from '@server/initializers/database' | 4 | import { sequelizeTypescript } from '@server/initializers/database' |
5 | import { ResultList } from '../../shared/models' | 5 | import { ResultList } from '../../shared/models' |
6 | import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' | 6 | import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model' |
7 | import { VideoCommentModel } from '../models/video/video-comment' | 7 | import { VideoCommentModel } from '../models/video/video-comment' |
8 | import { MAccountDefault, MComment, MCommentOwnerVideo, MCommentOwnerVideoReply, MVideoFullLight } from '../types/models' | 8 | import { MAccountDefault, MComment, MCommentOwnerVideo, MCommentOwnerVideoReply, MVideoFullLight } from '../types/models' |
9 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' | 9 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' |
@@ -18,9 +18,9 @@ async function removeComment (videoCommentInstance: MCommentOwnerVideo) { | |||
18 | await sendDeleteVideoComment(videoCommentInstance, t) | 18 | await sendDeleteVideoComment(videoCommentInstance, t) |
19 | } | 19 | } |
20 | 20 | ||
21 | markCommentAsDeleted(videoCommentInstance) | 21 | videoCommentInstance.markAsDeleted() |
22 | 22 | ||
23 | await videoCommentInstance.save() | 23 | await videoCommentInstance.save({ transaction: t }) |
24 | }) | 24 | }) |
25 | 25 | ||
26 | logger.info('Video comment %d deleted.', videoCommentInstance.id) | 26 | logger.info('Video comment %d deleted.', videoCommentInstance.id) |
@@ -95,17 +95,10 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): | |||
95 | return thread | 95 | return thread |
96 | } | 96 | } |
97 | 97 | ||
98 | function markCommentAsDeleted (comment: MComment): void { | ||
99 | comment.text = '' | ||
100 | comment.deletedAt = new Date() | ||
101 | comment.accountId = null | ||
102 | } | ||
103 | |||
104 | // --------------------------------------------------------------------------- | 98 | // --------------------------------------------------------------------------- |
105 | 99 | ||
106 | export { | 100 | export { |
107 | removeComment, | 101 | removeComment, |
108 | createVideoComment, | 102 | createVideoComment, |
109 | buildFormattedCommentTree, | 103 | buildFormattedCommentTree |
110 | markCommentAsDeleted | ||
111 | } | 104 | } |
diff --git a/server/lib/video.ts b/server/lib/video.ts index 21e4b7ff2..daf998704 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -10,7 +10,7 @@ import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } fro | |||
10 | import { federateVideoIfNeeded } from './activitypub/videos' | 10 | import { federateVideoIfNeeded } from './activitypub/videos' |
11 | import { JobQueue } from './job-queue/job-queue' | 11 | import { JobQueue } from './job-queue/job-queue' |
12 | import { Notifier } from './notifier' | 12 | import { Notifier } from './notifier' |
13 | import { createVideoMiniatureFromExisting } from './thumbnail' | 13 | import { updateVideoMiniatureFromExisting } from './thumbnail' |
14 | 14 | ||
15 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { | 15 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { |
16 | return { | 16 | return { |
@@ -28,6 +28,8 @@ function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): Fil | |||
28 | privacy: videoInfo.privacy || VideoPrivacy.PRIVATE, | 28 | privacy: videoInfo.privacy || VideoPrivacy.PRIVATE, |
29 | channelId: channelId, | 29 | channelId: channelId, |
30 | originallyPublishedAt: videoInfo.originallyPublishedAt | 30 | originallyPublishedAt: videoInfo.originallyPublishedAt |
31 | ? new Date(videoInfo.originallyPublishedAt) | ||
32 | : null | ||
31 | } | 33 | } |
32 | } | 34 | } |
33 | 35 | ||
@@ -52,7 +54,7 @@ async function buildVideoThumbnailsFromReq (options: { | |||
52 | const fields = files?.[p.fieldName] | 54 | const fields = files?.[p.fieldName] |
53 | 55 | ||
54 | if (fields) { | 56 | if (fields) { |
55 | return createVideoMiniatureFromExisting({ | 57 | return updateVideoMiniatureFromExisting({ |
56 | inputPath: fields[0].path, | 58 | inputPath: fields[0].path, |
57 | video, | 59 | video, |
58 | type: p.type, | 60 | type: p.type, |
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts index ce94a2129..6b43b7764 100644 --- a/server/middlewares/activitypub.ts +++ b/server/middlewares/activitypub.ts | |||
@@ -1,13 +1,12 @@ | |||
1 | import { NextFunction, Request, Response } from 'express' | 1 | import { NextFunction, Request, Response } from 'express' |
2 | import { getAPId } from '@server/helpers/activitypub' | ||
3 | import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor' | ||
2 | import { ActivityDelete, ActivityPubSignature } from '../../shared' | 4 | import { ActivityDelete, ActivityPubSignature } from '../../shared' |
5 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { logger } from '../helpers/logger' | 6 | import { logger } from '../helpers/logger' |
4 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' | 7 | import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' |
5 | import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants' | 8 | import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants' |
6 | import { getOrCreateActorAndServerAndModel } from '../lib/activitypub/actor' | 9 | import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../lib/activitypub/actors' |
7 | import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger' | ||
8 | import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor' | ||
9 | import { getAPId } from '@server/helpers/activitypub' | ||
10 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | ||
11 | 10 | ||
12 | async function checkSignature (req: Request, res: Response, next: NextFunction) { | 11 | async function checkSignature (req: Request, res: Response, next: NextFunction) { |
13 | try { | 12 | try { |
@@ -29,11 +28,14 @@ async function checkSignature (req: Request, res: Response, next: NextFunction) | |||
29 | const activity: ActivityDelete = req.body | 28 | const activity: ActivityDelete = req.body |
30 | if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) { | 29 | if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) { |
31 | logger.debug('Handling signature error on actor delete activity', { err }) | 30 | logger.debug('Handling signature error on actor delete activity', { err }) |
32 | return res.sendStatus(HttpStatusCode.NO_CONTENT_204) | 31 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
33 | } | 32 | } |
34 | 33 | ||
35 | logger.warn('Error in ActivityPub signature checker.', { err }) | 34 | logger.warn('Error in ActivityPub signature checker.', { err }) |
36 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 35 | return res.fail({ |
36 | status: HttpStatusCode.FORBIDDEN_403, | ||
37 | message: 'ActivityPub signature could not be checked' | ||
38 | }) | ||
37 | } | 39 | } |
38 | } | 40 | } |
39 | 41 | ||
@@ -71,13 +73,22 @@ async function checkHttpSignature (req: Request, res: Response) { | |||
71 | } catch (err) { | 73 | } catch (err) { |
72 | logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err }) | 74 | logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err }) |
73 | 75 | ||
74 | res.status(HttpStatusCode.FORBIDDEN_403).json({ error: err.message }) | 76 | res.fail({ |
77 | status: HttpStatusCode.FORBIDDEN_403, | ||
78 | message: err.message | ||
79 | }) | ||
75 | return false | 80 | return false |
76 | } | 81 | } |
77 | 82 | ||
78 | const keyId = parsed.keyId | 83 | const keyId = parsed.keyId |
79 | if (!keyId) { | 84 | if (!keyId) { |
80 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 85 | res.fail({ |
86 | status: HttpStatusCode.FORBIDDEN_403, | ||
87 | message: 'Invalid key ID', | ||
88 | data: { | ||
89 | keyId | ||
90 | } | ||
91 | }) | ||
81 | return false | 92 | return false |
82 | } | 93 | } |
83 | 94 | ||
@@ -88,18 +99,23 @@ async function checkHttpSignature (req: Request, res: Response) { | |||
88 | actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, '')) | 99 | actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, '')) |
89 | } | 100 | } |
90 | 101 | ||
91 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | 102 | const actor = await getOrCreateAPActor(actorUrl) |
92 | 103 | ||
93 | const verified = isHTTPSignatureVerified(parsed, actor) | 104 | const verified = isHTTPSignatureVerified(parsed, actor) |
94 | if (verified !== true) { | 105 | if (verified !== true) { |
95 | logger.warn('Signature from %s is invalid', actorUrl, { parsed }) | 106 | logger.warn('Signature from %s is invalid', actorUrl, { parsed }) |
96 | 107 | ||
97 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 108 | res.fail({ |
109 | status: HttpStatusCode.FORBIDDEN_403, | ||
110 | message: 'Invalid signature', | ||
111 | data: { | ||
112 | actorUrl | ||
113 | } | ||
114 | }) | ||
98 | return false | 115 | return false |
99 | } | 116 | } |
100 | 117 | ||
101 | res.locals.signature = { actor } | 118 | res.locals.signature = { actor } |
102 | |||
103 | return true | 119 | return true |
104 | } | 120 | } |
105 | 121 | ||
@@ -107,7 +123,10 @@ async function checkJsonLDSignature (req: Request, res: Response) { | |||
107 | const signatureObject: ActivityPubSignature = req.body.signature | 123 | const signatureObject: ActivityPubSignature = req.body.signature |
108 | 124 | ||
109 | if (!signatureObject || !signatureObject.creator) { | 125 | if (!signatureObject || !signatureObject.creator) { |
110 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 126 | res.fail({ |
127 | status: HttpStatusCode.FORBIDDEN_403, | ||
128 | message: 'Object and creator signature do not match' | ||
129 | }) | ||
111 | return false | 130 | return false |
112 | } | 131 | } |
113 | 132 | ||
@@ -115,17 +134,19 @@ async function checkJsonLDSignature (req: Request, res: Response) { | |||
115 | 134 | ||
116 | logger.debug('Checking JsonLD signature of actor %s...', creator) | 135 | logger.debug('Checking JsonLD signature of actor %s...', creator) |
117 | 136 | ||
118 | const actor = await getOrCreateActorAndServerAndModel(creator) | 137 | const actor = await getOrCreateAPActor(creator) |
119 | const verified = await isJsonLDSignatureVerified(actor, req.body) | 138 | const verified = await isJsonLDSignatureVerified(actor, req.body) |
120 | 139 | ||
121 | if (verified !== true) { | 140 | if (verified !== true) { |
122 | logger.warn('Signature not verified.', req.body) | 141 | logger.warn('Signature not verified.', req.body) |
123 | 142 | ||
124 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 143 | res.fail({ |
144 | status: HttpStatusCode.FORBIDDEN_403, | ||
145 | message: 'Signature could not be verified' | ||
146 | }) | ||
125 | return false | 147 | return false |
126 | } | 148 | } |
127 | 149 | ||
128 | res.locals.signature = { actor } | 150 | res.locals.signature = { actor } |
129 | |||
130 | return true | 151 | return true |
131 | } | 152 | } |
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index f38373624..176461cc2 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts | |||
@@ -16,11 +16,11 @@ function authenticate (req: express.Request, res: express.Response, next: expres | |||
16 | .catch(err => { | 16 | .catch(err => { |
17 | logger.warn('Cannot authenticate.', { err }) | 17 | logger.warn('Cannot authenticate.', { err }) |
18 | 18 | ||
19 | return res.status(err.status) | 19 | return res.fail({ |
20 | .json({ | 20 | status: err.status, |
21 | error: 'Token is invalid.', | 21 | message: 'Token is invalid', |
22 | code: err.name | 22 | type: err.name |
23 | }) | 23 | }) |
24 | }) | 24 | }) |
25 | } | 25 | } |
26 | 26 | ||
@@ -52,7 +52,12 @@ function authenticatePromiseIfNeeded (req: express.Request, res: express.Respons | |||
52 | // Already authenticated? (or tried to) | 52 | // Already authenticated? (or tried to) |
53 | if (res.locals.oauth?.token.User) return resolve() | 53 | if (res.locals.oauth?.token.User) return resolve() |
54 | 54 | ||
55 | if (res.locals.authenticated === false) return res.sendStatus(HttpStatusCode.UNAUTHORIZED_401) | 55 | if (res.locals.authenticated === false) { |
56 | return res.fail({ | ||
57 | status: HttpStatusCode.UNAUTHORIZED_401, | ||
58 | message: 'Not authenticated' | ||
59 | }) | ||
60 | } | ||
56 | 61 | ||
57 | authenticate(req, res, () => resolve(), authenticateInQuery) | 62 | authenticate(req, res, () => resolve(), authenticateInQuery) |
58 | }) | 63 | }) |
diff --git a/server/middlewares/doc.ts b/server/middlewares/doc.ts new file mode 100644 index 000000000..3db85c68d --- /dev/null +++ b/server/middlewares/doc.ts | |||
@@ -0,0 +1,16 @@ | |||
1 | import * as express from 'express' | ||
2 | |||
3 | function openapiOperationDoc (options: { | ||
4 | url?: string | ||
5 | operationId?: string | ||
6 | }) { | ||
7 | return (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
8 | res.locals.docUrl = options.url || 'https://docs.joinpeertube.org/api-rest-reference.html#operation/' + options.operationId | ||
9 | |||
10 | if (next) return next() | ||
11 | } | ||
12 | } | ||
13 | |||
14 | export { | ||
15 | openapiOperationDoc | ||
16 | } | ||
diff --git a/server/middlewares/error.ts b/server/middlewares/error.ts new file mode 100644 index 000000000..e3eb1c8f5 --- /dev/null +++ b/server/middlewares/error.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import * as express from 'express' | ||
2 | import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details' | ||
3 | import { HttpStatusCode } from '@shared/core-utils' | ||
4 | |||
5 | function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
6 | res.fail = options => { | ||
7 | const { status = HttpStatusCode.BAD_REQUEST_400, message, title, type, data, instance } = options | ||
8 | |||
9 | const extension = new ProblemDocumentExtension({ | ||
10 | ...data, | ||
11 | |||
12 | docs: res.locals.docUrl, | ||
13 | code: type, | ||
14 | |||
15 | // For <= 3.2 compatibility | ||
16 | error: message | ||
17 | }) | ||
18 | |||
19 | res.status(status) | ||
20 | res.setHeader('Content-Type', 'application/problem+json') | ||
21 | res.json(new ProblemDocument({ | ||
22 | status, | ||
23 | title, | ||
24 | instance, | ||
25 | |||
26 | detail: message, | ||
27 | |||
28 | type: type | ||
29 | ? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}` | ||
30 | : undefined | ||
31 | }, extension)) | ||
32 | } | ||
33 | |||
34 | if (next) next() | ||
35 | } | ||
36 | |||
37 | export { | ||
38 | apiFailMiddleware | ||
39 | } | ||
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 3e280e16f..413653dac 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts | |||
@@ -7,4 +7,6 @@ export * from './servers' | |||
7 | export * from './sort' | 7 | export * from './sort' |
8 | export * from './user-right' | 8 | export * from './user-right' |
9 | export * from './dnt' | 9 | export * from './dnt' |
10 | export * from './error' | ||
11 | export * from './doc' | ||
10 | export * from './csp' | 12 | export * from './csp' |
diff --git a/server/middlewares/servers.ts b/server/middlewares/servers.ts index 5e1c165f0..9aa56bc93 100644 --- a/server/middlewares/servers.ts +++ b/server/middlewares/servers.ts | |||
@@ -10,7 +10,10 @@ function setBodyHostsPort (req: express.Request, res: express.Response, next: ex | |||
10 | 10 | ||
11 | // Problem with the url parsing? | 11 | // Problem with the url parsing? |
12 | if (hostWithPort === null) { | 12 | if (hostWithPort === null) { |
13 | return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) | 13 | return res.fail({ |
14 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
15 | message: 'Could not parse hosts' | ||
16 | }) | ||
14 | } | 17 | } |
15 | 18 | ||
16 | req.body.hosts[i] = hostWithPort | 19 | req.body.hosts[i] = hostWithPort |
diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts index 45dda4781..d1888c2d3 100644 --- a/server/middlewares/user-right.ts +++ b/server/middlewares/user-right.ts | |||
@@ -10,8 +10,10 @@ function ensureUserHasRight (userRight: UserRight) { | |||
10 | const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.` | 10 | const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.` |
11 | logger.info(message) | 11 | logger.info(message) |
12 | 12 | ||
13 | return res.status(HttpStatusCode.FORBIDDEN_403) | 13 | return res.fail({ |
14 | .json({ error: message }) | 14 | status: HttpStatusCode.FORBIDDEN_403, |
15 | message | ||
16 | }) | ||
15 | } | 17 | } |
16 | 18 | ||
17 | return next() | 19 | return next() |
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts index 3b897fdef..c048bc6af 100644 --- a/server/middlewares/validators/abuse.ts +++ b/server/middlewares/validators/abuse.ts | |||
@@ -12,14 +12,12 @@ import { | |||
12 | isAbuseTimestampValid, | 12 | isAbuseTimestampValid, |
13 | isAbuseVideoIsValid | 13 | isAbuseVideoIsValid |
14 | } from '@server/helpers/custom-validators/abuses' | 14 | } from '@server/helpers/custom-validators/abuses' |
15 | import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' | 15 | import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from '@server/helpers/custom-validators/misc' |
16 | import { doesCommentIdExist } from '@server/helpers/custom-validators/video-comments' | ||
17 | import { logger } from '@server/helpers/logger' | 16 | import { logger } from '@server/helpers/logger' |
18 | import { doesAbuseExist, doesAccountIdExist, doesVideoExist } from '@server/helpers/middlewares' | ||
19 | import { AbuseMessageModel } from '@server/models/abuse/abuse-message' | 17 | import { AbuseMessageModel } from '@server/models/abuse/abuse-message' |
20 | import { AbuseCreate, UserRight } from '@shared/models' | 18 | import { AbuseCreate, UserRight } from '@shared/models' |
21 | import { areValidationErrors } from './utils' | ||
22 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 19 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
20 | import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared' | ||
23 | 21 | ||
24 | const abuseReportValidator = [ | 22 | const abuseReportValidator = [ |
25 | body('account.id') | 23 | body('account.id') |
@@ -29,6 +27,7 @@ const abuseReportValidator = [ | |||
29 | 27 | ||
30 | body('video.id') | 28 | body('video.id') |
31 | .optional() | 29 | .optional() |
30 | .customSanitizer(toCompleteUUID) | ||
32 | .custom(isIdOrUUIDValid) | 31 | .custom(isIdOrUUIDValid) |
33 | .withMessage('Should have a valid videoId'), | 32 | .withMessage('Should have a valid videoId'), |
34 | body('video.startAt') | 33 | body('video.startAt') |
@@ -71,9 +70,7 @@ const abuseReportValidator = [ | |||
71 | if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return | 70 | if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return |
72 | 71 | ||
73 | if (!body.video?.id && !body.account?.id && !body.comment?.id) { | 72 | if (!body.video?.id && !body.account?.id && !body.comment?.id) { |
74 | res.status(HttpStatusCode.BAD_REQUEST_400) | 73 | res.fail({ message: 'video id or account id or comment id is required.' }) |
75 | .json({ error: 'video id or account id or comment id is required.' }) | ||
76 | |||
77 | return | 74 | return |
78 | } | 75 | } |
79 | 76 | ||
@@ -195,8 +192,10 @@ const getAbuseValidator = [ | |||
195 | const message = `User ${user.username} does not have right to get abuse ${abuse.id}` | 192 | const message = `User ${user.username} does not have right to get abuse ${abuse.id}` |
196 | logger.warn(message) | 193 | logger.warn(message) |
197 | 194 | ||
198 | return res.status(HttpStatusCode.FORBIDDEN_403) | 195 | return res.fail({ |
199 | .json({ error: message }) | 196 | status: HttpStatusCode.FORBIDDEN_403, |
197 | message | ||
198 | }) | ||
200 | } | 199 | } |
201 | 200 | ||
202 | return next() | 201 | return next() |
@@ -209,10 +208,7 @@ const checkAbuseValidForMessagesValidator = [ | |||
209 | 208 | ||
210 | const abuse = res.locals.abuse | 209 | const abuse = res.locals.abuse |
211 | if (abuse.ReporterAccount.isOwned() === false) { | 210 | if (abuse.ReporterAccount.isOwned() === false) { |
212 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 211 | return res.fail({ message: 'This abuse was created by a user of your instance.' }) |
213 | .json({ | ||
214 | error: 'This abuse was created by a user of your instance.' | ||
215 | }) | ||
216 | } | 212 | } |
217 | 213 | ||
218 | return next() | 214 | return next() |
@@ -246,13 +242,17 @@ const deleteAbuseMessageValidator = [ | |||
246 | const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) | 242 | const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) |
247 | 243 | ||
248 | if (!abuseMessage) { | 244 | if (!abuseMessage) { |
249 | return res.status(HttpStatusCode.NOT_FOUND_404) | 245 | return res.fail({ |
250 | .json({ error: 'Abuse message not found' }) | 246 | status: HttpStatusCode.NOT_FOUND_404, |
247 | message: 'Abuse message not found' | ||
248 | }) | ||
251 | } | 249 | } |
252 | 250 | ||
253 | if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) { | 251 | if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) { |
254 | return res.status(HttpStatusCode.FORBIDDEN_403) | 252 | return res.fail({ |
255 | .json({ error: 'Cannot delete this abuse message' }) | 253 | status: HttpStatusCode.FORBIDDEN_403, |
254 | message: 'Cannot delete this abuse message' | ||
255 | }) | ||
256 | } | 256 | } |
257 | 257 | ||
258 | res.locals.abuseMessage = abuseMessage | 258 | res.locals.abuseMessage = abuseMessage |
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts index cbdcef2fd..599eb10bb 100644 --- a/server/middlewares/validators/account.ts +++ b/server/middlewares/validators/account.ts | |||
@@ -2,8 +2,7 @@ import * as express from 'express' | |||
2 | import { param } from 'express-validator' | 2 | import { param } from 'express-validator' |
3 | import { isAccountNameValid } from '../../helpers/custom-validators/accounts' | 3 | import { isAccountNameValid } from '../../helpers/custom-validators/accounts' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors, doesAccountNameWithHostExist, doesLocalAccountNameExist } from './shared' |
6 | import { doesAccountNameWithHostExist, doesLocalAccountNameExist } from '../../helpers/middlewares' | ||
7 | 6 | ||
8 | const localAccountValidator = [ | 7 | const localAccountValidator = [ |
9 | param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), | 8 | param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), |
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts index e78ef07ef..cc6acd4b1 100644 --- a/server/middlewares/validators/activitypub/activity.ts +++ b/server/middlewares/validators/activitypub/activity.ts | |||
@@ -9,16 +9,14 @@ async function activityPubValidator (req: express.Request, res: express.Response | |||
9 | 9 | ||
10 | if (!isRootActivityValid(req.body)) { | 10 | if (!isRootActivityValid(req.body)) { |
11 | logger.warn('Incorrect activity parameters.', { activity: req.body }) | 11 | logger.warn('Incorrect activity parameters.', { activity: req.body }) |
12 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 12 | return res.fail({ message: 'Incorrect activity' }) |
13 | .json({ error: 'Incorrect activity.' }) | ||
14 | } | 13 | } |
15 | 14 | ||
16 | const serverActor = await getServerActor() | 15 | const serverActor = await getServerActor() |
17 | const remoteActor = res.locals.signature.actor | 16 | const remoteActor = res.locals.signature.actor |
18 | if (serverActor.id === remoteActor.id || remoteActor.serverId === null) { | 17 | if (serverActor.id === remoteActor.id || remoteActor.serverId === null) { |
19 | logger.error('Receiving request in INBOX by ourselves!', req.body) | 18 | logger.error('Receiving request in INBOX by ourselves!', req.body) |
20 | return res.status(HttpStatusCode.CONFLICT_409) | 19 | return res.status(HttpStatusCode.CONFLICT_409).end() |
21 | .end() | ||
22 | } | 20 | } |
23 | 21 | ||
24 | return next() | 22 | return next() |
diff --git a/server/middlewares/validators/activitypub/pagination.ts b/server/middlewares/validators/activitypub/pagination.ts index fa21f063d..c8ec34eb6 100644 --- a/server/middlewares/validators/activitypub/pagination.ts +++ b/server/middlewares/validators/activitypub/pagination.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
3 | import { logger } from '../../../helpers/logger' | ||
4 | import { areValidationErrors } from '../utils' | ||
5 | import { PAGINATION } from '@server/initializers/constants' | 3 | import { PAGINATION } from '@server/initializers/constants' |
4 | import { logger } from '../../../helpers/logger' | ||
5 | import { areValidationErrors } from '../shared' | ||
6 | 6 | ||
7 | const apPaginationValidator = [ | 7 | const apPaginationValidator = [ |
8 | query('page') | 8 | query('page') |
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts index 7c4e49463..f2f7d5848 100644 --- a/server/middlewares/validators/activitypub/signature.ts +++ b/server/middlewares/validators/activitypub/signature.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { | 3 | import { |
4 | isSignatureCreatorValid, isSignatureTypeValid, | 4 | isSignatureCreatorValid, |
5 | isSignatureTypeValid, | ||
5 | isSignatureValueValid | 6 | isSignatureValueValid |
6 | } from '../../../helpers/custom-validators/activitypub/signature' | 7 | } from '../../../helpers/custom-validators/activitypub/signature' |
7 | import { isDateValid } from '../../../helpers/custom-validators/misc' | 8 | import { isDateValid } from '../../../helpers/custom-validators/misc' |
8 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
9 | import { areValidationErrors } from '../utils' | 10 | import { areValidationErrors } from '../shared' |
10 | 11 | ||
11 | const signatureValidator = [ | 12 | const signatureValidator = [ |
12 | body('signature.type') | 13 | body('signature.type') |
@@ -14,7 +15,7 @@ const signatureValidator = [ | |||
14 | .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), | 15 | .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), |
15 | body('signature.created') | 16 | body('signature.created') |
16 | .optional() | 17 | .optional() |
17 | .custom(isDateValid).withMessage('Should have a valid signature created date'), | 18 | .custom(isDateValid).withMessage('Should have a signature created date that conforms to ISO 8601'), |
18 | body('signature.creator') | 19 | body('signature.creator') |
19 | .optional() | 20 | .optional() |
20 | .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), | 21 | .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), |
diff --git a/server/middlewares/validators/actor-image.ts b/server/middlewares/validators/actor-image.ts index 961d7a7e5..49daadd61 100644 --- a/server/middlewares/validators/actor-image.ts +++ b/server/middlewares/validators/actor-image.ts | |||
@@ -4,7 +4,7 @@ import { isActorImageFile } from '@server/helpers/custom-validators/actor-images | |||
4 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 4 | import { cleanUpReqFiles } from '../../helpers/express-utils' |
5 | import { logger } from '../../helpers/logger' | 5 | import { logger } from '../../helpers/logger' |
6 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 6 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
7 | import { areValidationErrors } from './utils' | 7 | import { areValidationErrors } from './shared' |
8 | 8 | ||
9 | const updateActorImageValidatorFactory = (fieldname: string) => ([ | 9 | const updateActorImageValidatorFactory = (fieldname: string) => ([ |
10 | body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage( | 10 | body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage( |
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts index f61811a1a..826b16fc8 100644 --- a/server/middlewares/validators/blocklist.ts +++ b/server/middlewares/validators/blocklist.ts | |||
@@ -1,15 +1,14 @@ | |||
1 | import { body, param } from 'express-validator' | ||
2 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator' | ||
3 | import { getServerActor } from '@server/models/application/application' | ||
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
3 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
4 | import { areValidationErrors } from './utils' | 7 | import { WEBSERVER } from '../../initializers/constants' |
5 | import { AccountBlocklistModel } from '../../models/account/account-blocklist' | 8 | import { AccountBlocklistModel } from '../../models/account/account-blocklist' |
6 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
7 | import { ServerBlocklistModel } from '../../models/server/server-blocklist' | ||
8 | import { ServerModel } from '../../models/server/server' | 9 | import { ServerModel } from '../../models/server/server' |
9 | import { WEBSERVER } from '../../initializers/constants' | 10 | import { ServerBlocklistModel } from '../../models/server/server-blocklist' |
10 | import { doesAccountNameWithHostExist } from '../../helpers/middlewares' | 11 | import { areValidationErrors, doesAccountNameWithHostExist } from './shared' |
11 | import { getServerActor } from '@server/models/application/application' | ||
12 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
13 | 12 | ||
14 | const blockAccountValidator = [ | 13 | const blockAccountValidator = [ |
15 | body('accountName').exists().withMessage('Should have an account name with host'), | 14 | body('accountName').exists().withMessage('Should have an account name with host'), |
@@ -24,9 +23,10 @@ const blockAccountValidator = [ | |||
24 | const accountToBlock = res.locals.account | 23 | const accountToBlock = res.locals.account |
25 | 24 | ||
26 | if (user.Account.id === accountToBlock.id) { | 25 | if (user.Account.id === accountToBlock.id) { |
27 | res.status(HttpStatusCode.CONFLICT_409) | 26 | res.fail({ |
28 | .json({ error: 'You cannot block yourself.' }) | 27 | status: HttpStatusCode.CONFLICT_409, |
29 | 28 | message: 'You cannot block yourself.' | |
29 | }) | ||
30 | return | 30 | return |
31 | } | 31 | } |
32 | 32 | ||
@@ -79,8 +79,10 @@ const blockServerValidator = [ | |||
79 | const host: string = req.body.host | 79 | const host: string = req.body.host |
80 | 80 | ||
81 | if (host === WEBSERVER.HOST) { | 81 | if (host === WEBSERVER.HOST) { |
82 | return res.status(HttpStatusCode.CONFLICT_409) | 82 | return res.fail({ |
83 | .json({ error: 'You cannot block your own server.' }) | 83 | status: HttpStatusCode.CONFLICT_409, |
84 | message: 'You cannot block your own server.' | ||
85 | }) | ||
84 | } | 86 | } |
85 | 87 | ||
86 | const server = await ServerModel.loadOrCreateByHost(host) | 88 | const server = await ServerModel.loadOrCreateByHost(host) |
@@ -137,27 +139,27 @@ export { | |||
137 | async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) { | 139 | async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) { |
138 | const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) | 140 | const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) |
139 | if (!accountBlock) { | 141 | if (!accountBlock) { |
140 | res.status(HttpStatusCode.NOT_FOUND_404) | 142 | res.fail({ |
141 | .json({ error: 'Account block entry not found.' }) | 143 | status: HttpStatusCode.NOT_FOUND_404, |
142 | 144 | message: 'Account block entry not found.' | |
145 | }) | ||
143 | return false | 146 | return false |
144 | } | 147 | } |
145 | 148 | ||
146 | res.locals.accountBlock = accountBlock | 149 | res.locals.accountBlock = accountBlock |
147 | |||
148 | return true | 150 | return true |
149 | } | 151 | } |
150 | 152 | ||
151 | async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) { | 153 | async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) { |
152 | const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) | 154 | const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) |
153 | if (!serverBlock) { | 155 | if (!serverBlock) { |
154 | res.status(HttpStatusCode.NOT_FOUND_404) | 156 | res.fail({ |
155 | .json({ error: 'Server block entry not found.' }) | 157 | status: HttpStatusCode.NOT_FOUND_404, |
156 | 158 | message: 'Server block entry not found.' | |
159 | }) | ||
157 | return false | 160 | return false |
158 | } | 161 | } |
159 | 162 | ||
160 | res.locals.serverBlock = serverBlock | 163 | res.locals.serverBlock = serverBlock |
161 | |||
162 | return true | 164 | return true |
163 | } | 165 | } |
diff --git a/server/middlewares/validators/bulk.ts b/server/middlewares/validators/bulk.ts index cfb16d352..9bb95f5b7 100644 --- a/server/middlewares/validators/bulk.ts +++ b/server/middlewares/validators/bulk.ts | |||
@@ -1,12 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk' | 3 | import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk' |
4 | import { doesAccountNameWithHostExist } from '@server/helpers/middlewares' | 4 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
5 | import { UserRight } from '@shared/models' | 5 | import { UserRight } from '@shared/models' |
6 | import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' | 6 | import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' |
7 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../helpers/logger' |
8 | import { areValidationErrors } from './utils' | 8 | import { areValidationErrors, doesAccountNameWithHostExist } from './shared' |
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
10 | 9 | ||
11 | const bulkRemoveCommentsOfValidator = [ | 10 | const bulkRemoveCommentsOfValidator = [ |
12 | body('accountName').exists().withMessage('Should have an account name with host'), | 11 | body('accountName').exists().withMessage('Should have an account name with host'), |
@@ -23,9 +22,9 @@ const bulkRemoveCommentsOfValidator = [ | |||
23 | const body = req.body as BulkRemoveCommentsOfBody | 22 | const body = req.body as BulkRemoveCommentsOfBody |
24 | 23 | ||
25 | if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) { | 24 | if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) { |
26 | return res.status(HttpStatusCode.FORBIDDEN_403) | 25 | return res.fail({ |
27 | .json({ | 26 | status: HttpStatusCode.FORBIDDEN_403, |
28 | error: 'User cannot remove any comments of this instance.' | 27 | message: 'User cannot remove any comments of this instance.' |
29 | }) | 28 | }) |
30 | } | 29 | } |
31 | 30 | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index a85883b19..1aeadbe65 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -2,13 +2,12 @@ import * as express from 'express' | |||
2 | import { body } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { isIntOrNull } from '@server/helpers/custom-validators/misc' | 3 | import { isIntOrNull } from '@server/helpers/custom-validators/misc' |
4 | import { isEmailEnabled } from '@server/initializers/config' | 4 | import { isEmailEnabled } from '@server/initializers/config' |
5 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
6 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' | 5 | import { CustomConfig } from '../../../shared/models/server/custom-config.model' |
7 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 6 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
8 | import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' | 7 | import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' |
9 | import { logger } from '../../helpers/logger' | 8 | import { logger } from '../../helpers/logger' |
10 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | 9 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' |
11 | import { areValidationErrors } from './utils' | 10 | import { areValidationErrors } from './shared' |
12 | 11 | ||
13 | const customConfigUpdateValidator = [ | 12 | const customConfigUpdateValidator = [ |
14 | body('instance.name').exists().withMessage('Should have a valid instance name'), | 13 | body('instance.name').exists().withMessage('Should have a valid instance name'), |
@@ -30,6 +29,7 @@ const customConfigUpdateValidator = [ | |||
30 | body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), | 29 | body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), |
31 | body('signup.limit').isInt().withMessage('Should have a valid signup limit'), | 30 | body('signup.limit').isInt().withMessage('Should have a valid signup limit'), |
32 | body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'), | 31 | body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'), |
32 | body('signup.minimumAge').isInt().withMessage("Should have a valid minimum age required"), | ||
33 | 33 | ||
34 | body('admin.email').isEmail().withMessage('Should have a valid administrator email'), | 34 | body('admin.email').isEmail().withMessage('Should have a valid administrator email'), |
35 | body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'), | 35 | body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'), |
@@ -114,9 +114,7 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp | |||
114 | if (isEmailEnabled()) return true | 114 | if (isEmailEnabled()) return true |
115 | 115 | ||
116 | if (customConfig.signup.requiresEmailVerification === true) { | 116 | if (customConfig.signup.requiresEmailVerification === true) { |
117 | res.status(HttpStatusCode.BAD_REQUEST_400) | 117 | res.fail({ message: 'Emailer is disabled but you require signup email verification.' }) |
118 | .send({ error: 'Emailer is disabled but you require signup email verification.' }) | ||
119 | .end() | ||
120 | return false | 118 | return false |
121 | } | 119 | } |
122 | 120 | ||
@@ -127,9 +125,7 @@ function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express | |||
127 | if (customConfig.transcoding.enabled === false) return true | 125 | if (customConfig.transcoding.enabled === false) return true |
128 | 126 | ||
129 | if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { | 127 | if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { |
130 | res.status(HttpStatusCode.BAD_REQUEST_400) | 128 | res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' }) |
131 | .send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' }) | ||
132 | .end() | ||
133 | return false | 129 | return false |
134 | } | 130 | } |
135 | 131 | ||
@@ -140,9 +136,7 @@ function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Respon | |||
140 | if (customConfig.live.enabled === false) return true | 136 | if (customConfig.live.enabled === false) return true |
141 | 137 | ||
142 | if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) { | 138 | if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) { |
143 | res.status(HttpStatusCode.BAD_REQUEST_400) | 139 | res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' }) |
144 | .send({ error: 'You cannot allow live replay if transcoding is not enabled' }) | ||
145 | .end() | ||
146 | return false | 140 | return false |
147 | } | 141 | } |
148 | 142 | ||
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts index 617661813..51b8fdd19 100644 --- a/server/middlewares/validators/feeds.ts +++ b/server/middlewares/validators/feeds.ts | |||
@@ -1,18 +1,19 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { param, query } from 'express-validator' | 2 | import { param, query } from 'express-validator' |
3 | |||
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' | 5 | import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' |
4 | import { exists, isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' | 6 | import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc' |
5 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../helpers/logger' |
6 | import { | 8 | import { |
9 | areValidationErrors, | ||
7 | doesAccountIdExist, | 10 | doesAccountIdExist, |
8 | doesAccountNameWithHostExist, | 11 | doesAccountNameWithHostExist, |
9 | doesUserFeedTokenCorrespond, | 12 | doesUserFeedTokenCorrespond, |
10 | doesVideoChannelIdExist, | 13 | doesVideoChannelIdExist, |
11 | doesVideoChannelNameWithHostExist | 14 | doesVideoChannelNameWithHostExist, |
12 | } from '../../helpers/middlewares' | 15 | doesVideoExist |
13 | import { doesVideoExist } from '../../helpers/middlewares/videos' | 16 | } from './shared' |
14 | import { areValidationErrors } from './utils' | ||
15 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
16 | 17 | ||
17 | const feedsFormatValidator = [ | 18 | const feedsFormatValidator = [ |
18 | param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'), | 19 | param('format').optional().custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'), |
@@ -36,10 +37,10 @@ function setFeedFormatContentType (req: express.Request, res: express.Response, | |||
36 | if (req.accepts(acceptableContentTypes)) { | 37 | if (req.accepts(acceptableContentTypes)) { |
37 | res.set('Content-Type', req.accepts(acceptableContentTypes) as string) | 38 | res.set('Content-Type', req.accepts(acceptableContentTypes) as string) |
38 | } else { | 39 | } else { |
39 | return res.status(HttpStatusCode.NOT_ACCEPTABLE_406) | 40 | return res.fail({ |
40 | .json({ | 41 | status: HttpStatusCode.NOT_ACCEPTABLE_406, |
41 | message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` | 42 | message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` |
42 | }) | 43 | }) |
43 | } | 44 | } |
44 | 45 | ||
45 | return next() | 46 | return next() |
@@ -98,7 +99,10 @@ const videoSubscriptionFeedsValidator = [ | |||
98 | ] | 99 | ] |
99 | 100 | ||
100 | const videoCommentsFeedsValidator = [ | 101 | const videoCommentsFeedsValidator = [ |
101 | query('videoId').optional().custom(isIdOrUUIDValid), | 102 | query('videoId') |
103 | .customSanitizer(toCompleteUUID) | ||
104 | .optional() | ||
105 | .custom(isIdOrUUIDValid), | ||
102 | 106 | ||
103 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 107 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
104 | logger.debug('Checking feeds parameters', { parameters: req.query }) | 108 | logger.debug('Checking feeds parameters', { parameters: req.query }) |
@@ -106,10 +110,7 @@ const videoCommentsFeedsValidator = [ | |||
106 | if (areValidationErrors(req, res)) return | 110 | if (areValidationErrors(req, res)) return |
107 | 111 | ||
108 | if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) { | 112 | if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) { |
109 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 113 | return res.fail({ message: 'videoId cannot be mixed with a channel filter' }) |
110 | .json({ | ||
111 | message: 'videoId cannot be mixed with a channel filter' | ||
112 | }) | ||
113 | } | 114 | } |
114 | 115 | ||
115 | if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return | 116 | if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return |
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index bb849dc72..205baca48 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts | |||
@@ -1,18 +1,18 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { isFollowStateValid } from '@server/helpers/custom-validators/follows' | ||
4 | import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors' | ||
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { MActorFollowActorsDefault } from '@server/types/models' | ||
7 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { isTestInstance } from '../../helpers/core-utils' | 8 | import { isTestInstance } from '../../helpers/core-utils' |
9 | import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | ||
4 | import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' | 10 | import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' |
5 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
6 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' | 12 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' |
7 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 13 | import { ActorModel } from '../../models/actor/actor' |
8 | import { areValidationErrors } from './utils' | 14 | import { ActorFollowModel } from '../../models/actor/actor-follow' |
9 | import { ActorModel } from '../../models/activitypub/actor' | 15 | import { areValidationErrors } from './shared' |
10 | import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' | ||
11 | import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | ||
12 | import { MActorFollowActorsDefault } from '@server/types/models' | ||
13 | import { isFollowStateValid } from '@server/helpers/custom-validators/follows' | ||
14 | import { getServerActor } from '@server/models/application/application' | ||
15 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
16 | 16 | ||
17 | const listFollowsValidator = [ | 17 | const listFollowsValidator = [ |
18 | query('state') | 18 | query('state') |
@@ -63,11 +63,10 @@ const removeFollowingValidator = [ | |||
63 | const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host) | 63 | const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host) |
64 | 64 | ||
65 | if (!follow) { | 65 | if (!follow) { |
66 | return res | 66 | return res.fail({ |
67 | .status(HttpStatusCode.NOT_FOUND_404) | 67 | status: HttpStatusCode.NOT_FOUND_404, |
68 | .json({ | 68 | message: `Following ${req.params.host} not found.` |
69 | error: `Following ${req.params.host} not found.` | 69 | }) |
70 | }) | ||
71 | } | 70 | } |
72 | 71 | ||
73 | res.locals.follow = follow | 72 | res.locals.follow = follow |
@@ -95,12 +94,10 @@ const getFollowerValidator = [ | |||
95 | } | 94 | } |
96 | 95 | ||
97 | if (!follow) { | 96 | if (!follow) { |
98 | return res | 97 | return res.fail({ |
99 | .status(HttpStatusCode.NOT_FOUND_404) | 98 | status: HttpStatusCode.NOT_FOUND_404, |
100 | .json({ | 99 | message: `Follower ${req.params.nameWithHost} not found.` |
101 | error: `Follower ${req.params.nameWithHost} not found.` | 100 | }) |
102 | }) | ||
103 | .end() | ||
104 | } | 101 | } |
105 | 102 | ||
106 | res.locals.follow = follow | 103 | res.locals.follow = follow |
@@ -114,12 +111,7 @@ const acceptOrRejectFollowerValidator = [ | |||
114 | 111 | ||
115 | const follow = res.locals.follow | 112 | const follow = res.locals.follow |
116 | if (follow.state !== 'pending') { | 113 | if (follow.state !== 'pending') { |
117 | return res | 114 | return res.fail({ message: 'Follow is not in pending state.' }) |
118 | .status(HttpStatusCode.BAD_REQUEST_400) | ||
119 | .json({ | ||
120 | error: 'Follow is not in pending state.' | ||
121 | }) | ||
122 | .end() | ||
123 | } | 115 | } |
124 | 116 | ||
125 | return next() | 117 | return next() |
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 24faeea3e..94a3c2dea 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -11,7 +11,7 @@ export * from './sort' | |||
11 | export * from './users' | 11 | export * from './users' |
12 | export * from './user-subscriptions' | 12 | export * from './user-subscriptions' |
13 | export * from './videos' | 13 | export * from './videos' |
14 | export * from './webfinger' | ||
15 | export * from './search' | 14 | export * from './search' |
16 | export * from './server' | 15 | export * from './server' |
17 | export * from './user-history' | 16 | export * from './user-history' |
17 | export * from './webfinger' | ||
diff --git a/server/middlewares/validators/jobs.ts b/server/middlewares/validators/jobs.ts index d87b28c06..5d89d167f 100644 --- a/server/middlewares/validators/jobs.ts +++ b/server/middlewares/validators/jobs.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { param, query } from 'express-validator' | 2 | import { param, query } from 'express-validator' |
3 | import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' | 3 | import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' |
4 | import { logger, loggerTagsFactory } from '../../helpers/logger' | 4 | import { logger, loggerTagsFactory } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './shared' |
6 | 6 | ||
7 | const lTags = loggerTagsFactory('validators', 'jobs') | 7 | const lTags = loggerTagsFactory('validators', 'jobs') |
8 | 8 | ||
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts index 70e4d0d99..c55baaee3 100644 --- a/server/middlewares/validators/logs.ts +++ b/server/middlewares/validators/logs.ts | |||
@@ -1,19 +1,19 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { logger } from '../../helpers/logger' | ||
3 | import { areValidationErrors } from './utils' | ||
4 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
5 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
6 | import { isValidLogLevel } from '../../helpers/custom-validators/logs' | 3 | import { isValidLogLevel } from '../../helpers/custom-validators/logs' |
4 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { areValidationErrors } from './shared' | ||
7 | 7 | ||
8 | const getLogsValidator = [ | 8 | const getLogsValidator = [ |
9 | query('startDate') | 9 | query('startDate') |
10 | .custom(isDateValid).withMessage('Should have a valid start date'), | 10 | .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'), |
11 | query('level') | 11 | query('level') |
12 | .optional() | 12 | .optional() |
13 | .custom(isValidLogLevel).withMessage('Should have a valid level'), | 13 | .custom(isValidLogLevel).withMessage('Should have a valid level'), |
14 | query('endDate') | 14 | query('endDate') |
15 | .optional() | 15 | .optional() |
16 | .custom(isDateValid).withMessage('Should have a valid end date'), | 16 | .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'), |
17 | 17 | ||
18 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 18 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
19 | logger.debug('Checking getLogsValidator parameters.', { parameters: req.query }) | 19 | logger.debug('Checking getLogsValidator parameters.', { parameters: req.query }) |
@@ -26,10 +26,10 @@ const getLogsValidator = [ | |||
26 | 26 | ||
27 | const getAuditLogsValidator = [ | 27 | const getAuditLogsValidator = [ |
28 | query('startDate') | 28 | query('startDate') |
29 | .custom(isDateValid).withMessage('Should have a valid start date'), | 29 | .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'), |
30 | query('endDate') | 30 | query('endDate') |
31 | .optional() | 31 | .optional() |
32 | .custom(isDateValid).withMessage('Should have a valid end date'), | 32 | .custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'), |
33 | 33 | ||
34 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 34 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
35 | logger.debug('Checking getAuditLogsValidator parameters.', { parameters: req.query }) | 35 | logger.debug('Checking getAuditLogsValidator parameters.', { parameters: req.query }) |
diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts index 2a7dc257b..0a82e6932 100644 --- a/server/middlewares/validators/oembed.ts +++ b/server/middlewares/validators/oembed.ts | |||
@@ -1,18 +1,32 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { fetchVideo } from '@server/helpers/video' | 4 | import { loadVideo } from '@server/lib/model-loaders' |
5 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 5 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
6 | import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | 6 | import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' |
7 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
7 | import { isTestInstance } from '../../helpers/core-utils' | 8 | import { isTestInstance } from '../../helpers/core-utils' |
8 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 9 | import { isIdOrUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc' |
9 | import { logger } from '../../helpers/logger' | 10 | import { logger } from '../../helpers/logger' |
10 | import { WEBSERVER } from '../../initializers/constants' | 11 | import { WEBSERVER } from '../../initializers/constants' |
11 | import { areValidationErrors } from './utils' | 12 | import { areValidationErrors } from './shared' |
12 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 13 | |
14 | const playlistPaths = [ | ||
15 | join('videos', 'watch', 'playlist'), | ||
16 | join('w', 'p') | ||
17 | ] | ||
18 | |||
19 | const videoPaths = [ | ||
20 | join('videos', 'watch'), | ||
21 | 'w' | ||
22 | ] | ||
23 | |||
24 | function buildUrls (paths: string[]) { | ||
25 | return paths.map(p => WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, p) + '/') | ||
26 | } | ||
13 | 27 | ||
14 | const startVideoPlaylistsURL = WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, 'videos', 'watch', 'playlist') + '/' | 28 | const startPlaylistURLs = buildUrls(playlistPaths) |
15 | const startVideosURL = WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, 'videos', 'watch') + '/' | 29 | const startVideoURLs = buildUrls(videoPaths) |
16 | 30 | ||
17 | const watchRegex = /([^/]+)$/ | 31 | const watchRegex = /([^/]+)$/ |
18 | const isURLOptions = { | 32 | const isURLOptions = { |
@@ -37,41 +51,54 @@ const oembedValidator = [ | |||
37 | if (areValidationErrors(req, res)) return | 51 | if (areValidationErrors(req, res)) return |
38 | 52 | ||
39 | if (req.query.format !== undefined && req.query.format !== 'json') { | 53 | if (req.query.format !== undefined && req.query.format !== 'json') { |
40 | return res.status(HttpStatusCode.NOT_IMPLEMENTED_501) | 54 | return res.fail({ |
41 | .json({ error: 'Requested format is not implemented on server.' }) | 55 | status: HttpStatusCode.NOT_IMPLEMENTED_501, |
56 | message: 'Requested format is not implemented on server.', | ||
57 | data: { | ||
58 | format: req.query.format | ||
59 | } | ||
60 | }) | ||
42 | } | 61 | } |
43 | 62 | ||
44 | const url = req.query.url as string | 63 | const url = req.query.url as string |
45 | 64 | ||
46 | const isPlaylist = url.startsWith(startVideoPlaylistsURL) | 65 | const isPlaylist = startPlaylistURLs.some(u => url.startsWith(u)) |
47 | const isVideo = isPlaylist ? false : url.startsWith(startVideosURL) | 66 | const isVideo = isPlaylist ? false : startVideoURLs.some(u => url.startsWith(u)) |
48 | 67 | ||
49 | const startIsOk = isVideo || isPlaylist | 68 | const startIsOk = isVideo || isPlaylist |
50 | 69 | ||
51 | const matches = watchRegex.exec(url) | 70 | const matches = watchRegex.exec(url) |
52 | 71 | ||
53 | if (startIsOk === false || matches === null) { | 72 | if (startIsOk === false || matches === null) { |
54 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 73 | return res.fail({ |
55 | .json({ error: 'Invalid url.' }) | 74 | status: HttpStatusCode.BAD_REQUEST_400, |
75 | message: 'Invalid url.', | ||
76 | data: { | ||
77 | url | ||
78 | } | ||
79 | }) | ||
56 | } | 80 | } |
57 | 81 | ||
58 | const elementId = matches[1] | 82 | const elementId = toCompleteUUID(matches[1]) |
59 | if (isIdOrUUIDValid(elementId) === false) { | 83 | if (isIdOrUUIDValid(elementId) === false) { |
60 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 84 | return res.fail({ message: 'Invalid video or playlist id.' }) |
61 | .json({ error: 'Invalid video or playlist id.' }) | ||
62 | } | 85 | } |
63 | 86 | ||
64 | if (isVideo) { | 87 | if (isVideo) { |
65 | const video = await fetchVideo(elementId, 'all') | 88 | const video = await loadVideo(elementId, 'all') |
66 | 89 | ||
67 | if (!video) { | 90 | if (!video) { |
68 | return res.status(HttpStatusCode.NOT_FOUND_404) | 91 | return res.fail({ |
69 | .json({ error: 'Video not found' }) | 92 | status: HttpStatusCode.NOT_FOUND_404, |
93 | message: 'Video not found' | ||
94 | }) | ||
70 | } | 95 | } |
71 | 96 | ||
72 | if (video.privacy !== VideoPrivacy.PUBLIC) { | 97 | if (video.privacy !== VideoPrivacy.PUBLIC) { |
73 | return res.status(HttpStatusCode.FORBIDDEN_403) | 98 | return res.fail({ |
74 | .json({ error: 'Video is not public' }) | 99 | status: HttpStatusCode.FORBIDDEN_403, |
100 | message: 'Video is not public' | ||
101 | }) | ||
75 | } | 102 | } |
76 | 103 | ||
77 | res.locals.videoAll = video | 104 | res.locals.videoAll = video |
@@ -82,13 +109,17 @@ const oembedValidator = [ | |||
82 | 109 | ||
83 | const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined) | 110 | const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined) |
84 | if (!videoPlaylist) { | 111 | if (!videoPlaylist) { |
85 | return res.status(HttpStatusCode.NOT_FOUND_404) | 112 | return res.fail({ |
86 | .json({ error: 'Video playlist not found' }) | 113 | status: HttpStatusCode.NOT_FOUND_404, |
114 | message: 'Video playlist not found' | ||
115 | }) | ||
87 | } | 116 | } |
88 | 117 | ||
89 | if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) { | 118 | if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) { |
90 | return res.status(HttpStatusCode.FORBIDDEN_403) | 119 | return res.fail({ |
91 | .json({ error: 'Playlist is not public' }) | 120 | status: HttpStatusCode.FORBIDDEN_403, |
121 | message: 'Playlist is not public' | ||
122 | }) | ||
92 | } | 123 | } |
93 | 124 | ||
94 | res.locals.videoPlaylistSummary = videoPlaylist | 125 | res.locals.videoPlaylistSummary = videoPlaylist |
diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts index 6b0a83d80..74eae251e 100644 --- a/server/middlewares/validators/pagination.ts +++ b/server/middlewares/validators/pagination.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
3 | import { logger } from '../../helpers/logger' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { PAGINATION } from '@server/initializers/constants' | 3 | import { PAGINATION } from '@server/initializers/constants' |
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './shared' | ||
6 | 6 | ||
7 | const paginationValidator = paginationValidatorBuilder() | 7 | const paginationValidator = paginationValidatorBuilder() |
8 | 8 | ||
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index ab87fe720..8c76d2e36 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query, ValidationChain } from 'express-validator' | 2 | import { body, param, query, ValidationChain } from 'express-validator' |
3 | import { logger } from '../../helpers/logger' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { areValidationErrors } from './utils' | 4 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
5 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model' | ||
6 | import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | ||
5 | import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' | 7 | import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' |
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CONFIG } from '../../initializers/config' | ||
6 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 10 | import { PluginManager } from '../../lib/plugins/plugin-manager' |
7 | import { isBooleanValid, isSafePath, toBooleanOrNull, exists, toIntOrNull } from '../../helpers/custom-validators/misc' | ||
8 | import { PluginModel } from '../../models/server/plugin' | 11 | import { PluginModel } from '../../models/server/plugin' |
9 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' | 12 | import { areValidationErrors } from './shared' |
10 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | ||
11 | import { CONFIG } from '../../initializers/config' | ||
12 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
13 | 13 | ||
14 | const getPluginValidator = (pluginType: PluginType, withVersion = true) => { | 14 | const getPluginValidator = (pluginType: PluginType, withVersion = true) => { |
15 | const validators: (ValidationChain | express.Handler)[] = [ | 15 | const validators: (ValidationChain | express.Handler)[] = [ |
@@ -31,8 +31,18 @@ const getPluginValidator = (pluginType: PluginType, withVersion = true) => { | |||
31 | const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) | 31 | const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) |
32 | const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) | 32 | const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) |
33 | 33 | ||
34 | if (!plugin) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 34 | if (!plugin) { |
35 | if (withVersion && plugin.version !== req.params.pluginVersion) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 35 | return res.fail({ |
36 | status: HttpStatusCode.NOT_FOUND_404, | ||
37 | message: 'No plugin found named ' + npmName | ||
38 | }) | ||
39 | } | ||
40 | if (withVersion && plugin.version !== req.params.pluginVersion) { | ||
41 | return res.fail({ | ||
42 | status: HttpStatusCode.NOT_FOUND_404, | ||
43 | message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion | ||
44 | }) | ||
45 | } | ||
36 | 46 | ||
37 | res.locals.registeredPlugin = plugin | 47 | res.locals.registeredPlugin = plugin |
38 | 48 | ||
@@ -50,10 +60,20 @@ const getExternalAuthValidator = [ | |||
50 | if (areValidationErrors(req, res)) return | 60 | if (areValidationErrors(req, res)) return |
51 | 61 | ||
52 | const plugin = res.locals.registeredPlugin | 62 | const plugin = res.locals.registeredPlugin |
53 | if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 63 | if (!plugin.registerHelpers) { |
64 | return res.fail({ | ||
65 | status: HttpStatusCode.NOT_FOUND_404, | ||
66 | message: 'No registered helpers were found for this plugin' | ||
67 | }) | ||
68 | } | ||
54 | 69 | ||
55 | const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName) | 70 | const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName) |
56 | if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 71 | if (!externalAuth) { |
72 | return res.fail({ | ||
73 | status: HttpStatusCode.NOT_FOUND_404, | ||
74 | message: 'No external auths were found for this plugin' | ||
75 | }) | ||
76 | } | ||
57 | 77 | ||
58 | res.locals.externalAuth = externalAuth | 78 | res.locals.externalAuth = externalAuth |
59 | 79 | ||
@@ -107,8 +127,7 @@ const installOrUpdatePluginValidator = [ | |||
107 | 127 | ||
108 | const body: InstallOrUpdatePlugin = req.body | 128 | const body: InstallOrUpdatePlugin = req.body |
109 | if (!body.path && !body.npmName) { | 129 | if (!body.path && !body.npmName) { |
110 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 130 | return res.fail({ message: 'Should have either a npmName or a path' }) |
111 | .json({ error: 'Should have either a npmName or a path' }) | ||
112 | } | 131 | } |
113 | 132 | ||
114 | return next() | 133 | return next() |
@@ -137,12 +156,13 @@ const existingPluginValidator = [ | |||
137 | 156 | ||
138 | const plugin = await PluginModel.loadByNpmName(req.params.npmName) | 157 | const plugin = await PluginModel.loadByNpmName(req.params.npmName) |
139 | if (!plugin) { | 158 | if (!plugin) { |
140 | return res.status(HttpStatusCode.NOT_FOUND_404) | 159 | return res.fail({ |
141 | .json({ error: 'Plugin not found' }) | 160 | status: HttpStatusCode.NOT_FOUND_404, |
161 | message: 'Plugin not found' | ||
162 | }) | ||
142 | } | 163 | } |
143 | 164 | ||
144 | res.locals.plugin = plugin | 165 | res.locals.plugin = plugin |
145 | |||
146 | return next() | 166 | return next() |
147 | } | 167 | } |
148 | ] | 168 | ] |
@@ -177,9 +197,7 @@ const listAvailablePluginsValidator = [ | |||
177 | if (areValidationErrors(req, res)) return | 197 | if (areValidationErrors(req, res)) return |
178 | 198 | ||
179 | if (CONFIG.PLUGINS.INDEX.ENABLED === false) { | 199 | if (CONFIG.PLUGINS.INDEX.ENABLED === false) { |
180 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 200 | return res.fail({ message: 'Plugin index is not enabled' }) |
181 | .json({ error: 'Plugin index is not enabled' }) | ||
182 | .end() | ||
183 | } | 201 | } |
184 | 202 | ||
185 | return next() | 203 | return next() |
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index c379aebe4..116c8c611 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts | |||
@@ -1,17 +1,25 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 3 | import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies' |
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { | ||
6 | exists, | ||
7 | isBooleanValid, | ||
8 | isIdOrUUIDValid, | ||
9 | isIdValid, | ||
10 | toBooleanOrNull, | ||
11 | toCompleteUUID, | ||
12 | toIntOrNull | ||
13 | } from '../../helpers/custom-validators/misc' | ||
14 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
4 | import { logger } from '../../helpers/logger' | 15 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | ||
6 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | 16 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' |
7 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
8 | import { ServerModel } from '../../models/server/server' | 17 | import { ServerModel } from '../../models/server/server' |
9 | import { doesVideoExist } from '../../helpers/middlewares' | 18 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared' |
10 | import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies' | ||
11 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
12 | 19 | ||
13 | const videoFileRedundancyGetValidator = [ | 20 | const videoFileRedundancyGetValidator = [ |
14 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 21 | isValidVideoIdParam('videoId'), |
22 | |||
15 | param('resolution') | 23 | param('resolution') |
16 | .customSanitizer(toIntOrNull) | 24 | .customSanitizer(toIntOrNull) |
17 | .custom(exists).withMessage('Should have a valid resolution'), | 25 | .custom(exists).withMessage('Should have a valid resolution'), |
@@ -35,11 +43,21 @@ const videoFileRedundancyGetValidator = [ | |||
35 | return f.resolution === paramResolution && (!req.params.fps || paramFPS) | 43 | return f.resolution === paramResolution && (!req.params.fps || paramFPS) |
36 | }) | 44 | }) |
37 | 45 | ||
38 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video file not found.' }) | 46 | if (!videoFile) { |
47 | return res.fail({ | ||
48 | status: HttpStatusCode.NOT_FOUND_404, | ||
49 | message: 'Video file not found.' | ||
50 | }) | ||
51 | } | ||
39 | res.locals.videoFile = videoFile | 52 | res.locals.videoFile = videoFile |
40 | 53 | ||
41 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) | 54 | const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) |
42 | if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) | 55 | if (!videoRedundancy) { |
56 | return res.fail({ | ||
57 | status: HttpStatusCode.NOT_FOUND_404, | ||
58 | message: 'Video redundancy not found.' | ||
59 | }) | ||
60 | } | ||
43 | res.locals.videoRedundancy = videoRedundancy | 61 | res.locals.videoRedundancy = videoRedundancy |
44 | 62 | ||
45 | return next() | 63 | return next() |
@@ -47,9 +65,8 @@ const videoFileRedundancyGetValidator = [ | |||
47 | ] | 65 | ] |
48 | 66 | ||
49 | const videoPlaylistRedundancyGetValidator = [ | 67 | const videoPlaylistRedundancyGetValidator = [ |
50 | param('videoId') | 68 | isValidVideoIdParam('videoId'), |
51 | .custom(isIdOrUUIDValid) | 69 | |
52 | .not().isEmpty().withMessage('Should have a valid video id'), | ||
53 | param('streamingPlaylistType') | 70 | param('streamingPlaylistType') |
54 | .customSanitizer(toIntOrNull) | 71 | .customSanitizer(toIntOrNull) |
55 | .custom(exists).withMessage('Should have a valid streaming playlist type'), | 72 | .custom(exists).withMessage('Should have a valid streaming playlist type'), |
@@ -65,11 +82,21 @@ const videoPlaylistRedundancyGetValidator = [ | |||
65 | const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above | 82 | const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above |
66 | const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType) | 83 | const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType) |
67 | 84 | ||
68 | if (!videoStreamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video playlist not found.' }) | 85 | if (!videoStreamingPlaylist) { |
86 | return res.fail({ | ||
87 | status: HttpStatusCode.NOT_FOUND_404, | ||
88 | message: 'Video playlist not found.' | ||
89 | }) | ||
90 | } | ||
69 | res.locals.videoStreamingPlaylist = videoStreamingPlaylist | 91 | res.locals.videoStreamingPlaylist = videoStreamingPlaylist |
70 | 92 | ||
71 | const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) | 93 | const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) |
72 | if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) | 94 | if (!videoRedundancy) { |
95 | return res.fail({ | ||
96 | status: HttpStatusCode.NOT_FOUND_404, | ||
97 | message: 'Video redundancy not found.' | ||
98 | }) | ||
99 | } | ||
73 | res.locals.videoRedundancy = videoRedundancy | 100 | res.locals.videoRedundancy = videoRedundancy |
74 | 101 | ||
75 | return next() | 102 | return next() |
@@ -90,12 +117,10 @@ const updateServerRedundancyValidator = [ | |||
90 | const server = await ServerModel.loadByHost(req.params.host) | 117 | const server = await ServerModel.loadByHost(req.params.host) |
91 | 118 | ||
92 | if (!server) { | 119 | if (!server) { |
93 | return res | 120 | return res.fail({ |
94 | .status(HttpStatusCode.NOT_FOUND_404) | 121 | status: HttpStatusCode.NOT_FOUND_404, |
95 | .json({ | 122 | message: `Server ${req.params.host} not found.` |
96 | error: `Server ${req.params.host} not found.` | 123 | }) |
97 | }) | ||
98 | .end() | ||
99 | } | 124 | } |
100 | 125 | ||
101 | res.locals.server = server | 126 | res.locals.server = server |
@@ -118,7 +143,8 @@ const listVideoRedundanciesValidator = [ | |||
118 | 143 | ||
119 | const addVideoRedundancyValidator = [ | 144 | const addVideoRedundancyValidator = [ |
120 | body('videoId') | 145 | body('videoId') |
121 | .custom(isIdValid) | 146 | .customSanitizer(toCompleteUUID) |
147 | .custom(isIdOrUUIDValid) | ||
122 | .withMessage('Should have a valid video id'), | 148 | .withMessage('Should have a valid video id'), |
123 | 149 | ||
124 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 150 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
@@ -129,19 +155,19 @@ const addVideoRedundancyValidator = [ | |||
129 | if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return | 155 | if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return |
130 | 156 | ||
131 | if (res.locals.onlyVideo.remote === false) { | 157 | if (res.locals.onlyVideo.remote === false) { |
132 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 158 | return res.fail({ message: 'Cannot create a redundancy on a local video' }) |
133 | .json({ error: 'Cannot create a redundancy on a local video' }) | ||
134 | } | 159 | } |
135 | 160 | ||
136 | if (res.locals.onlyVideo.isLive) { | 161 | if (res.locals.onlyVideo.isLive) { |
137 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 162 | return res.fail({ message: 'Cannot create a redundancy of a live video' }) |
138 | .json({ error: 'Cannot create a redundancy of a live video' }) | ||
139 | } | 163 | } |
140 | 164 | ||
141 | const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid) | 165 | const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid) |
142 | if (alreadyExists) { | 166 | if (alreadyExists) { |
143 | return res.status(HttpStatusCode.CONFLICT_409) | 167 | return res.fail({ |
144 | .json({ error: 'This video is already duplicated by your instance.' }) | 168 | status: HttpStatusCode.CONFLICT_409, |
169 | message: 'This video is already duplicated by your instance.' | ||
170 | }) | ||
145 | } | 171 | } |
146 | 172 | ||
147 | return next() | 173 | return next() |
@@ -160,9 +186,10 @@ const removeVideoRedundancyValidator = [ | |||
160 | 186 | ||
161 | const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) | 187 | const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) |
162 | if (!redundancy) { | 188 | if (!redundancy) { |
163 | return res.status(HttpStatusCode.NOT_FOUND_404) | 189 | return res.fail({ |
164 | .json({ error: 'Video redundancy not found' }) | 190 | status: HttpStatusCode.NOT_FOUND_404, |
165 | .end() | 191 | message: 'Video redundancy not found' |
192 | }) | ||
166 | } | 193 | } |
167 | 194 | ||
168 | res.locals.videoRedundancy = redundancy | 195 | res.locals.videoRedundancy = redundancy |
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index 78213c70d..7bbf81048 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts | |||
@@ -1,18 +1,26 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { areValidationErrors } from './utils' | ||
3 | import { logger } from '../../helpers/logger' | ||
4 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
5 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
6 | import { isSearchTargetValid } from '@server/helpers/custom-validators/search' | 3 | import { isSearchTargetValid } from '@server/helpers/custom-validators/search' |
4 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { areValidationErrors } from './shared' | ||
7 | 7 | ||
8 | const videosSearchValidator = [ | 8 | const videosSearchValidator = [ |
9 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | 9 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), |
10 | 10 | ||
11 | query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'), | 11 | query('startDate') |
12 | query('endDate').optional().custom(isDateValid).withMessage('Should have a valid end date'), | 12 | .optional() |
13 | .custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'), | ||
14 | query('endDate') | ||
15 | .optional() | ||
16 | .custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'), | ||
13 | 17 | ||
14 | query('originallyPublishedStartDate').optional().custom(isDateValid).withMessage('Should have a valid published start date'), | 18 | query('originallyPublishedStartDate') |
15 | query('originallyPublishedEndDate').optional().custom(isDateValid).withMessage('Should have a valid published end date'), | 19 | .optional() |
20 | .custom(isDateValid).withMessage('Should have a published start date that conforms to ISO 8601'), | ||
21 | query('originallyPublishedEndDate') | ||
22 | .optional() | ||
23 | .custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'), | ||
16 | 24 | ||
17 | query('durationMin').optional().isInt().withMessage('Should have a valid min duration'), | 25 | query('durationMin').optional().isInt().withMessage('Should have a valid min duration'), |
18 | query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), | 26 | query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), |
@@ -41,11 +49,12 @@ const videoChannelsListSearchValidator = [ | |||
41 | } | 49 | } |
42 | ] | 50 | ] |
43 | 51 | ||
44 | const videoChannelsOwnSearchValidator = [ | 52 | const videoPlaylistsListSearchValidator = [ |
45 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | 53 | query('search').not().isEmpty().withMessage('Should have a valid search'), |
54 | query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), | ||
46 | 55 | ||
47 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 56 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
48 | logger.debug('Checking video channels search query', { parameters: req.query }) | 57 | logger.debug('Checking video playlists search query', { parameters: req.query }) |
49 | 58 | ||
50 | if (areValidationErrors(req, res)) return | 59 | if (areValidationErrors(req, res)) return |
51 | 60 | ||
@@ -58,5 +67,5 @@ const videoChannelsOwnSearchValidator = [ | |||
58 | export { | 67 | export { |
59 | videosSearchValidator, | 68 | videosSearchValidator, |
60 | videoChannelsListSearchValidator, | 69 | videoChannelsListSearchValidator, |
61 | videoChannelsOwnSearchValidator | 70 | videoPlaylistsListSearchValidator |
62 | } | 71 | } |
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts index fe6704716..fc7239b25 100644 --- a/server/middlewares/validators/server.ts +++ b/server/middlewares/validators/server.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { logger } from '../../helpers/logger' | ||
3 | import { areValidationErrors } from './utils' | ||
4 | import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers' | ||
5 | import { ServerModel } from '../../models/server/server' | ||
6 | import { body } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
4 | import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers' | ||
7 | import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' | 5 | import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' |
8 | import { Redis } from '../../lib/redis' | 6 | import { logger } from '../../helpers/logger' |
9 | import { CONFIG, isEmailEnabled } from '../../initializers/config' | 7 | import { CONFIG, isEmailEnabled } from '../../initializers/config' |
10 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 8 | import { Redis } from '../../lib/redis' |
9 | import { ServerModel } from '../../models/server/server' | ||
10 | import { areValidationErrors } from './shared' | ||
11 | 11 | ||
12 | const serverGetValidator = [ | 12 | const serverGetValidator = [ |
13 | body('host').custom(isHostValid).withMessage('Should have a valid host'), | 13 | body('host').custom(isHostValid).withMessage('Should have a valid host'), |
@@ -19,9 +19,10 @@ const serverGetValidator = [ | |||
19 | 19 | ||
20 | const server = await ServerModel.loadByHost(req.body.host) | 20 | const server = await ServerModel.loadByHost(req.body.host) |
21 | if (!server) { | 21 | if (!server) { |
22 | return res.status(HttpStatusCode.NOT_FOUND_404) | 22 | return res.fail({ |
23 | .send({ error: 'Server host not found.' }) | 23 | status: HttpStatusCode.NOT_FOUND_404, |
24 | .end() | 24 | message: 'Server host not found.' |
25 | }) | ||
25 | } | 26 | } |
26 | 27 | ||
27 | res.locals.server = server | 28 | res.locals.server = server |
@@ -44,26 +45,26 @@ const contactAdministratorValidator = [ | |||
44 | if (areValidationErrors(req, res)) return | 45 | if (areValidationErrors(req, res)) return |
45 | 46 | ||
46 | if (CONFIG.CONTACT_FORM.ENABLED === false) { | 47 | if (CONFIG.CONTACT_FORM.ENABLED === false) { |
47 | return res | 48 | return res.fail({ |
48 | .status(HttpStatusCode.CONFLICT_409) | 49 | status: HttpStatusCode.CONFLICT_409, |
49 | .send({ error: 'Contact form is not enabled on this instance.' }) | 50 | message: 'Contact form is not enabled on this instance.' |
50 | .end() | 51 | }) |
51 | } | 52 | } |
52 | 53 | ||
53 | if (isEmailEnabled() === false) { | 54 | if (isEmailEnabled() === false) { |
54 | return res | 55 | return res.fail({ |
55 | .status(HttpStatusCode.CONFLICT_409) | 56 | status: HttpStatusCode.CONFLICT_409, |
56 | .send({ error: 'Emailer is not enabled on this instance.' }) | 57 | message: 'Emailer is not enabled on this instance.' |
57 | .end() | 58 | }) |
58 | } | 59 | } |
59 | 60 | ||
60 | if (await Redis.Instance.doesContactFormIpExist(req.ip)) { | 61 | if (await Redis.Instance.doesContactFormIpExist(req.ip)) { |
61 | logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) | 62 | logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) |
62 | 63 | ||
63 | return res | 64 | return res.fail({ |
64 | .status(HttpStatusCode.FORBIDDEN_403) | 65 | status: HttpStatusCode.FORBIDDEN_403, |
65 | .send({ error: 'You already sent a contact form recently.' }) | 66 | message: 'You already sent a contact form recently.' |
66 | .end() | 67 | }) |
67 | } | 68 | } |
68 | 69 | ||
69 | return next() | 70 | return next() |
diff --git a/server/helpers/middlewares/abuses.ts b/server/middlewares/validators/shared/abuses.ts index c53bd9efd..4a20a55fa 100644 --- a/server/helpers/middlewares/abuses.ts +++ b/server/middlewares/validators/shared/abuses.ts | |||
@@ -1,13 +1,15 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { AbuseModel } from '../../models/abuse/abuse' | 2 | import { AbuseModel } from '@server/models/abuse/abuse' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '@shared/core-utils' |
4 | 4 | ||
5 | async function doesAbuseExist (abuseId: number | string, res: Response) { | 5 | async function doesAbuseExist (abuseId: number | string, res: Response) { |
6 | const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10)) | 6 | const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10)) |
7 | 7 | ||
8 | if (!abuse) { | 8 | if (!abuse) { |
9 | res.status(HttpStatusCode.NOT_FOUND_404) | 9 | res.fail({ |
10 | .json({ error: 'Abuse not found' }) | 10 | status: HttpStatusCode.NOT_FOUND_404, |
11 | message: 'Abuse not found' | ||
12 | }) | ||
11 | 13 | ||
12 | return false | 14 | return false |
13 | } | 15 | } |
diff --git a/server/helpers/middlewares/accounts.ts b/server/middlewares/validators/shared/accounts.ts index 13ae6cdf4..04da15441 100644 --- a/server/helpers/middlewares/accounts.ts +++ b/server/middlewares/validators/shared/accounts.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { UserModel } from '@server/models/account/user' | 2 | import { AccountModel } from '@server/models/account/account' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { UserModel } from '@server/models/user/user' |
4 | import { AccountModel } from '../../models/account/account' | 4 | import { MAccountDefault } from '@server/types/models' |
5 | import { MAccountDefault } from '../../types/models' | 5 | import { HttpStatusCode } from '@shared/core-utils' |
6 | 6 | ||
7 | function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { | 7 | function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { |
8 | const promise = AccountModel.load(parseInt(id + '', 10)) | 8 | const promise = AccountModel.load(parseInt(id + '', 10)) |
@@ -27,15 +27,15 @@ async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sen | |||
27 | 27 | ||
28 | if (!account) { | 28 | if (!account) { |
29 | if (sendNotFound === true) { | 29 | if (sendNotFound === true) { |
30 | res.status(HttpStatusCode.NOT_FOUND_404) | 30 | res.fail({ |
31 | .json({ error: 'Account not found' }) | 31 | status: HttpStatusCode.NOT_FOUND_404, |
32 | message: 'Account not found' | ||
33 | }) | ||
32 | } | 34 | } |
33 | |||
34 | return false | 35 | return false |
35 | } | 36 | } |
36 | 37 | ||
37 | res.locals.account = account | 38 | res.locals.account = account |
38 | |||
39 | return true | 39 | return true |
40 | } | 40 | } |
41 | 41 | ||
@@ -43,14 +43,14 @@ async function doesUserFeedTokenCorrespond (id: number, token: string, res: Resp | |||
43 | const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) | 43 | const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) |
44 | 44 | ||
45 | if (token !== user.feedToken) { | 45 | if (token !== user.feedToken) { |
46 | res.status(HttpStatusCode.FORBIDDEN_403) | 46 | res.fail({ |
47 | .json({ error: 'User and token mismatch' }) | 47 | status: HttpStatusCode.FORBIDDEN_403, |
48 | 48 | message: 'User and token mismatch' | |
49 | }) | ||
49 | return false | 50 | return false |
50 | } | 51 | } |
51 | 52 | ||
52 | res.locals.user = user | 53 | res.locals.user = user |
53 | |||
54 | return true | 54 | return true |
55 | } | 55 | } |
56 | 56 | ||
diff --git a/server/helpers/middlewares/index.ts b/server/middlewares/validators/shared/index.ts index f57f3ad31..fa89d05f2 100644 --- a/server/helpers/middlewares/index.ts +++ b/server/middlewares/validators/shared/index.ts | |||
@@ -1,7 +1,11 @@ | |||
1 | export * from './abuses' | 1 | export * from './abuses' |
2 | export * from './accounts' | 2 | export * from './accounts' |
3 | export * from './utils' | ||
3 | export * from './video-blacklists' | 4 | export * from './video-blacklists' |
4 | export * from './video-captions' | 5 | export * from './video-captions' |
5 | export * from './video-channels' | 6 | export * from './video-channels' |
7 | export * from './video-comments' | ||
8 | export * from './video-imports' | ||
9 | export * from './video-ownerships' | ||
6 | export * from './video-playlists' | 10 | export * from './video-playlists' |
7 | export * from './videos' | 11 | export * from './videos' |
diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/shared/utils.ts index 4167f6d43..4f08560af 100644 --- a/server/middlewares/validators/utils.ts +++ b/server/middlewares/validators/shared/utils.ts | |||
@@ -1,15 +1,20 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { query, validationResult } from 'express-validator' | 2 | import { param, query, validationResult } from 'express-validator' |
3 | import { logger } from '../../helpers/logger' | 3 | import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' |
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 4 | import { logger } from '../../../helpers/logger' |
5 | 5 | ||
6 | function areValidationErrors (req: express.Request, res: express.Response) { | 6 | function areValidationErrors (req: express.Request, res: express.Response) { |
7 | const errors = validationResult(req) | 7 | const errors = validationResult(req) |
8 | 8 | ||
9 | if (!errors.isEmpty()) { | 9 | if (!errors.isEmpty()) { |
10 | logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) | 10 | logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) |
11 | res.status(HttpStatusCode.BAD_REQUEST_400) | 11 | res.fail({ |
12 | .json({ errors: errors.mapped() }) | 12 | message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '), |
13 | instance: req.originalUrl, | ||
14 | data: { | ||
15 | 'invalid-params': errors.mapped() | ||
16 | } | ||
17 | }) | ||
13 | 18 | ||
14 | return true | 19 | return true |
15 | } | 20 | } |
@@ -37,10 +42,24 @@ function createSortableColumns (sortableColumns: string[]) { | |||
37 | return sortableColumns.concat(sortableColumnDesc) | 42 | return sortableColumns.concat(sortableColumnDesc) |
38 | } | 43 | } |
39 | 44 | ||
45 | function isValidVideoIdParam (paramName: string) { | ||
46 | return param(paramName) | ||
47 | .customSanitizer(toCompleteUUID) | ||
48 | .custom(isIdOrUUIDValid).withMessage('Should have a valid video id') | ||
49 | } | ||
50 | |||
51 | function isValidPlaylistIdParam (paramName: string) { | ||
52 | return param(paramName) | ||
53 | .customSanitizer(toCompleteUUID) | ||
54 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id') | ||
55 | } | ||
56 | |||
40 | // --------------------------------------------------------------------------- | 57 | // --------------------------------------------------------------------------- |
41 | 58 | ||
42 | export { | 59 | export { |
43 | areValidationErrors, | 60 | areValidationErrors, |
44 | checkSort, | 61 | checkSort, |
45 | createSortableColumns | 62 | createSortableColumns, |
63 | isValidVideoIdParam, | ||
64 | isValidPlaylistIdParam | ||
46 | } | 65 | } |
diff --git a/server/helpers/middlewares/video-blacklists.ts b/server/middlewares/validators/shared/video-blacklists.ts index eda1324d3..01491c10f 100644 --- a/server/helpers/middlewares/video-blacklists.ts +++ b/server/middlewares/validators/shared/video-blacklists.ts | |||
@@ -1,15 +1,15 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { VideoBlacklistModel } from '../../models/video/video-blacklist' | 2 | import { VideoBlacklistModel } from '@server/models/video/video-blacklist' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '@shared/core-utils' |
4 | 4 | ||
5 | async function doesVideoBlacklistExist (videoId: number, res: Response) { | 5 | async function doesVideoBlacklistExist (videoId: number, res: Response) { |
6 | const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId) | 6 | const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId) |
7 | 7 | ||
8 | if (videoBlacklist === null) { | 8 | if (videoBlacklist === null) { |
9 | res.status(HttpStatusCode.NOT_FOUND_404) | 9 | res.fail({ |
10 | .json({ error: 'Blacklisted video not found' }) | 10 | status: HttpStatusCode.NOT_FOUND_404, |
11 | .end() | 11 | message: 'Blacklisted video not found' |
12 | 12 | }) | |
13 | return false | 13 | return false |
14 | } | 14 | } |
15 | 15 | ||
diff --git a/server/helpers/middlewares/video-captions.ts b/server/middlewares/validators/shared/video-captions.ts index 226d3c5f8..80f6c5a52 100644 --- a/server/helpers/middlewares/video-captions.ts +++ b/server/middlewares/validators/shared/video-captions.ts | |||
@@ -1,15 +1,16 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { VideoCaptionModel } from '../../models/video/video-caption' | 2 | import { VideoCaptionModel } from '@server/models/video/video-caption' |
3 | import { MVideoId } from '@server/types/models' | 3 | import { MVideoId } from '@server/types/models' |
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '@shared/core-utils' |
5 | 5 | ||
6 | async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) { | 6 | async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) { |
7 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) | 7 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) |
8 | 8 | ||
9 | if (!videoCaption) { | 9 | if (!videoCaption) { |
10 | res.status(HttpStatusCode.NOT_FOUND_404) | 10 | res.fail({ |
11 | .json({ error: 'Video caption not found' }) | 11 | status: HttpStatusCode.NOT_FOUND_404, |
12 | 12 | message: 'Video caption not found' | |
13 | }) | ||
13 | return false | 14 | return false |
14 | } | 15 | } |
15 | 16 | ||
diff --git a/server/helpers/middlewares/video-channels.ts b/server/middlewares/validators/shared/video-channels.ts index e6eab65a2..fe2e663b7 100644 --- a/server/helpers/middlewares/video-channels.ts +++ b/server/middlewares/validators/shared/video-channels.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
2 | import { MChannelBannerAccountDefault } from '@server/types/models' | 3 | import { MChannelBannerAccountDefault } from '@server/types/models' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '@shared/core-utils' |
4 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
5 | 5 | ||
6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { | 6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { |
7 | const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) | 7 | const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) |
@@ -31,9 +31,10 @@ export { | |||
31 | 31 | ||
32 | function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { | 32 | function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { |
33 | if (!videoChannel) { | 33 | if (!videoChannel) { |
34 | res.status(HttpStatusCode.NOT_FOUND_404) | 34 | res.fail({ |
35 | .json({ error: 'Video channel not found' }) | 35 | status: HttpStatusCode.NOT_FOUND_404, |
36 | 36 | message: 'Video channel not found' | |
37 | }) | ||
37 | return false | 38 | return false |
38 | } | 39 | } |
39 | 40 | ||
diff --git a/server/middlewares/validators/shared/video-comments.ts b/server/middlewares/validators/shared/video-comments.ts new file mode 100644 index 000000000..83ea15c98 --- /dev/null +++ b/server/middlewares/validators/shared/video-comments.ts | |||
@@ -0,0 +1,73 @@ | |||
1 | import * as express from 'express' | ||
2 | import { VideoCommentModel } from '@server/models/video/video-comment' | ||
3 | import { MVideoId } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '@shared/core-utils' | ||
5 | |||
6 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
7 | const id = parseInt(idArg + '', 10) | ||
8 | const videoComment = await VideoCommentModel.loadById(id) | ||
9 | |||
10 | if (!videoComment) { | ||
11 | res.fail({ | ||
12 | status: HttpStatusCode.NOT_FOUND_404, | ||
13 | message: 'Video comment thread not found' | ||
14 | }) | ||
15 | return false | ||
16 | } | ||
17 | |||
18 | if (videoComment.videoId !== video.id) { | ||
19 | res.fail({ message: 'Video comment is not associated to this video.' }) | ||
20 | return false | ||
21 | } | ||
22 | |||
23 | if (videoComment.inReplyToCommentId !== null) { | ||
24 | res.fail({ message: 'Video comment is not a thread.' }) | ||
25 | return false | ||
26 | } | ||
27 | |||
28 | res.locals.videoCommentThread = videoComment | ||
29 | return true | ||
30 | } | ||
31 | |||
32 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { | ||
33 | const id = parseInt(idArg + '', 10) | ||
34 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
35 | |||
36 | if (!videoComment) { | ||
37 | res.fail({ | ||
38 | status: HttpStatusCode.NOT_FOUND_404, | ||
39 | message: 'Video comment thread not found' | ||
40 | }) | ||
41 | return false | ||
42 | } | ||
43 | |||
44 | if (videoComment.videoId !== video.id) { | ||
45 | res.fail({ message: 'Video comment is not associated to this video.' }) | ||
46 | return false | ||
47 | } | ||
48 | |||
49 | res.locals.videoCommentFull = videoComment | ||
50 | return true | ||
51 | } | ||
52 | |||
53 | async function doesCommentIdExist (idArg: number | string, res: express.Response) { | ||
54 | const id = parseInt(idArg + '', 10) | ||
55 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | ||
56 | |||
57 | if (!videoComment) { | ||
58 | res.fail({ | ||
59 | status: HttpStatusCode.NOT_FOUND_404, | ||
60 | message: 'Video comment thread not found' | ||
61 | }) | ||
62 | return false | ||
63 | } | ||
64 | |||
65 | res.locals.videoCommentFull = videoComment | ||
66 | return true | ||
67 | } | ||
68 | |||
69 | export { | ||
70 | doesVideoCommentThreadExist, | ||
71 | doesVideoCommentExist, | ||
72 | doesCommentIdExist | ||
73 | } | ||
diff --git a/server/middlewares/validators/shared/video-imports.ts b/server/middlewares/validators/shared/video-imports.ts new file mode 100644 index 000000000..0f984bc17 --- /dev/null +++ b/server/middlewares/validators/shared/video-imports.ts | |||
@@ -0,0 +1,22 @@ | |||
1 | import * as express from 'express' | ||
2 | import { VideoImportModel } from '@server/models/video/video-import' | ||
3 | import { HttpStatusCode } from '@shared/core-utils' | ||
4 | |||
5 | async function doesVideoImportExist (id: number, res: express.Response) { | ||
6 | const videoImport = await VideoImportModel.loadAndPopulateVideo(id) | ||
7 | |||
8 | if (!videoImport) { | ||
9 | res.fail({ | ||
10 | status: HttpStatusCode.NOT_FOUND_404, | ||
11 | message: 'Video import not found' | ||
12 | }) | ||
13 | return false | ||
14 | } | ||
15 | |||
16 | res.locals.videoImport = videoImport | ||
17 | return true | ||
18 | } | ||
19 | |||
20 | export { | ||
21 | doesVideoImportExist | ||
22 | } | ||
diff --git a/server/middlewares/validators/shared/video-ownerships.ts b/server/middlewares/validators/shared/video-ownerships.ts new file mode 100644 index 000000000..fc27006ce --- /dev/null +++ b/server/middlewares/validators/shared/video-ownerships.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import * as express from 'express' | ||
2 | import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership' | ||
3 | import { HttpStatusCode } from '@shared/core-utils' | ||
4 | |||
5 | async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) { | ||
6 | const id = parseInt(idArg + '', 10) | ||
7 | const videoChangeOwnership = await VideoChangeOwnershipModel.load(id) | ||
8 | |||
9 | if (!videoChangeOwnership) { | ||
10 | res.fail({ | ||
11 | status: HttpStatusCode.NOT_FOUND_404, | ||
12 | message: 'Video change ownership not found' | ||
13 | }) | ||
14 | return false | ||
15 | } | ||
16 | |||
17 | res.locals.videoChangeOwnership = videoChangeOwnership | ||
18 | |||
19 | return true | ||
20 | } | ||
21 | |||
22 | export { | ||
23 | doesChangeVideoOwnershipExist | ||
24 | } | ||
diff --git a/server/helpers/middlewares/video-playlists.ts b/server/middlewares/validators/shared/video-playlists.ts index d2dd80a35..d762859a8 100644 --- a/server/helpers/middlewares/video-playlists.ts +++ b/server/middlewares/validators/shared/video-playlists.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
3 | import { MVideoPlaylist } from '../../types/models/video/video-playlist' | 3 | import { MVideoPlaylist } from '@server/types/models' |
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 4 | import { HttpStatusCode } from '@shared/core-utils' |
5 | 5 | ||
6 | export type VideoPlaylistFetchType = 'summary' | 'all' | 6 | export type VideoPlaylistFetchType = 'summary' | 'all' |
7 | async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') { | 7 | async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') { |
@@ -28,10 +28,10 @@ export { | |||
28 | 28 | ||
29 | function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) { | 29 | function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) { |
30 | if (!videoPlaylist) { | 30 | if (!videoPlaylist) { |
31 | res.status(HttpStatusCode.NOT_FOUND_404) | 31 | res.fail({ |
32 | .json({ error: 'Video playlist not found' }) | 32 | status: HttpStatusCode.NOT_FOUND_404, |
33 | .end() | 33 | message: 'Video playlist not found' |
34 | 34 | }) | |
35 | return false | 35 | return false |
36 | } | 36 | } |
37 | 37 | ||
diff --git a/server/helpers/middlewares/videos.ts b/server/middlewares/validators/shared/videos.ts index 403cae092..2c66c1a3a 100644 --- a/server/helpers/middlewares/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -1,34 +1,38 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { fetchVideo, VideoFetchType } from '../video' | 2 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' |
3 | import { UserRight } from '../../../shared/models/users' | 3 | import { VideoChannelModel } from '@server/models/video/video-channel' |
4 | import { VideoChannelModel } from '../../models/video/video-channel' | 4 | import { VideoFileModel } from '@server/models/video/video-file' |
5 | import { | 5 | import { |
6 | MUser, | 6 | MUser, |
7 | MUserAccountId, | 7 | MUserAccountId, |
8 | MVideoAccountLight, | 8 | MVideoAccountLight, |
9 | MVideoFormattableDetails, | ||
9 | MVideoFullLight, | 10 | MVideoFullLight, |
10 | MVideoIdThumbnail, | 11 | MVideoId, |
11 | MVideoImmutable, | 12 | MVideoImmutable, |
12 | MVideoThumbnail, | 13 | MVideoThumbnail |
13 | MVideoWithRights | ||
14 | } from '@server/types/models' | 14 | } from '@server/types/models' |
15 | import { VideoFileModel } from '@server/models/video/video-file' | 15 | import { HttpStatusCode } from '@shared/core-utils' |
16 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 16 | import { UserRight } from '@shared/models' |
17 | 17 | ||
18 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') { | 18 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { |
19 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 19 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined |
20 | 20 | ||
21 | const video = await fetchVideo(id, fetchType, userId) | 21 | const video = await loadVideo(id, fetchType, userId) |
22 | 22 | ||
23 | if (video === null) { | 23 | if (video === null) { |
24 | res.status(HttpStatusCode.NOT_FOUND_404) | 24 | res.fail({ |
25 | .json({ error: 'Video not found' }) | 25 | status: HttpStatusCode.NOT_FOUND_404, |
26 | .end() | 26 | message: 'Video not found' |
27 | 27 | }) | |
28 | return false | 28 | return false |
29 | } | 29 | } |
30 | 30 | ||
31 | switch (fetchType) { | 31 | switch (fetchType) { |
32 | case 'for-api': | ||
33 | res.locals.videoAPI = video as MVideoFormattableDetails | ||
34 | break | ||
35 | |||
32 | case 'all': | 36 | case 'all': |
33 | res.locals.videoAll = video as MVideoFullLight | 37 | res.locals.videoAll = video as MVideoFullLight |
34 | break | 38 | break |
@@ -38,16 +42,12 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi | |||
38 | break | 42 | break |
39 | 43 | ||
40 | case 'id': | 44 | case 'id': |
41 | res.locals.videoId = video as MVideoIdThumbnail | 45 | res.locals.videoId = video as MVideoId |
42 | break | 46 | break |
43 | 47 | ||
44 | case 'only-video': | 48 | case 'only-video': |
45 | res.locals.onlyVideo = video as MVideoThumbnail | 49 | res.locals.onlyVideo = video as MVideoThumbnail |
46 | break | 50 | break |
47 | |||
48 | case 'only-video-with-rights': | ||
49 | res.locals.onlyVideoWithRights = video as MVideoWithRights | ||
50 | break | ||
51 | } | 51 | } |
52 | 52 | ||
53 | return true | 53 | return true |
@@ -55,10 +55,10 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi | |||
55 | 55 | ||
56 | async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { | 56 | async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { |
57 | if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { | 57 | if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { |
58 | res.status(HttpStatusCode.NOT_FOUND_404) | 58 | res.fail({ |
59 | .json({ error: 'VideoFile matching Video not found' }) | 59 | status: HttpStatusCode.NOT_FOUND_404, |
60 | .end() | 60 | message: 'VideoFile matching Video not found' |
61 | 61 | }) | |
62 | return false | 62 | return false |
63 | } | 63 | } |
64 | 64 | ||
@@ -69,9 +69,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc | |||
69 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) | 69 | const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) |
70 | 70 | ||
71 | if (videoChannel === null) { | 71 | if (videoChannel === null) { |
72 | res.status(HttpStatusCode.BAD_REQUEST_400) | 72 | res.fail({ message: 'Unknown video "video channel" for this instance.' }) |
73 | .json({ error: 'Unknown video "video channel" for this instance.' }) | ||
74 | |||
75 | return false | 73 | return false |
76 | } | 74 | } |
77 | 75 | ||
@@ -82,9 +80,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc | |||
82 | } | 80 | } |
83 | 81 | ||
84 | if (videoChannel.Account.id !== user.Account.id) { | 82 | if (videoChannel.Account.id !== user.Account.id) { |
85 | res.status(HttpStatusCode.BAD_REQUEST_400) | 83 | res.fail({ |
86 | .json({ error: 'Unknown video "video channel" for this account.' }) | 84 | message: 'Unknown video "video channel" for this account.' |
87 | 85 | }) | |
88 | return false | 86 | return false |
89 | } | 87 | } |
90 | 88 | ||
@@ -95,9 +93,10 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc | |||
95 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { | 93 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { |
96 | // Retrieve the user who did the request | 94 | // Retrieve the user who did the request |
97 | if (onlyOwned && video.isOwned() === false) { | 95 | if (onlyOwned && video.isOwned() === false) { |
98 | res.status(HttpStatusCode.FORBIDDEN_403) | 96 | res.fail({ |
99 | .json({ error: 'Cannot manage a video of another server.' }) | 97 | status: HttpStatusCode.FORBIDDEN_403, |
100 | .end() | 98 | message: 'Cannot manage a video of another server.' |
99 | }) | ||
101 | return false | 100 | return false |
102 | } | 101 | } |
103 | 102 | ||
@@ -106,9 +105,10 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: | |||
106 | // Or if s/he is the video's account | 105 | // Or if s/he is the video's account |
107 | const account = video.VideoChannel.Account | 106 | const account = video.VideoChannel.Account |
108 | if (user.hasRight(right) === false && account.userId !== user.id) { | 107 | if (user.hasRight(right) === false && account.userId !== user.id) { |
109 | res.status(HttpStatusCode.FORBIDDEN_403) | 108 | res.fail({ |
110 | .json({ error: 'Cannot manage a video of another user.' }) | 109 | status: HttpStatusCode.FORBIDDEN_403, |
111 | .end() | 110 | message: 'Cannot manage a video of another user.' |
111 | }) | ||
112 | return false | 112 | return false |
113 | } | 113 | } |
114 | 114 | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index beecc155b..473010460 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { SORTABLE_COLUMNS } from '../../initializers/constants' | 1 | import { SORTABLE_COLUMNS } from '../../initializers/constants' |
2 | import { checkSort, createSortableColumns } from './utils' | 2 | import { checkSort, createSortableColumns } from './shared' |
3 | 3 | ||
4 | // Initialize constants here for better performances | 4 | // Initialize constants here for better performances |
5 | const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) | 5 | const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) |
@@ -9,6 +9,7 @@ const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES) | |||
9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) | 9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) |
10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) | 10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) |
11 | const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) | 11 | const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) |
12 | const SORTABLE_VIDEO_PLAYLISTS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH) | ||
12 | const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) | 13 | const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) |
13 | const SORTABLE_VIDEO_COMMENTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | 14 | const SORTABLE_VIDEO_COMMENTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) |
14 | const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | 15 | const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) |
@@ -34,6 +35,7 @@ const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | |||
34 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) | 35 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) |
35 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) | 36 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) |
36 | const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS) | 37 | const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS) |
38 | const videoPlaylistsSearchSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_SEARCH_COLUMNS) | ||
37 | const videoCommentsValidator = checkSort(SORTABLE_VIDEO_COMMENTS_COLUMNS) | 39 | const videoCommentsValidator = checkSort(SORTABLE_VIDEO_COMMENTS_COLUMNS) |
38 | const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) | 40 | const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) |
39 | const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS) | 41 | const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS) |
@@ -75,5 +77,6 @@ export { | |||
75 | userNotificationsSortValidator, | 77 | userNotificationsSortValidator, |
76 | videoPlaylistsSortValidator, | 78 | videoPlaylistsSortValidator, |
77 | videoRedundanciesSortValidator, | 79 | videoRedundanciesSortValidator, |
80 | videoPlaylistsSearchSortValidator, | ||
78 | pluginsSortValidator | 81 | pluginsSortValidator |
79 | } | 82 | } |
diff --git a/server/middlewares/validators/themes.ts b/server/middlewares/validators/themes.ts index a726a567b..d4716257f 100644 --- a/server/middlewares/validators/themes.ts +++ b/server/middlewares/validators/themes.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { param } from 'express-validator' | 2 | import { param } from 'express-validator' |
3 | import { logger } from '../../helpers/logger' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { areValidationErrors } from './utils' | 4 | import { isSafePath } from '../../helpers/custom-validators/misc' |
5 | import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' | 5 | import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' |
6 | import { logger } from '../../helpers/logger' | ||
6 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 7 | import { PluginManager } from '../../lib/plugins/plugin-manager' |
7 | import { isSafePath } from '../../helpers/custom-validators/misc' | 8 | import { areValidationErrors } from './shared' |
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
9 | 9 | ||
10 | const serveThemeCSSValidator = [ | 10 | const serveThemeCSSValidator = [ |
11 | param('themeName').custom(isPluginNameValid).withMessage('Should have a valid theme name'), | 11 | param('themeName').custom(isPluginNameValid).withMessage('Should have a valid theme name'), |
@@ -20,11 +20,17 @@ const serveThemeCSSValidator = [ | |||
20 | const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName) | 20 | const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName) |
21 | 21 | ||
22 | if (!theme || theme.version !== req.params.themeVersion) { | 22 | if (!theme || theme.version !== req.params.themeVersion) { |
23 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 23 | return res.fail({ |
24 | status: HttpStatusCode.NOT_FOUND_404, | ||
25 | message: 'No theme named ' + req.params.themeName + ' was found with version ' + req.params.themeVersion | ||
26 | }) | ||
24 | } | 27 | } |
25 | 28 | ||
26 | if (theme.css.includes(req.params.staticEndpoint) === false) { | 29 | if (theme.css.includes(req.params.staticEndpoint) === false) { |
27 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 30 | return res.fail({ |
31 | status: HttpStatusCode.NOT_FOUND_404, | ||
32 | message: 'No static endpoint was found for this theme' | ||
33 | }) | ||
28 | } | 34 | } |
29 | 35 | ||
30 | res.locals.registeredPlugin = theme | 36 | res.locals.registeredPlugin = theme |
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts index 058bf7758..1db0d9b26 100644 --- a/server/middlewares/validators/user-history.ts +++ b/server/middlewares/validators/user-history.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, query } from 'express-validator' | 2 | import { body, query } from 'express-validator' |
3 | import { logger } from '../../helpers/logger' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { exists, isDateValid } from '../../helpers/custom-validators/misc' | 3 | import { exists, isDateValid } from '../../helpers/custom-validators/misc' |
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './shared' | ||
6 | 6 | ||
7 | const userHistoryListValidator = [ | 7 | const userHistoryListValidator = [ |
8 | query('search') | 8 | query('search') |
@@ -21,7 +21,7 @@ const userHistoryListValidator = [ | |||
21 | const userHistoryRemoveValidator = [ | 21 | const userHistoryRemoveValidator = [ |
22 | body('beforeDate') | 22 | body('beforeDate') |
23 | .optional() | 23 | .optional() |
24 | .custom(isDateValid).withMessage('Should have a valid before date'), | 24 | .custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'), |
25 | 25 | ||
26 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 26 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
27 | logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body }) | 27 | logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body }) |
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts index 21a7be08d..2f8e7686e 100644 --- a/server/middlewares/validators/user-notifications.ts +++ b/server/middlewares/validators/user-notifications.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, query } from 'express-validator' | 2 | import { body, query } from 'express-validator' |
3 | import { logger } from '../../helpers/logger' | ||
4 | import { areValidationErrors } from './utils' | ||
5 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
6 | import { isNotEmptyIntArray, toBooleanOrNull } from '../../helpers/custom-validators/misc' | 3 | import { isNotEmptyIntArray, toBooleanOrNull } from '../../helpers/custom-validators/misc' |
4 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { areValidationErrors } from './shared' | ||
7 | 7 | ||
8 | const listUserNotificationsValidator = [ | 8 | const listUserNotificationsValidator = [ |
9 | query('unread') | 9 | query('unread') |
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts index 0d0c8ccbf..ab7962923 100644 --- a/server/middlewares/validators/user-subscriptions.ts +++ b/server/middlewares/validators/user-subscriptions.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { logger } from '../../helpers/logger' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { areValidationErrors } from './utils' | ||
5 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | ||
6 | import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | 4 | import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' |
7 | import { toArray } from '../../helpers/custom-validators/misc' | 5 | import { toArray } from '../../helpers/custom-validators/misc' |
6 | import { logger } from '../../helpers/logger' | ||
8 | import { WEBSERVER } from '../../initializers/constants' | 7 | import { WEBSERVER } from '../../initializers/constants' |
9 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 8 | import { ActorFollowModel } from '../../models/actor/actor-follow' |
9 | import { areValidationErrors } from './shared' | ||
10 | 10 | ||
11 | const userSubscriptionListValidator = [ | 11 | const userSubscriptionListValidator = [ |
12 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | 12 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), |
@@ -61,11 +61,10 @@ const userSubscriptionGetValidator = [ | |||
61 | const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) | 61 | const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) |
62 | 62 | ||
63 | if (!subscription || !subscription.ActorFollowing.VideoChannel) { | 63 | if (!subscription || !subscription.ActorFollowing.VideoChannel) { |
64 | return res | 64 | return res.fail({ |
65 | .status(HttpStatusCode.NOT_FOUND_404) | 65 | status: HttpStatusCode.NOT_FOUND_404, |
66 | .json({ | 66 | message: `Subscription ${req.params.uri} not found.` |
67 | error: `Subscription ${req.params.uri} not found.` | 67 | }) |
68 | }) | ||
69 | } | 68 | } |
70 | 69 | ||
71 | res.locals.subscription = subscription | 70 | res.locals.subscription = subscription |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 37119e279..698d7d814 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -7,7 +7,7 @@ import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-code | |||
7 | import { UserRole } from '../../../shared/models/users' | 7 | import { UserRole } from '../../../shared/models/users' |
8 | import { UserRegister } from '../../../shared/models/users/user-register.model' | 8 | import { UserRegister } from '../../../shared/models/users/user-register.model' |
9 | import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' | 9 | import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' |
10 | import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 10 | import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
11 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 11 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
12 | import { | 12 | import { |
13 | isNoInstanceConfigWarningModal, | 13 | isNoInstanceConfigWarningModal, |
@@ -30,13 +30,12 @@ import { | |||
30 | } from '../../helpers/custom-validators/users' | 30 | } from '../../helpers/custom-validators/users' |
31 | import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' | 31 | import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' |
32 | import { logger } from '../../helpers/logger' | 32 | import { logger } from '../../helpers/logger' |
33 | import { doesVideoExist } from '../../helpers/middlewares' | ||
34 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' | ||
35 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | 33 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' |
36 | import { Redis } from '../../lib/redis' | 34 | import { Redis } from '../../lib/redis' |
37 | import { UserModel } from '../../models/account/user' | 35 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' |
38 | import { ActorModel } from '../../models/activitypub/actor' | 36 | import { ActorModel } from '../../models/actor/actor' |
39 | import { areValidationErrors } from './utils' | 37 | import { UserModel } from '../../models/user/user' |
38 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared' | ||
40 | 39 | ||
41 | const usersListValidator = [ | 40 | const usersListValidator = [ |
42 | query('blocked') | 41 | query('blocked') |
@@ -73,23 +72,23 @@ const usersAddValidator = [ | |||
73 | 72 | ||
74 | const authUser = res.locals.oauth.token.User | 73 | const authUser = res.locals.oauth.token.User |
75 | if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { | 74 | if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { |
76 | return res | 75 | return res.fail({ |
77 | .status(HttpStatusCode.FORBIDDEN_403) | 76 | status: HttpStatusCode.FORBIDDEN_403, |
78 | .json({ error: 'You can only create users (and not administrators or moderators)' }) | 77 | message: 'You can only create users (and not administrators or moderators)' |
78 | }) | ||
79 | } | 79 | } |
80 | 80 | ||
81 | if (req.body.channelName) { | 81 | if (req.body.channelName) { |
82 | if (req.body.channelName === req.body.username) { | 82 | if (req.body.channelName === req.body.username) { |
83 | return res | 83 | return res.fail({ message: 'Channel name cannot be the same as user username.' }) |
84 | .status(HttpStatusCode.BAD_REQUEST_400) | ||
85 | .json({ error: 'Channel name cannot be the same as user username.' }) | ||
86 | } | 84 | } |
87 | 85 | ||
88 | const existing = await ActorModel.loadLocalByName(req.body.channelName) | 86 | const existing = await ActorModel.loadLocalByName(req.body.channelName) |
89 | if (existing) { | 87 | if (existing) { |
90 | return res | 88 | return res.fail({ |
91 | .status(HttpStatusCode.CONFLICT_409) | 89 | status: HttpStatusCode.CONFLICT_409, |
92 | .json({ error: `Channel with name ${req.body.channelName} already exists.` }) | 90 | message: `Channel with name ${req.body.channelName} already exists.` |
91 | }) | ||
93 | } | 92 | } |
94 | } | 93 | } |
95 | 94 | ||
@@ -121,20 +120,19 @@ const usersRegisterValidator = [ | |||
121 | const body: UserRegister = req.body | 120 | const body: UserRegister = req.body |
122 | if (body.channel) { | 121 | if (body.channel) { |
123 | if (!body.channel.name || !body.channel.displayName) { | 122 | if (!body.channel.name || !body.channel.displayName) { |
124 | return res | 123 | return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) |
125 | .status(HttpStatusCode.BAD_REQUEST_400) | ||
126 | .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' }) | ||
127 | } | 124 | } |
128 | 125 | ||
129 | if (body.channel.name === body.username) { | 126 | if (body.channel.name === body.username) { |
130 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 127 | return res.fail({ message: 'Channel name cannot be the same as user username.' }) |
131 | .json({ error: 'Channel name cannot be the same as user username.' }) | ||
132 | } | 128 | } |
133 | 129 | ||
134 | const existing = await ActorModel.loadLocalByName(body.channel.name) | 130 | const existing = await ActorModel.loadLocalByName(body.channel.name) |
135 | if (existing) { | 131 | if (existing) { |
136 | return res.status(HttpStatusCode.CONFLICT_409) | 132 | return res.fail({ |
137 | .json({ error: `Channel with name ${body.channel.name} already exists.` }) | 133 | status: HttpStatusCode.CONFLICT_409, |
134 | message: `Channel with name ${body.channel.name} already exists.` | ||
135 | }) | ||
138 | } | 136 | } |
139 | } | 137 | } |
140 | 138 | ||
@@ -153,8 +151,7 @@ const usersRemoveValidator = [ | |||
153 | 151 | ||
154 | const user = res.locals.user | 152 | const user = res.locals.user |
155 | if (user.username === 'root') { | 153 | if (user.username === 'root') { |
156 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 154 | return res.fail({ message: 'Cannot remove the root user' }) |
157 | .json({ error: 'Cannot remove the root user' }) | ||
158 | } | 155 | } |
159 | 156 | ||
160 | return next() | 157 | return next() |
@@ -173,8 +170,7 @@ const usersBlockingValidator = [ | |||
173 | 170 | ||
174 | const user = res.locals.user | 171 | const user = res.locals.user |
175 | if (user.username === 'root') { | 172 | if (user.username === 'root') { |
176 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 173 | return res.fail({ message: 'Cannot block the root user' }) |
177 | .json({ error: 'Cannot block the root user' }) | ||
178 | } | 174 | } |
179 | 175 | ||
180 | return next() | 176 | return next() |
@@ -185,9 +181,7 @@ const deleteMeValidator = [ | |||
185 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 181 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
186 | const user = res.locals.oauth.token.User | 182 | const user = res.locals.oauth.token.User |
187 | if (user.username === 'root') { | 183 | if (user.username === 'root') { |
188 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 184 | return res.fail({ message: 'You cannot delete your root account.' }) |
189 | .json({ error: 'You cannot delete your root account.' }) | ||
190 | .end() | ||
191 | } | 185 | } |
192 | 186 | ||
193 | return next() | 187 | return next() |
@@ -217,8 +211,7 @@ const usersUpdateValidator = [ | |||
217 | 211 | ||
218 | const user = res.locals.user | 212 | const user = res.locals.user |
219 | if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { | 213 | if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { |
220 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 214 | return res.fail({ message: 'Cannot change root role.' }) |
221 | .json({ error: 'Cannot change root role.' }) | ||
222 | } | 215 | } |
223 | 216 | ||
224 | return next() | 217 | return next() |
@@ -273,18 +266,18 @@ const usersUpdateMeValidator = [ | |||
273 | 266 | ||
274 | if (req.body.password || req.body.email) { | 267 | if (req.body.password || req.body.email) { |
275 | if (user.pluginAuth !== null) { | 268 | if (user.pluginAuth !== null) { |
276 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 269 | return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' }) |
277 | .json({ error: 'You cannot update your email or password that is associated with an external auth system.' }) | ||
278 | } | 270 | } |
279 | 271 | ||
280 | if (!req.body.currentPassword) { | 272 | if (!req.body.currentPassword) { |
281 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 273 | return res.fail({ message: 'currentPassword parameter is missing.' }) |
282 | .json({ error: 'currentPassword parameter is missing.' }) | ||
283 | } | 274 | } |
284 | 275 | ||
285 | if (await user.isPasswordMatch(req.body.currentPassword) !== true) { | 276 | if (await user.isPasswordMatch(req.body.currentPassword) !== true) { |
286 | return res.status(HttpStatusCode.UNAUTHORIZED_401) | 277 | return res.fail({ |
287 | .json({ error: 'currentPassword is invalid.' }) | 278 | status: HttpStatusCode.UNAUTHORIZED_401, |
279 | message: 'currentPassword is invalid.' | ||
280 | }) | ||
288 | } | 281 | } |
289 | } | 282 | } |
290 | 283 | ||
@@ -309,7 +302,7 @@ const usersGetValidator = [ | |||
309 | ] | 302 | ] |
310 | 303 | ||
311 | const usersVideoRatingValidator = [ | 304 | const usersVideoRatingValidator = [ |
312 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 305 | isValidVideoIdParam('videoId'), |
313 | 306 | ||
314 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 307 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
315 | logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) | 308 | logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) |
@@ -335,8 +328,10 @@ const ensureUserRegistrationAllowed = [ | |||
335 | ) | 328 | ) |
336 | 329 | ||
337 | if (allowedResult.allowed === false) { | 330 | if (allowedResult.allowed === false) { |
338 | return res.status(HttpStatusCode.FORBIDDEN_403) | 331 | return res.fail({ |
339 | .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' }) | 332 | status: HttpStatusCode.FORBIDDEN_403, |
333 | message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' | ||
334 | }) | ||
340 | } | 335 | } |
341 | 336 | ||
342 | return next() | 337 | return next() |
@@ -348,8 +343,10 @@ const ensureUserRegistrationAllowedForIP = [ | |||
348 | const allowed = isSignupAllowedForCurrentIP(req.ip) | 343 | const allowed = isSignupAllowedForCurrentIP(req.ip) |
349 | 344 | ||
350 | if (allowed === false) { | 345 | if (allowed === false) { |
351 | return res.status(HttpStatusCode.FORBIDDEN_403) | 346 | return res.fail({ |
352 | .json({ error: 'You are not on a network authorized for registration.' }) | 347 | status: HttpStatusCode.FORBIDDEN_403, |
348 | message: 'You are not on a network authorized for registration.' | ||
349 | }) | ||
353 | } | 350 | } |
354 | 351 | ||
355 | return next() | 352 | return next() |
@@ -390,9 +387,10 @@ const usersResetPasswordValidator = [ | |||
390 | const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) | 387 | const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) |
391 | 388 | ||
392 | if (redisVerificationString !== req.body.verificationString) { | 389 | if (redisVerificationString !== req.body.verificationString) { |
393 | return res | 390 | return res.fail({ |
394 | .status(HttpStatusCode.FORBIDDEN_403) | 391 | status: HttpStatusCode.FORBIDDEN_403, |
395 | .json({ error: 'Invalid verification string.' }) | 392 | message: 'Invalid verification string.' |
393 | }) | ||
396 | } | 394 | } |
397 | 395 | ||
398 | return next() | 396 | return next() |
@@ -437,9 +435,10 @@ const usersVerifyEmailValidator = [ | |||
437 | const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) | 435 | const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) |
438 | 436 | ||
439 | if (redisVerificationString !== req.body.verificationString) { | 437 | if (redisVerificationString !== req.body.verificationString) { |
440 | return res | 438 | return res.fail({ |
441 | .status(HttpStatusCode.FORBIDDEN_403) | 439 | status: HttpStatusCode.FORBIDDEN_403, |
442 | .json({ error: 'Invalid verification string.' }) | 440 | message: 'Invalid verification string.' |
441 | }) | ||
443 | } | 442 | } |
444 | 443 | ||
445 | return next() | 444 | return next() |
@@ -455,8 +454,10 @@ const ensureAuthUserOwnsAccountValidator = [ | |||
455 | const user = res.locals.oauth.token.User | 454 | const user = res.locals.oauth.token.User |
456 | 455 | ||
457 | if (res.locals.account.id !== user.Account.id) { | 456 | if (res.locals.account.id !== user.Account.id) { |
458 | return res.status(HttpStatusCode.FORBIDDEN_403) | 457 | return res.fail({ |
459 | .json({ error: 'Only owner can access ratings list.' }) | 458 | status: HttpStatusCode.FORBIDDEN_403, |
459 | message: 'Only owner can access ratings list.' | ||
460 | }) | ||
460 | } | 461 | } |
461 | 462 | ||
462 | return next() | 463 | return next() |
@@ -471,8 +472,10 @@ const ensureCanManageUser = [ | |||
471 | if (authUser.role === UserRole.ADMINISTRATOR) return next() | 472 | if (authUser.role === UserRole.ADMINISTRATOR) return next() |
472 | if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next() | 473 | if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next() |
473 | 474 | ||
474 | return res.status(HttpStatusCode.FORBIDDEN_403) | 475 | return res.fail({ |
475 | .json({ error: 'A moderator can only manager users.' }) | 476 | status: HttpStatusCode.FORBIDDEN_403, |
477 | message: 'A moderator can only manager users.' | ||
478 | }) | ||
476 | } | 479 | } |
477 | ] | 480 | ] |
478 | 481 | ||
@@ -515,15 +518,19 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: | |||
515 | const user = await UserModel.loadByUsernameOrEmail(username, email) | 518 | const user = await UserModel.loadByUsernameOrEmail(username, email) |
516 | 519 | ||
517 | if (user) { | 520 | if (user) { |
518 | res.status(HttpStatusCode.CONFLICT_409) | 521 | res.fail({ |
519 | .json({ error: 'User with this username or email already exists.' }) | 522 | status: HttpStatusCode.CONFLICT_409, |
523 | message: 'User with this username or email already exists.' | ||
524 | }) | ||
520 | return false | 525 | return false |
521 | } | 526 | } |
522 | 527 | ||
523 | const actor = await ActorModel.loadLocalByName(username) | 528 | const actor = await ActorModel.loadLocalByName(username) |
524 | if (actor) { | 529 | if (actor) { |
525 | res.status(HttpStatusCode.CONFLICT_409) | 530 | res.fail({ |
526 | .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) | 531 | status: HttpStatusCode.CONFLICT_409, |
532 | message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' | ||
533 | }) | ||
527 | return false | 534 | return false |
528 | } | 535 | } |
529 | 536 | ||
@@ -535,14 +542,15 @@ async function checkUserExist (finder: () => Promise<MUserDefault>, res: express | |||
535 | 542 | ||
536 | if (!user) { | 543 | if (!user) { |
537 | if (abortResponse === true) { | 544 | if (abortResponse === true) { |
538 | res.status(HttpStatusCode.NOT_FOUND_404) | 545 | res.fail({ |
539 | .json({ error: 'User not found' }) | 546 | status: HttpStatusCode.NOT_FOUND_404, |
547 | message: 'User not found' | ||
548 | }) | ||
540 | } | 549 | } |
541 | 550 | ||
542 | return false | 551 | return false |
543 | } | 552 | } |
544 | 553 | ||
545 | res.locals.user = user | 554 | res.locals.user = user |
546 | |||
547 | return true | 555 | return true |
548 | } | 556 | } |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index 1eabada0a..369c2c9b6 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -3,6 +3,8 @@ export * from './video-captions' | |||
3 | export * from './video-channels' | 3 | export * from './video-channels' |
4 | export * from './video-comments' | 4 | export * from './video-comments' |
5 | export * from './video-imports' | 5 | export * from './video-imports' |
6 | export * from './video-live' | ||
7 | export * from './video-ownership-changes' | ||
6 | export * from './video-watch' | 8 | export * from './video-watch' |
7 | export * from './video-rates' | 9 | export * from './video-rates' |
8 | export * from './video-shares' | 10 | export * from './video-shares' |
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts index 88c788a43..21141d84d 100644 --- a/server/middlewares/validators/videos/video-blacklist.ts +++ b/server/middlewares/validators/videos/video-blacklist.ts | |||
@@ -1,14 +1,13 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, query } from 'express-validator' |
3 | import { isBooleanValid, isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 3 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
4 | import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
4 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist' | 5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist' |
5 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
6 | import { doesVideoBlacklistExist, doesVideoExist } from '../../../helpers/middlewares' | 7 | import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist, isValidVideoIdParam } from '../shared' |
7 | import { areValidationErrors } from '../utils' | ||
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
9 | 8 | ||
10 | const videosBlacklistRemoveValidator = [ | 9 | const videosBlacklistRemoveValidator = [ |
11 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 10 | isValidVideoIdParam('videoId'), |
12 | 11 | ||
13 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 12 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
14 | logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) | 13 | logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) |
@@ -22,7 +21,8 @@ const videosBlacklistRemoveValidator = [ | |||
22 | ] | 21 | ] |
23 | 22 | ||
24 | const videosBlacklistAddValidator = [ | 23 | const videosBlacklistAddValidator = [ |
25 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 24 | isValidVideoIdParam('videoId'), |
25 | |||
26 | body('unfederate') | 26 | body('unfederate') |
27 | .optional() | 27 | .optional() |
28 | .customSanitizer(toBooleanOrNull) | 28 | .customSanitizer(toBooleanOrNull) |
@@ -39,10 +39,10 @@ const videosBlacklistAddValidator = [ | |||
39 | 39 | ||
40 | const video = res.locals.videoAll | 40 | const video = res.locals.videoAll |
41 | if (req.body.unfederate === true && video.remote === true) { | 41 | if (req.body.unfederate === true && video.remote === true) { |
42 | return res | 42 | return res.fail({ |
43 | .status(HttpStatusCode.CONFLICT_409) | 43 | status: HttpStatusCode.CONFLICT_409, |
44 | .send({ error: 'You cannot unfederate a remote video.' }) | 44 | message: 'You cannot unfederate a remote video.' |
45 | .end() | 45 | }) |
46 | } | 46 | } |
47 | 47 | ||
48 | return next() | 48 | return next() |
@@ -50,7 +50,8 @@ const videosBlacklistAddValidator = [ | |||
50 | ] | 50 | ] |
51 | 51 | ||
52 | const videosBlacklistUpdateValidator = [ | 52 | const videosBlacklistUpdateValidator = [ |
53 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 53 | isValidVideoIdParam('videoId'), |
54 | |||
54 | body('reason') | 55 | body('reason') |
55 | .optional() | 56 | .optional() |
56 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), | 57 | .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), |
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index 872d9c2ab..2946f3e15 100644 --- a/server/middlewares/validators/videos/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts | |||
@@ -1,17 +1,18 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { areValidationErrors } from '../utils' | ||
3 | import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' | ||
4 | import { body, param } from 'express-validator' | 2 | import { body, param } from 'express-validator' |
5 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' | ||
6 | import { UserRight } from '../../../../shared' | 3 | import { UserRight } from '../../../../shared' |
7 | import { logger } from '../../../helpers/logger' | ||
8 | import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' | 4 | import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' |
9 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 5 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
10 | import { checkUserCanManageVideo, doesVideoCaptionExist, doesVideoExist } from '../../../helpers/middlewares' | 6 | import { logger } from '../../../helpers/logger' |
7 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' | ||
8 | import { areValidationErrors, checkUserCanManageVideo, doesVideoCaptionExist, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
11 | 9 | ||
12 | const addVideoCaptionValidator = [ | 10 | const addVideoCaptionValidator = [ |
13 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 11 | isValidVideoIdParam('videoId'), |
14 | param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), | 12 | |
13 | param('captionLanguage') | ||
14 | .custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), | ||
15 | |||
15 | body('captionfile') | 16 | body('captionfile') |
16 | .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile')) | 17 | .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile')) |
17 | .withMessage( | 18 | .withMessage( |
@@ -35,8 +36,10 @@ const addVideoCaptionValidator = [ | |||
35 | ] | 36 | ] |
36 | 37 | ||
37 | const deleteVideoCaptionValidator = [ | 38 | const deleteVideoCaptionValidator = [ |
38 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 39 | isValidVideoIdParam('videoId'), |
39 | param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), | 40 | |
41 | param('captionLanguage') | ||
42 | .custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), | ||
40 | 43 | ||
41 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 44 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
42 | logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params }) | 45 | logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params }) |
@@ -54,7 +57,7 @@ const deleteVideoCaptionValidator = [ | |||
54 | ] | 57 | ] |
55 | 58 | ||
56 | const listVideoCaptionsValidator = [ | 59 | const listVideoCaptionsValidator = [ |
57 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), | 60 | isValidVideoIdParam('videoId'), |
58 | 61 | ||
59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 62 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
60 | logger.debug('Checking listVideoCaptions parameters', { parameters: req.params }) | 63 | logger.debug('Checking listVideoCaptions parameters', { parameters: req.params }) |
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts index 2463d281c..e7df185e4 100644 --- a/server/middlewares/validators/videos/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts | |||
@@ -3,6 +3,7 @@ import { body, param, query } from 'express-validator' | |||
3 | import { VIDEO_CHANNELS } from '@server/initializers/constants' | 3 | import { VIDEO_CHANNELS } from '@server/initializers/constants' |
4 | import { MChannelAccountDefault, MUser } from '@server/types/models' | 4 | import { MChannelAccountDefault, MUser } from '@server/types/models' |
5 | import { UserRight } from '../../../../shared' | 5 | import { UserRight } from '../../../../shared' |
6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
6 | import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' | 7 | import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' |
7 | import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' | 8 | import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' |
8 | import { | 9 | import { |
@@ -11,11 +12,9 @@ import { | |||
11 | isVideoChannelSupportValid | 12 | isVideoChannelSupportValid |
12 | } from '../../../helpers/custom-validators/video-channels' | 13 | } from '../../../helpers/custom-validators/video-channels' |
13 | import { logger } from '../../../helpers/logger' | 14 | import { logger } from '../../../helpers/logger' |
14 | import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares' | 15 | import { ActorModel } from '../../../models/actor/actor' |
15 | import { ActorModel } from '../../../models/activitypub/actor' | ||
16 | import { VideoChannelModel } from '../../../models/video/video-channel' | 16 | import { VideoChannelModel } from '../../../models/video/video-channel' |
17 | import { areValidationErrors } from '../utils' | 17 | import { areValidationErrors, doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../shared' |
18 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
19 | 18 | ||
20 | const videoChannelsAddValidator = [ | 19 | const videoChannelsAddValidator = [ |
21 | body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), | 20 | body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), |
@@ -30,17 +29,16 @@ const videoChannelsAddValidator = [ | |||
30 | 29 | ||
31 | const actor = await ActorModel.loadLocalByName(req.body.name) | 30 | const actor = await ActorModel.loadLocalByName(req.body.name) |
32 | if (actor) { | 31 | if (actor) { |
33 | res.status(HttpStatusCode.CONFLICT_409) | 32 | res.fail({ |
34 | .send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) | 33 | status: HttpStatusCode.CONFLICT_409, |
35 | .end() | 34 | message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' |
35 | }) | ||
36 | return false | 36 | return false |
37 | } | 37 | } |
38 | 38 | ||
39 | const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) | 39 | const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) |
40 | if (count >= VIDEO_CHANNELS.MAX_PER_USER) { | 40 | if (count >= VIDEO_CHANNELS.MAX_PER_USER) { |
41 | res.status(HttpStatusCode.BAD_REQUEST_400) | 41 | res.fail({ message: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` }) |
42 | .send({ error: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` }) | ||
43 | .end() | ||
44 | return false | 42 | return false |
45 | } | 43 | } |
46 | 44 | ||
@@ -71,13 +69,17 @@ const videoChannelsUpdateValidator = [ | |||
71 | 69 | ||
72 | // We need to make additional checks | 70 | // We need to make additional checks |
73 | if (res.locals.videoChannel.Actor.isOwned() === false) { | 71 | if (res.locals.videoChannel.Actor.isOwned() === false) { |
74 | return res.status(HttpStatusCode.FORBIDDEN_403) | 72 | return res.fail({ |
75 | .json({ error: 'Cannot update video channel of another server' }) | 73 | status: HttpStatusCode.FORBIDDEN_403, |
74 | message: 'Cannot update video channel of another server' | ||
75 | }) | ||
76 | } | 76 | } |
77 | 77 | ||
78 | if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { | 78 | if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { |
79 | return res.status(HttpStatusCode.FORBIDDEN_403) | 79 | return res.fail({ |
80 | .json({ error: 'Cannot update video channel of another user' }) | 80 | status: HttpStatusCode.FORBIDDEN_403, |
81 | message: 'Cannot update video channel of another user' | ||
82 | }) | ||
81 | } | 83 | } |
82 | 84 | ||
83 | return next() | 85 | return next() |
@@ -139,6 +141,18 @@ const videoChannelStatsValidator = [ | |||
139 | } | 141 | } |
140 | ] | 142 | ] |
141 | 143 | ||
144 | const videoChannelsListValidator = [ | ||
145 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | ||
146 | |||
147 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
148 | logger.debug('Checking video channels search query', { parameters: req.query }) | ||
149 | |||
150 | if (areValidationErrors(req, res)) return | ||
151 | |||
152 | return next() | ||
153 | } | ||
154 | ] | ||
155 | |||
142 | // --------------------------------------------------------------------------- | 156 | // --------------------------------------------------------------------------- |
143 | 157 | ||
144 | export { | 158 | export { |
@@ -146,6 +160,7 @@ export { | |||
146 | videoChannelsUpdateValidator, | 160 | videoChannelsUpdateValidator, |
147 | videoChannelsRemoveValidator, | 161 | videoChannelsRemoveValidator, |
148 | videoChannelsNameWithHostValidator, | 162 | videoChannelsNameWithHostValidator, |
163 | videoChannelsListValidator, | ||
149 | localVideoChannelValidator, | 164 | localVideoChannelValidator, |
150 | videoChannelStatsValidator | 165 | videoChannelStatsValidator |
151 | } | 166 | } |
@@ -154,10 +169,10 @@ export { | |||
154 | 169 | ||
155 | function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { | 170 | function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { |
156 | if (videoChannel.Actor.isOwned() === false) { | 171 | if (videoChannel.Actor.isOwned() === false) { |
157 | res.status(HttpStatusCode.FORBIDDEN_403) | 172 | res.fail({ |
158 | .json({ error: 'Cannot remove video channel of another server.' }) | 173 | status: HttpStatusCode.FORBIDDEN_403, |
159 | .end() | 174 | message: 'Cannot remove video channel of another server.' |
160 | 175 | }) | |
161 | return false | 176 | return false |
162 | } | 177 | } |
163 | 178 | ||
@@ -165,10 +180,10 @@ function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAcco | |||
165 | // The user can delete it if s/he is an admin | 180 | // The user can delete it if s/he is an admin |
166 | // Or if s/he is the video channel's account | 181 | // Or if s/he is the video channel's account |
167 | if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) { | 182 | if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) { |
168 | res.status(HttpStatusCode.FORBIDDEN_403) | 183 | res.fail({ |
169 | .json({ error: 'Cannot remove video channel of another user' }) | 184 | status: HttpStatusCode.FORBIDDEN_403, |
170 | .end() | 185 | message: 'Cannot remove video channel of another user' |
171 | 186 | }) | |
172 | return false | 187 | return false |
173 | } | 188 | } |
174 | 189 | ||
@@ -179,10 +194,10 @@ async function checkVideoChannelIsNotTheLastOne (res: express.Response) { | |||
179 | const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) | 194 | const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) |
180 | 195 | ||
181 | if (count <= 1) { | 196 | if (count <= 1) { |
182 | res.status(HttpStatusCode.CONFLICT_409) | 197 | res.fail({ |
183 | .json({ error: 'Cannot remove the last channel of this user' }) | 198 | status: HttpStatusCode.CONFLICT_409, |
184 | .end() | 199 | message: 'Cannot remove the last channel of this user' |
185 | 200 | }) | |
186 | return false | 201 | return false |
187 | } | 202 | } |
188 | 203 | ||
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 1afacfed8..885506ebe 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -2,19 +2,14 @@ import * as express from 'express' | |||
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { MUserAccountUrl } from '@server/types/models' | 3 | import { MUserAccountUrl } from '@server/types/models' |
4 | import { UserRight } from '../../../../shared' | 4 | import { UserRight } from '../../../../shared' |
5 | import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' | 5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
6 | import { | 6 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' |
7 | doesVideoCommentExist, | 7 | import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' |
8 | doesVideoCommentThreadExist, | ||
9 | isValidVideoCommentText | ||
10 | } from '../../../helpers/custom-validators/video-comments' | ||
11 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
12 | import { doesVideoExist } from '../../../helpers/middlewares' | ||
13 | import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' | 9 | import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' |
14 | import { Hooks } from '../../../lib/plugins/hooks' | 10 | import { Hooks } from '../../../lib/plugins/hooks' |
15 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' | 11 | import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' |
16 | import { areValidationErrors } from '../utils' | 12 | import { areValidationErrors, doesVideoCommentExist, doesVideoCommentThreadExist, doesVideoExist, isValidVideoIdParam } from '../shared' |
17 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
18 | 13 | ||
19 | const listVideoCommentsValidator = [ | 14 | const listVideoCommentsValidator = [ |
20 | query('isLocal') | 15 | query('isLocal') |
@@ -45,7 +40,7 @@ const listVideoCommentsValidator = [ | |||
45 | ] | 40 | ] |
46 | 41 | ||
47 | const listVideoCommentThreadsValidator = [ | 42 | const listVideoCommentThreadsValidator = [ |
48 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 43 | isValidVideoIdParam('videoId'), |
49 | 44 | ||
50 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 45 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
51 | logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params }) | 46 | logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params }) |
@@ -58,8 +53,10 @@ const listVideoCommentThreadsValidator = [ | |||
58 | ] | 53 | ] |
59 | 54 | ||
60 | const listVideoThreadCommentsValidator = [ | 55 | const listVideoThreadCommentsValidator = [ |
61 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 56 | isValidVideoIdParam('videoId'), |
62 | param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'), | 57 | |
58 | param('threadId') | ||
59 | .custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'), | ||
63 | 60 | ||
64 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 61 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
65 | logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params }) | 62 | logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params }) |
@@ -73,8 +70,10 @@ const listVideoThreadCommentsValidator = [ | |||
73 | ] | 70 | ] |
74 | 71 | ||
75 | const addVideoCommentThreadValidator = [ | 72 | const addVideoCommentThreadValidator = [ |
76 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 73 | isValidVideoIdParam('videoId'), |
77 | body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), | 74 | |
75 | body('text') | ||
76 | .custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), | ||
78 | 77 | ||
79 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 78 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
80 | logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body }) | 79 | logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body }) |
@@ -89,8 +88,10 @@ const addVideoCommentThreadValidator = [ | |||
89 | ] | 88 | ] |
90 | 89 | ||
91 | const addVideoCommentReplyValidator = [ | 90 | const addVideoCommentReplyValidator = [ |
92 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 91 | isValidVideoIdParam('videoId'), |
92 | |||
93 | param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), | 93 | param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), |
94 | |||
94 | body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), | 95 | body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), |
95 | 96 | ||
96 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 97 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
@@ -107,8 +108,10 @@ const addVideoCommentReplyValidator = [ | |||
107 | ] | 108 | ] |
108 | 109 | ||
109 | const videoCommentGetValidator = [ | 110 | const videoCommentGetValidator = [ |
110 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 111 | isValidVideoIdParam('videoId'), |
111 | param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), | 112 | |
113 | param('commentId') | ||
114 | .custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), | ||
112 | 115 | ||
113 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 116 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
114 | logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) | 117 | logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) |
@@ -122,7 +125,8 @@ const videoCommentGetValidator = [ | |||
122 | ] | 125 | ] |
123 | 126 | ||
124 | const removeVideoCommentValidator = [ | 127 | const removeVideoCommentValidator = [ |
125 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 128 | isValidVideoIdParam('videoId'), |
129 | |||
126 | param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), | 130 | param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), |
127 | 131 | ||
128 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 132 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
@@ -155,9 +159,10 @@ export { | |||
155 | 159 | ||
156 | function isVideoCommentsEnabled (video: MVideo, res: express.Response) { | 160 | function isVideoCommentsEnabled (video: MVideo, res: express.Response) { |
157 | if (video.commentsEnabled !== true) { | 161 | if (video.commentsEnabled !== true) { |
158 | res.status(HttpStatusCode.CONFLICT_409) | 162 | res.fail({ |
159 | .json({ error: 'Video comments are disabled for this video.' }) | 163 | status: HttpStatusCode.CONFLICT_409, |
160 | 164 | message: 'Video comments are disabled for this video.' | |
165 | }) | ||
161 | return false | 166 | return false |
162 | } | 167 | } |
163 | 168 | ||
@@ -166,9 +171,10 @@ function isVideoCommentsEnabled (video: MVideo, res: express.Response) { | |||
166 | 171 | ||
167 | function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) { | 172 | function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) { |
168 | if (videoComment.isDeleted()) { | 173 | if (videoComment.isDeleted()) { |
169 | res.status(HttpStatusCode.CONFLICT_409) | 174 | res.fail({ |
170 | .json({ error: 'This comment is already deleted' }) | 175 | status: HttpStatusCode.CONFLICT_409, |
171 | 176 | message: 'This comment is already deleted' | |
177 | }) | ||
172 | return false | 178 | return false |
173 | } | 179 | } |
174 | 180 | ||
@@ -179,9 +185,10 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC | |||
179 | videoComment.accountId !== userAccount.id && // Not the comment owner | 185 | videoComment.accountId !== userAccount.id && // Not the comment owner |
180 | videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner | 186 | videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner |
181 | ) { | 187 | ) { |
182 | res.status(HttpStatusCode.FORBIDDEN_403) | 188 | res.fail({ |
183 | .json({ error: 'Cannot remove video comment of another user' }) | 189 | status: HttpStatusCode.FORBIDDEN_403, |
184 | 190 | message: 'Cannot remove video comment of another user' | |
191 | }) | ||
185 | return false | 192 | return false |
186 | } | 193 | } |
187 | 194 | ||
@@ -215,9 +222,11 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon | |||
215 | 222 | ||
216 | if (!acceptedResult || acceptedResult.accepted !== true) { | 223 | if (!acceptedResult || acceptedResult.accepted !== true) { |
217 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) | 224 | logger.info('Refused local comment.', { acceptedResult, acceptParameters }) |
218 | res.status(HttpStatusCode.FORBIDDEN_403) | ||
219 | .json({ error: acceptedResult?.errorMessage || 'Refused local comment' }) | ||
220 | 225 | ||
226 | res.fail({ | ||
227 | status: HttpStatusCode.FORBIDDEN_403, | ||
228 | message: acceptedResult?.errorMessage || 'Refused local comment' | ||
229 | }) | ||
221 | return false | 230 | return false |
222 | } | 231 | } |
223 | 232 | ||
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index c53af3861..85dc647ce 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -2,18 +2,17 @@ import * as express from 'express' | |||
2 | import { body } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { isPreImportVideoAccepted } from '@server/lib/moderation' | 3 | import { isPreImportVideoAccepted } from '@server/lib/moderation' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
5 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' | 6 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' |
6 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | 7 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' |
7 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' | 8 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' |
8 | import { isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' | 9 | import { isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' |
9 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 10 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
10 | import { logger } from '../../../helpers/logger' | 11 | import { logger } from '../../../helpers/logger' |
11 | import { doesVideoChannelOfAccountExist } from '../../../helpers/middlewares' | ||
12 | import { CONFIG } from '../../../initializers/config' | 12 | import { CONFIG } from '../../../initializers/config' |
13 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 13 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
14 | import { areValidationErrors } from '../utils' | 14 | import { areValidationErrors, doesVideoChannelOfAccountExist } from '../shared' |
15 | import { getCommonVideoEditAttributes } from './videos' | 15 | import { getCommonVideoEditAttributes } from './videos' |
16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
17 | 16 | ||
18 | const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | 17 | const videoImportAddValidator = getCommonVideoEditAttributes().concat([ |
19 | body('channelId') | 18 | body('channelId') |
@@ -33,7 +32,9 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
33 | ), | 32 | ), |
34 | body('name') | 33 | body('name') |
35 | .optional() | 34 | .optional() |
36 | .custom(isVideoNameValid).withMessage('Should have a valid name'), | 35 | .custom(isVideoNameValid).withMessage( |
36 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
37 | ), | ||
37 | 38 | ||
38 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 39 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
39 | logger.debug('Checking videoImportAddValidator parameters', { parameters: req.body }) | 40 | logger.debug('Checking videoImportAddValidator parameters', { parameters: req.body }) |
@@ -45,16 +46,20 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
45 | 46 | ||
46 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { | 47 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { |
47 | cleanUpReqFiles(req) | 48 | cleanUpReqFiles(req) |
48 | return res.status(HttpStatusCode.CONFLICT_409) | 49 | |
49 | .json({ error: 'HTTP import is not enabled on this instance.' }) | 50 | return res.fail({ |
50 | .end() | 51 | status: HttpStatusCode.CONFLICT_409, |
52 | message: 'HTTP import is not enabled on this instance.' | ||
53 | }) | ||
51 | } | 54 | } |
52 | 55 | ||
53 | if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { | 56 | if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { |
54 | cleanUpReqFiles(req) | 57 | cleanUpReqFiles(req) |
55 | return res.status(HttpStatusCode.CONFLICT_409) | 58 | |
56 | .json({ error: 'Torrent/magnet URI import is not enabled on this instance.' }) | 59 | return res.fail({ |
57 | .end() | 60 | status: HttpStatusCode.CONFLICT_409, |
61 | message: 'Torrent/magnet URI import is not enabled on this instance.' | ||
62 | }) | ||
58 | } | 63 | } |
59 | 64 | ||
60 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) | 65 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) |
@@ -63,9 +68,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
63 | if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { | 68 | if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { |
64 | cleanUpReqFiles(req) | 69 | cleanUpReqFiles(req) |
65 | 70 | ||
66 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 71 | return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' }) |
67 | .json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' }) | ||
68 | .end() | ||
69 | } | 72 | } |
70 | 73 | ||
71 | if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req) | 74 | if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req) |
@@ -101,9 +104,11 @@ async function isImportAccepted (req: express.Request, res: express.Response) { | |||
101 | 104 | ||
102 | if (!acceptedResult || acceptedResult.accepted !== true) { | 105 | if (!acceptedResult || acceptedResult.accepted !== true) { |
103 | logger.info('Refused to import video.', { acceptedResult, acceptParameters }) | 106 | logger.info('Refused to import video.', { acceptedResult, acceptParameters }) |
104 | res.status(HttpStatusCode.FORBIDDEN_403) | ||
105 | .json({ error: acceptedResult.errorMessage || 'Refused to import video' }) | ||
106 | 107 | ||
108 | res.fail({ | ||
109 | status: HttpStatusCode.FORBIDDEN_403, | ||
110 | message: acceptedResult.errorMessage || 'Refused to import video' | ||
111 | }) | ||
107 | return false | 112 | return false |
108 | } | 113 | } |
109 | 114 | ||
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index 3a73e1272..7cfb935e3 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts | |||
@@ -1,22 +1,28 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator' | 2 | import { body } from 'express-validator' |
3 | import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '@server/helpers/middlewares/videos' | 3 | import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' |
4 | import { isLocalLiveVideoAccepted } from '@server/lib/moderation' | ||
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
6 | import { VideoModel } from '@server/models/video/video' | ||
4 | import { VideoLiveModel } from '@server/models/video/video-live' | 7 | import { VideoLiveModel } from '@server/models/video/video-live' |
8 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
5 | import { ServerErrorCode, UserRight, VideoState } from '@shared/models' | 9 | import { ServerErrorCode, UserRight, VideoState } from '@shared/models' |
6 | import { isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 10 | import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' |
7 | import { isVideoNameValid } from '../../../helpers/custom-validators/videos' | 11 | import { isVideoNameValid } from '../../../helpers/custom-validators/videos' |
8 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 12 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
9 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
10 | import { CONFIG } from '../../../initializers/config' | 14 | import { CONFIG } from '../../../initializers/config' |
11 | import { areValidationErrors } from '../utils' | 15 | import { |
16 | areValidationErrors, | ||
17 | checkUserCanManageVideo, | ||
18 | doesVideoChannelOfAccountExist, | ||
19 | doesVideoExist, | ||
20 | isValidVideoIdParam | ||
21 | } from '../shared' | ||
12 | import { getCommonVideoEditAttributes } from './videos' | 22 | import { getCommonVideoEditAttributes } from './videos' |
13 | import { VideoModel } from '@server/models/video/video' | ||
14 | import { Hooks } from '@server/lib/plugins/hooks' | ||
15 | import { isLocalLiveVideoAccepted } from '@server/lib/moderation' | ||
16 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
17 | 23 | ||
18 | const videoLiveGetValidator = [ | 24 | const videoLiveGetValidator = [ |
19 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 25 | isValidVideoIdParam('videoId'), |
20 | 26 | ||
21 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 27 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
22 | logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.params, user: res.locals.oauth.token.User.username }) | 28 | logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.params, user: res.locals.oauth.token.User.username }) |
@@ -29,7 +35,12 @@ const videoLiveGetValidator = [ | |||
29 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return | 35 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return |
30 | 36 | ||
31 | const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id) | 37 | const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id) |
32 | if (!videoLive) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 38 | if (!videoLive) { |
39 | return res.fail({ | ||
40 | status: HttpStatusCode.NOT_FOUND_404, | ||
41 | message: 'Live video not found' | ||
42 | }) | ||
43 | } | ||
33 | 44 | ||
34 | res.locals.videoLive = videoLive | 45 | res.locals.videoLive = videoLive |
35 | 46 | ||
@@ -43,7 +54,9 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
43 | .custom(isIdValid).withMessage('Should have correct video channel id'), | 54 | .custom(isIdValid).withMessage('Should have correct video channel id'), |
44 | 55 | ||
45 | body('name') | 56 | body('name') |
46 | .custom(isVideoNameValid).withMessage('Should have a valid name'), | 57 | .custom(isVideoNameValid).withMessage( |
58 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
59 | ), | ||
47 | 60 | ||
48 | body('saveReplay') | 61 | body('saveReplay') |
49 | .optional() | 62 | .optional() |
@@ -63,22 +76,27 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
63 | if (CONFIG.LIVE.ENABLED !== true) { | 76 | if (CONFIG.LIVE.ENABLED !== true) { |
64 | cleanUpReqFiles(req) | 77 | cleanUpReqFiles(req) |
65 | 78 | ||
66 | return res.status(HttpStatusCode.FORBIDDEN_403) | 79 | return res.fail({ |
67 | .json({ error: 'Live is not enabled on this instance' }) | 80 | status: HttpStatusCode.FORBIDDEN_403, |
81 | message: 'Live is not enabled on this instance', | ||
82 | type: ServerErrorCode.LIVE_NOT_ENABLED | ||
83 | }) | ||
68 | } | 84 | } |
69 | 85 | ||
70 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { | 86 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { |
71 | cleanUpReqFiles(req) | 87 | cleanUpReqFiles(req) |
72 | 88 | ||
73 | return res.status(HttpStatusCode.FORBIDDEN_403) | 89 | return res.fail({ |
74 | .json({ error: 'Saving live replay is not allowed instance' }) | 90 | status: HttpStatusCode.FORBIDDEN_403, |
91 | message: 'Saving live replay is not enabled on this instance', | ||
92 | type: ServerErrorCode.LIVE_NOT_ALLOWING_REPLAY | ||
93 | }) | ||
75 | } | 94 | } |
76 | 95 | ||
77 | if (req.body.permanentLive && req.body.saveReplay) { | 96 | if (req.body.permanentLive && req.body.saveReplay) { |
78 | cleanUpReqFiles(req) | 97 | cleanUpReqFiles(req) |
79 | 98 | ||
80 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 99 | return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) |
81 | .json({ error: 'Cannot set this live as permanent while saving its replay' }) | ||
82 | } | 100 | } |
83 | 101 | ||
84 | const user = res.locals.oauth.token.User | 102 | const user = res.locals.oauth.token.User |
@@ -90,11 +108,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
90 | if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { | 108 | if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { |
91 | cleanUpReqFiles(req) | 109 | cleanUpReqFiles(req) |
92 | 110 | ||
93 | return res.status(HttpStatusCode.FORBIDDEN_403) | 111 | return res.fail({ |
94 | .json({ | 112 | status: HttpStatusCode.FORBIDDEN_403, |
95 | code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED, | 113 | message: 'Cannot create this live because the max instance lives limit is reached.', |
96 | error: 'Cannot create this live because the max instance lives limit is reached.' | 114 | type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED |
97 | }) | 115 | }) |
98 | } | 116 | } |
99 | } | 117 | } |
100 | 118 | ||
@@ -104,11 +122,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
104 | if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) { | 122 | if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) { |
105 | cleanUpReqFiles(req) | 123 | cleanUpReqFiles(req) |
106 | 124 | ||
107 | return res.status(HttpStatusCode.FORBIDDEN_403) | 125 | return res.fail({ |
108 | .json({ | 126 | status: HttpStatusCode.FORBIDDEN_403, |
109 | code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED, | 127 | message: 'Cannot create this live because the max user lives limit is reached.', |
110 | error: 'Cannot create this live because the max user lives limit is reached.' | 128 | type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED |
111 | }) | 129 | }) |
112 | } | 130 | } |
113 | } | 131 | } |
114 | 132 | ||
@@ -130,18 +148,18 @@ const videoLiveUpdateValidator = [ | |||
130 | if (areValidationErrors(req, res)) return | 148 | if (areValidationErrors(req, res)) return |
131 | 149 | ||
132 | if (req.body.permanentLive && req.body.saveReplay) { | 150 | if (req.body.permanentLive && req.body.saveReplay) { |
133 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 151 | return res.fail({ message: 'Cannot set this live as permanent while saving its replay' }) |
134 | .json({ error: 'Cannot set this live as permanent while saving its replay' }) | ||
135 | } | 152 | } |
136 | 153 | ||
137 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { | 154 | if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { |
138 | return res.status(HttpStatusCode.FORBIDDEN_403) | 155 | return res.fail({ |
139 | .json({ error: 'Saving live replay is not allowed instance' }) | 156 | status: HttpStatusCode.FORBIDDEN_403, |
157 | message: 'Saving live replay is not allowed instance' | ||
158 | }) | ||
140 | } | 159 | } |
141 | 160 | ||
142 | if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { | 161 | if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { |
143 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 162 | return res.fail({ message: 'Cannot update a live that has already started' }) |
144 | .json({ error: 'Cannot update a live that has already started' }) | ||
145 | } | 163 | } |
146 | 164 | ||
147 | // Check the user can manage the live | 165 | // Check the user can manage the live |
@@ -177,9 +195,10 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response) | |||
177 | if (!acceptedResult || acceptedResult.accepted !== true) { | 195 | if (!acceptedResult || acceptedResult.accepted !== true) { |
178 | logger.info('Refused local live video.', { acceptedResult, acceptParameters }) | 196 | logger.info('Refused local live video.', { acceptedResult, acceptParameters }) |
179 | 197 | ||
180 | res.status(HttpStatusCode.FORBIDDEN_403) | 198 | res.fail({ |
181 | .json({ error: acceptedResult.errorMessage || 'Refused local live video' }) | 199 | status: HttpStatusCode.FORBIDDEN_403, |
182 | 200 | message: acceptedResult.errorMessage || 'Refused local live video' | |
201 | }) | ||
183 | return false | 202 | return false |
184 | } | 203 | } |
185 | 204 | ||
diff --git a/server/middlewares/validators/videos/video-ownership-changes.ts b/server/middlewares/validators/videos/video-ownership-changes.ts new file mode 100644 index 000000000..54ac46c99 --- /dev/null +++ b/server/middlewares/validators/videos/video-ownership-changes.ts | |||
@@ -0,0 +1,121 @@ | |||
1 | import * as express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
4 | import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership' | ||
5 | import { logger } from '@server/helpers/logger' | ||
6 | import { isAbleToUploadVideo } from '@server/lib/user' | ||
7 | import { AccountModel } from '@server/models/account/account' | ||
8 | import { MVideoWithAllFiles } from '@server/types/models' | ||
9 | import { HttpStatusCode } from '@shared/core-utils' | ||
10 | import { ServerErrorCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@shared/models' | ||
11 | import { | ||
12 | areValidationErrors, | ||
13 | checkUserCanManageVideo, | ||
14 | doesChangeVideoOwnershipExist, | ||
15 | doesVideoChannelOfAccountExist, | ||
16 | doesVideoExist, | ||
17 | isValidVideoIdParam | ||
18 | } from '../shared' | ||
19 | |||
20 | const videosChangeOwnershipValidator = [ | ||
21 | isValidVideoIdParam('videoId'), | ||
22 | |||
23 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
24 | logger.debug('Checking changeOwnership parameters', { parameters: req.params }) | ||
25 | |||
26 | if (areValidationErrors(req, res)) return | ||
27 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
28 | |||
29 | // Check if the user who did the request is able to change the ownership of the video | ||
30 | if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return | ||
31 | |||
32 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) | ||
33 | if (!nextOwner) { | ||
34 | res.fail({ message: 'Changing video ownership to a remote account is not supported yet' }) | ||
35 | return | ||
36 | } | ||
37 | |||
38 | res.locals.nextOwner = nextOwner | ||
39 | return next() | ||
40 | } | ||
41 | ] | ||
42 | |||
43 | const videosTerminateChangeOwnershipValidator = [ | ||
44 | param('id') | ||
45 | .custom(isIdValid).withMessage('Should have a valid id'), | ||
46 | |||
47 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
48 | logger.debug('Checking changeOwnership parameters', { parameters: req.params }) | ||
49 | |||
50 | if (areValidationErrors(req, res)) return | ||
51 | if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return | ||
52 | |||
53 | // Check if the user who did the request is able to change the ownership of the video | ||
54 | if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return | ||
55 | |||
56 | const videoChangeOwnership = res.locals.videoChangeOwnership | ||
57 | |||
58 | if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) { | ||
59 | res.fail({ | ||
60 | status: HttpStatusCode.FORBIDDEN_403, | ||
61 | message: 'Ownership already accepted or refused' | ||
62 | }) | ||
63 | return | ||
64 | } | ||
65 | |||
66 | return next() | ||
67 | } | ||
68 | ] | ||
69 | |||
70 | const videosAcceptChangeOwnershipValidator = [ | ||
71 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
72 | const body = req.body as VideoChangeOwnershipAccept | ||
73 | if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return | ||
74 | |||
75 | const videoChangeOwnership = res.locals.videoChangeOwnership | ||
76 | |||
77 | const video = videoChangeOwnership.Video | ||
78 | |||
79 | if (!await checkCanAccept(video, res)) return | ||
80 | |||
81 | return next() | ||
82 | } | ||
83 | ] | ||
84 | |||
85 | export { | ||
86 | videosChangeOwnershipValidator, | ||
87 | videosTerminateChangeOwnershipValidator, | ||
88 | videosAcceptChangeOwnershipValidator | ||
89 | } | ||
90 | |||
91 | // --------------------------------------------------------------------------- | ||
92 | |||
93 | async function checkCanAccept (video: MVideoWithAllFiles, res: express.Response): Promise<boolean> { | ||
94 | if (video.isLive) { | ||
95 | |||
96 | if (video.state !== VideoState.WAITING_FOR_LIVE) { | ||
97 | res.fail({ | ||
98 | status: HttpStatusCode.BAD_REQUEST_400, | ||
99 | message: 'You can accept an ownership change of a published live.' | ||
100 | }) | ||
101 | |||
102 | return false | ||
103 | } | ||
104 | |||
105 | return true | ||
106 | } | ||
107 | |||
108 | const user = res.locals.oauth.token.User | ||
109 | |||
110 | if (!await isAbleToUploadVideo(user.id, video.getMaxQualityFile().size)) { | ||
111 | res.fail({ | ||
112 | status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, | ||
113 | message: 'The user video quota is exceeded with this video.', | ||
114 | type: ServerErrorCode.QUOTA_REACHED | ||
115 | }) | ||
116 | |||
117 | return false | ||
118 | } | ||
119 | |||
120 | return true | ||
121 | } | ||
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index c872d045e..5ee7ee0ce 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -11,6 +11,7 @@ import { | |||
11 | isIdOrUUIDValid, | 11 | isIdOrUUIDValid, |
12 | isIdValid, | 12 | isIdValid, |
13 | isUUIDValid, | 13 | isUUIDValid, |
14 | toCompleteUUID, | ||
14 | toIntArray, | 15 | toIntArray, |
15 | toIntOrNull, | 16 | toIntOrNull, |
16 | toValueOrNull | 17 | toValueOrNull |
@@ -25,12 +26,18 @@ import { | |||
25 | import { isVideoImage } from '../../../helpers/custom-validators/videos' | 26 | import { isVideoImage } from '../../../helpers/custom-validators/videos' |
26 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 27 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
27 | import { logger } from '../../../helpers/logger' | 28 | import { logger } from '../../../helpers/logger' |
28 | import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoPlaylistFetchType } from '../../../helpers/middlewares' | ||
29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' | 30 | import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' |
31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' | 31 | import { MVideoPlaylist } from '../../../types/models/video/video-playlist' |
32 | import { authenticatePromiseIfNeeded } from '../../auth' | 32 | import { authenticatePromiseIfNeeded } from '../../auth' |
33 | import { areValidationErrors } from '../utils' | 33 | import { |
34 | areValidationErrors, | ||
35 | doesVideoChannelIdExist, | ||
36 | doesVideoExist, | ||
37 | doesVideoPlaylistExist, | ||
38 | isValidPlaylistIdParam, | ||
39 | VideoPlaylistFetchType | ||
40 | } from '../shared' | ||
34 | 41 | ||
35 | const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ | 42 | const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ |
36 | body('displayName') | 43 | body('displayName') |
@@ -44,10 +51,13 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ | |||
44 | const body: VideoPlaylistCreate = req.body | 51 | const body: VideoPlaylistCreate = req.body |
45 | if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) | 52 | if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) |
46 | 53 | ||
47 | if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) { | 54 | if ( |
55 | !body.videoChannelId && | ||
56 | (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED) | ||
57 | ) { | ||
48 | cleanUpReqFiles(req) | 58 | cleanUpReqFiles(req) |
49 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 59 | |
50 | .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) | 60 | return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' }) |
51 | } | 61 | } |
52 | 62 | ||
53 | return next() | 63 | return next() |
@@ -55,8 +65,7 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ | |||
55 | ]) | 65 | ]) |
56 | 66 | ||
57 | const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ | 67 | const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ |
58 | param('playlistId') | 68 | isValidPlaylistIdParam('playlistId'), |
59 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | ||
60 | 69 | ||
61 | body('displayName') | 70 | body('displayName') |
62 | .optional() | 71 | .optional() |
@@ -85,14 +94,14 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ | |||
85 | ) | 94 | ) |
86 | ) { | 95 | ) { |
87 | cleanUpReqFiles(req) | 96 | cleanUpReqFiles(req) |
88 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 97 | |
89 | .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) | 98 | return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' }) |
90 | } | 99 | } |
91 | 100 | ||
92 | if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { | 101 | if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { |
93 | cleanUpReqFiles(req) | 102 | cleanUpReqFiles(req) |
94 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 103 | |
95 | .json({ error: 'Cannot update a watch later playlist.' }) | 104 | return res.fail({ message: 'Cannot update a watch later playlist.' }) |
96 | } | 105 | } |
97 | 106 | ||
98 | if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) | 107 | if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) |
@@ -102,8 +111,7 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ | |||
102 | ]) | 111 | ]) |
103 | 112 | ||
104 | const videoPlaylistsDeleteValidator = [ | 113 | const videoPlaylistsDeleteValidator = [ |
105 | param('playlistId') | 114 | isValidPlaylistIdParam('playlistId'), |
106 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | ||
107 | 115 | ||
108 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 116 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
109 | logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params }) | 117 | logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params }) |
@@ -114,8 +122,7 @@ const videoPlaylistsDeleteValidator = [ | |||
114 | 122 | ||
115 | const videoPlaylist = getPlaylist(res) | 123 | const videoPlaylist = getPlaylist(res) |
116 | if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { | 124 | if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { |
117 | return res.status(HttpStatusCode.BAD_REQUEST_400) | 125 | return res.fail({ message: 'Cannot delete a watch later playlist.' }) |
118 | .json({ error: 'Cannot delete a watch later playlist.' }) | ||
119 | } | 126 | } |
120 | 127 | ||
121 | if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { | 128 | if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { |
@@ -128,8 +135,7 @@ const videoPlaylistsDeleteValidator = [ | |||
128 | 135 | ||
129 | const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { | 136 | const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { |
130 | return [ | 137 | return [ |
131 | param('playlistId') | 138 | isValidPlaylistIdParam('playlistId'), |
132 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | ||
133 | 139 | ||
134 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 140 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
135 | logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params }) | 141 | logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params }) |
@@ -144,7 +150,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { | |||
144 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) { | 150 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) { |
145 | if (isUUIDValid(req.params.playlistId)) return next() | 151 | if (isUUIDValid(req.params.playlistId)) return next() |
146 | 152 | ||
147 | return res.status(HttpStatusCode.NOT_FOUND_404).end() | 153 | return res.fail({ |
154 | status: HttpStatusCode.NOT_FOUND_404, | ||
155 | message: 'Playlist not found' | ||
156 | }) | ||
148 | } | 157 | } |
149 | 158 | ||
150 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { | 159 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { |
@@ -156,8 +165,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { | |||
156 | !user || | 165 | !user || |
157 | (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST)) | 166 | (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST)) |
158 | ) { | 167 | ) { |
159 | return res.status(HttpStatusCode.FORBIDDEN_403) | 168 | return res.fail({ |
160 | .json({ error: 'Cannot get this private video playlist.' }) | 169 | status: HttpStatusCode.FORBIDDEN_403, |
170 | message: 'Cannot get this private video playlist.' | ||
171 | }) | ||
161 | } | 172 | } |
162 | 173 | ||
163 | return next() | 174 | return next() |
@@ -181,9 +192,10 @@ const videoPlaylistsSearchValidator = [ | |||
181 | ] | 192 | ] |
182 | 193 | ||
183 | const videoPlaylistsAddVideoValidator = [ | 194 | const videoPlaylistsAddVideoValidator = [ |
184 | param('playlistId') | 195 | isValidPlaylistIdParam('playlistId'), |
185 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | 196 | |
186 | body('videoId') | 197 | body('videoId') |
198 | .customSanitizer(toCompleteUUID) | ||
187 | .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'), | 199 | .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'), |
188 | body('startTimestamp') | 200 | body('startTimestamp') |
189 | .optional() | 201 | .optional() |
@@ -211,9 +223,9 @@ const videoPlaylistsAddVideoValidator = [ | |||
211 | ] | 223 | ] |
212 | 224 | ||
213 | const videoPlaylistsUpdateOrRemoveVideoValidator = [ | 225 | const videoPlaylistsUpdateOrRemoveVideoValidator = [ |
214 | param('playlistId') | 226 | isValidPlaylistIdParam('playlistId'), |
215 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | ||
216 | param('playlistElementId') | 227 | param('playlistElementId') |
228 | .customSanitizer(toCompleteUUID) | ||
217 | .custom(isIdValid).withMessage('Should have an element id/uuid'), | 229 | .custom(isIdValid).withMessage('Should have an element id/uuid'), |
218 | body('startTimestamp') | 230 | body('startTimestamp') |
219 | .optional() | 231 | .optional() |
@@ -233,10 +245,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [ | |||
233 | 245 | ||
234 | const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) | 246 | const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) |
235 | if (!videoPlaylistElement) { | 247 | if (!videoPlaylistElement) { |
236 | res.status(HttpStatusCode.NOT_FOUND_404) | 248 | res.fail({ |
237 | .json({ error: 'Video playlist element not found' }) | 249 | status: HttpStatusCode.NOT_FOUND_404, |
238 | .end() | 250 | message: 'Video playlist element not found' |
239 | 251 | }) | |
240 | return | 252 | return |
241 | } | 253 | } |
242 | res.locals.videoPlaylistElement = videoPlaylistElement | 254 | res.locals.videoPlaylistElement = videoPlaylistElement |
@@ -248,8 +260,7 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [ | |||
248 | ] | 260 | ] |
249 | 261 | ||
250 | const videoPlaylistElementAPGetValidator = [ | 262 | const videoPlaylistElementAPGetValidator = [ |
251 | param('playlistId') | 263 | isValidPlaylistIdParam('playlistId'), |
252 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | ||
253 | param('playlistElementId') | 264 | param('playlistElementId') |
254 | .custom(isIdValid).withMessage('Should have an playlist element id'), | 265 | .custom(isIdValid).withMessage('Should have an playlist element id'), |
255 | 266 | ||
@@ -263,15 +274,18 @@ const videoPlaylistElementAPGetValidator = [ | |||
263 | 274 | ||
264 | const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) | 275 | const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) |
265 | if (!videoPlaylistElement) { | 276 | if (!videoPlaylistElement) { |
266 | res.status(HttpStatusCode.NOT_FOUND_404) | 277 | res.fail({ |
267 | .json({ error: 'Video playlist element not found' }) | 278 | status: HttpStatusCode.NOT_FOUND_404, |
268 | .end() | 279 | message: 'Video playlist element not found' |
269 | 280 | }) | |
270 | return | 281 | return |
271 | } | 282 | } |
272 | 283 | ||
273 | if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { | 284 | if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { |
274 | return res.status(HttpStatusCode.FORBIDDEN_403).end() | 285 | return res.fail({ |
286 | status: HttpStatusCode.FORBIDDEN_403, | ||
287 | message: 'Cannot get this private video playlist.' | ||
288 | }) | ||
275 | } | 289 | } |
276 | 290 | ||
277 | res.locals.videoPlaylistElementAP = videoPlaylistElement | 291 | res.locals.videoPlaylistElementAP = videoPlaylistElement |
@@ -281,8 +295,7 @@ const videoPlaylistElementAPGetValidator = [ | |||
281 | ] | 295 | ] |
282 | 296 | ||
283 | const videoPlaylistsReorderVideosValidator = [ | 297 | const videoPlaylistsReorderVideosValidator = [ |
284 | param('playlistId') | 298 | isValidPlaylistIdParam('playlistId'), |
285 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | ||
286 | body('startPosition') | 299 | body('startPosition') |
287 | .isInt({ min: 1 }).withMessage('Should have a valid start position'), | 300 | .isInt({ min: 1 }).withMessage('Should have a valid start position'), |
288 | body('insertAfterPosition') | 301 | body('insertAfterPosition') |
@@ -307,18 +320,12 @@ const videoPlaylistsReorderVideosValidator = [ | |||
307 | const reorderLength: number = req.body.reorderLength | 320 | const reorderLength: number = req.body.reorderLength |
308 | 321 | ||
309 | if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) { | 322 | if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) { |
310 | res.status(HttpStatusCode.BAD_REQUEST_400) | 323 | res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` }) |
311 | .json({ error: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` }) | ||
312 | .end() | ||
313 | |||
314 | return | 324 | return |
315 | } | 325 | } |
316 | 326 | ||
317 | if (reorderLength && reorderLength + startPosition > nextPosition) { | 327 | if (reorderLength && reorderLength + startPosition > nextPosition) { |
318 | res.status(HttpStatusCode.BAD_REQUEST_400) | 328 | res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` }) |
319 | .json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` }) | ||
320 | .end() | ||
321 | |||
322 | return | 329 | return |
323 | } | 330 | } |
324 | 331 | ||
@@ -401,10 +408,10 @@ function getCommonPlaylistEditAttributes () { | |||
401 | 408 | ||
402 | function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) { | 409 | function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) { |
403 | if (videoPlaylist.isOwned() === false) { | 410 | if (videoPlaylist.isOwned() === false) { |
404 | res.status(HttpStatusCode.FORBIDDEN_403) | 411 | res.fail({ |
405 | .json({ error: 'Cannot manage video playlist of another server.' }) | 412 | status: HttpStatusCode.FORBIDDEN_403, |
406 | .end() | 413 | message: 'Cannot manage video playlist of another server.' |
407 | 414 | }) | |
408 | return false | 415 | return false |
409 | } | 416 | } |
410 | 417 | ||
@@ -412,10 +419,10 @@ function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: M | |||
412 | // The user can delete it if s/he is an admin | 419 | // The user can delete it if s/he is an admin |
413 | // Or if s/he is the video playlist's owner | 420 | // Or if s/he is the video playlist's owner |
414 | if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) { | 421 | if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) { |
415 | res.status(HttpStatusCode.FORBIDDEN_403) | 422 | res.fail({ |
416 | .json({ error: 'Cannot manage video playlist of another user' }) | 423 | status: HttpStatusCode.FORBIDDEN_403, |
417 | .end() | 424 | message: 'Cannot manage video playlist of another user' |
418 | 425 | }) | |
419 | return false | 426 | return false |
420 | } | 427 | } |
421 | 428 | ||
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts index 01bdef25f..5d5dfb222 100644 --- a/server/middlewares/validators/videos/video-rates.ts +++ b/server/middlewares/validators/videos/video-rates.ts | |||
@@ -1,18 +1,18 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | 3 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
4 | import { VideoRateType } from '../../../../shared/models/videos' | ||
5 | import { isAccountNameValid } from '../../../helpers/custom-validators/accounts' | ||
6 | import { isIdValid } from '../../../helpers/custom-validators/misc' | ||
4 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' | 7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' |
5 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | 8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' |
6 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
7 | import { areValidationErrors } from '../utils' | ||
8 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 10 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
9 | import { VideoRateType } from '../../../../shared/models/videos' | 11 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
10 | import { isAccountNameValid } from '../../../helpers/custom-validators/accounts' | ||
11 | import { doesVideoExist } from '../../../helpers/middlewares' | ||
12 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
13 | 12 | ||
14 | const videoUpdateRateValidator = [ | 13 | const videoUpdateRateValidator = [ |
15 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 14 | isValidVideoIdParam('id'), |
15 | |||
16 | body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), | 16 | body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), |
17 | 17 | ||
18 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 18 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
@@ -37,8 +37,10 @@ const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) { | |||
37 | 37 | ||
38 | const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId) | 38 | const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId) |
39 | if (!rate) { | 39 | if (!rate) { |
40 | return res.status(HttpStatusCode.NOT_FOUND_404) | 40 | return res.fail({ |
41 | .json({ error: 'Video rate not found' }) | 41 | status: HttpStatusCode.NOT_FOUND_404, |
42 | message: 'Video rate not found' | ||
43 | }) | ||
42 | } | 44 | } |
43 | 45 | ||
44 | res.locals.accountVideoRate = rate | 46 | res.locals.accountVideoRate = rate |
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts index f0d8e0c36..7e54b6fc0 100644 --- a/server/middlewares/validators/videos/video-shares.ts +++ b/server/middlewares/validators/videos/video-shares.ts | |||
@@ -1,15 +1,16 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { param } from 'express-validator' | 2 | import { param } from 'express-validator' |
3 | import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' | 3 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
4 | import { isIdValid } from '../../../helpers/custom-validators/misc' | ||
4 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
5 | import { VideoShareModel } from '../../../models/video/video-share' | 6 | import { VideoShareModel } from '../../../models/video/video-share' |
6 | import { areValidationErrors } from '../utils' | 7 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
7 | import { doesVideoExist } from '../../../helpers/middlewares' | ||
8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
9 | 8 | ||
10 | const videosShareValidator = [ | 9 | const videosShareValidator = [ |
11 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 10 | isValidVideoIdParam('id'), |
12 | param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'), | 11 | |
12 | param('actorId') | ||
13 | .custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'), | ||
13 | 14 | ||
14 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 15 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
15 | logger.debug('Checking videoShare parameters', { parameters: req.params }) | 16 | logger.debug('Checking videoShare parameters', { parameters: req.params }) |
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts index 29ce0dab6..43306f7cd 100644 --- a/server/middlewares/validators/videos/video-watch.ts +++ b/server/middlewares/validators/videos/video-watch.ts | |||
@@ -1,13 +1,13 @@ | |||
1 | import { body, param } from 'express-validator' | ||
2 | import * as express from 'express' | 1 | import * as express from 'express' |
3 | import { isIdOrUUIDValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | 2 | import { body } from 'express-validator' |
4 | import { areValidationErrors } from '../utils' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | import { doesVideoExist } from '../../../helpers/middlewares' | ||
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
4 | import { toIntOrNull } from '../../../helpers/custom-validators/misc' | ||
5 | import { logger } from '../../../helpers/logger' | ||
6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | ||
8 | 7 | ||
9 | const videoWatchingValidator = [ | 8 | const videoWatchingValidator = [ |
10 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 9 | isValidVideoIdParam('videoId'), |
10 | |||
11 | body('currentTime') | 11 | body('currentTime') |
12 | .customSanitizer(toIntOrNull) | 12 | .customSanitizer(toIntOrNull) |
13 | .isInt().withMessage('Should have correct current time'), | 13 | .isInt().withMessage('Should have correct current time'), |
@@ -21,7 +21,10 @@ const videoWatchingValidator = [ | |||
21 | const user = res.locals.oauth.token.User | 21 | const user = res.locals.oauth.token.User |
22 | if (user.videosHistoryEnabled === false) { | 22 | if (user.videosHistoryEnabled === false) { |
23 | logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) | 23 | logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) |
24 | return res.status(HttpStatusCode.CONFLICT_409).end() | 24 | return res.fail({ |
25 | status: HttpStatusCode.CONFLICT_409, | ||
26 | message: 'Video history is disabled' | ||
27 | }) | ||
25 | } | 28 | } |
26 | 29 | ||
27 | return next() | 30 | return next() |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index d26bcd4a6..49e10e2b5 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -4,16 +4,14 @@ import { getResumableUploadPath } from '@server/helpers/upload' | |||
4 | import { isAbleToUploadVideo } from '@server/lib/user' | 4 | import { isAbleToUploadVideo } from '@server/lib/user' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { ExpressPromiseHandler } from '@server/types/express' | 6 | import { ExpressPromiseHandler } from '@server/types/express' |
7 | import { MUserAccountId, MVideoWithRights } from '@server/types/models' | 7 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' |
8 | import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' | 8 | import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' |
9 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 9 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
10 | import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' | ||
11 | import { | 10 | import { |
12 | exists, | 11 | exists, |
13 | isBooleanValid, | 12 | isBooleanValid, |
14 | isDateValid, | 13 | isDateValid, |
15 | isFileFieldValid, | 14 | isFileFieldValid, |
16 | isIdOrUUIDValid, | ||
17 | isIdValid, | 15 | isIdValid, |
18 | isUUIDValid, | 16 | isUUIDValid, |
19 | toArray, | 17 | toArray, |
@@ -22,7 +20,6 @@ import { | |||
22 | toValueOrNull | 20 | toValueOrNull |
23 | } from '../../../helpers/custom-validators/misc' | 21 | } from '../../../helpers/custom-validators/misc' |
24 | import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' | 22 | import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' |
25 | import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' | ||
26 | import { | 23 | import { |
27 | isScheduleVideoUpdatePrivacyValid, | 24 | isScheduleVideoUpdatePrivacyValid, |
28 | isVideoCategoryValid, | 25 | isVideoCategoryValid, |
@@ -42,22 +39,22 @@ import { | |||
42 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 39 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
43 | import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils' | 40 | import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils' |
44 | import { logger } from '../../../helpers/logger' | 41 | import { logger } from '../../../helpers/logger' |
45 | import { | ||
46 | checkUserCanManageVideo, | ||
47 | doesVideoChannelOfAccountExist, | ||
48 | doesVideoExist, | ||
49 | doesVideoFileOfVideoExist | ||
50 | } from '../../../helpers/middlewares' | ||
51 | import { deleteFileAndCatch } from '../../../helpers/utils' | 42 | import { deleteFileAndCatch } from '../../../helpers/utils' |
52 | import { getVideoWithAttributes } from '../../../helpers/video' | 43 | import { getVideoWithAttributes } from '../../../helpers/video' |
53 | import { CONFIG } from '../../../initializers/config' | 44 | import { CONFIG } from '../../../initializers/config' |
54 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | 45 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' |
55 | import { isLocalVideoAccepted } from '../../../lib/moderation' | 46 | import { isLocalVideoAccepted } from '../../../lib/moderation' |
56 | import { Hooks } from '../../../lib/plugins/hooks' | 47 | import { Hooks } from '../../../lib/plugins/hooks' |
57 | import { AccountModel } from '../../../models/account/account' | ||
58 | import { VideoModel } from '../../../models/video/video' | 48 | import { VideoModel } from '../../../models/video/video' |
59 | import { authenticatePromiseIfNeeded } from '../../auth' | 49 | import { authenticatePromiseIfNeeded } from '../../auth' |
60 | import { areValidationErrors } from '../utils' | 50 | import { |
51 | areValidationErrors, | ||
52 | checkUserCanManageVideo, | ||
53 | doesVideoChannelOfAccountExist, | ||
54 | doesVideoExist, | ||
55 | doesVideoFileOfVideoExist, | ||
56 | isValidVideoIdParam | ||
57 | } from '../shared' | ||
61 | 58 | ||
62 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | 59 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ |
63 | body('videofile') | 60 | body('videofile') |
@@ -65,8 +62,9 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
65 | .withMessage('Should have a file'), | 62 | .withMessage('Should have a file'), |
66 | body('name') | 63 | body('name') |
67 | .trim() | 64 | .trim() |
68 | .custom(isVideoNameValid) | 65 | .custom(isVideoNameValid).withMessage( |
69 | .withMessage('Should have a valid name'), | 66 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` |
67 | ), | ||
70 | body('channelId') | 68 | body('channelId') |
71 | .customSanitizer(toIntOrNull) | 69 | .customSanitizer(toIntOrNull) |
72 | .custom(isIdValid).withMessage('Should have correct video channel id'), | 70 | .custom(isIdValid).withMessage('Should have correct video channel id'), |
@@ -87,9 +85,11 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
87 | if (!videoFile.duration) await addDurationToVideo(videoFile) | 85 | if (!videoFile.duration) await addDurationToVideo(videoFile) |
88 | } catch (err) { | 86 | } catch (err) { |
89 | logger.error('Invalid input file in videosAddLegacyValidator.', { err }) | 87 | logger.error('Invalid input file in videosAddLegacyValidator.', { err }) |
90 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) | ||
91 | .json({ error: 'Video file unreadable.' }) | ||
92 | 88 | ||
89 | res.fail({ | ||
90 | status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, | ||
91 | message: 'Video file unreadable.' | ||
92 | }) | ||
93 | return cleanUpReqFiles(req) | 93 | return cleanUpReqFiles(req) |
94 | } | 94 | } |
95 | 95 | ||
@@ -117,9 +117,11 @@ const videosAddResumableValidator = [ | |||
117 | if (!file.duration) await addDurationToVideo(file) | 117 | if (!file.duration) await addDurationToVideo(file) |
118 | } catch (err) { | 118 | } catch (err) { |
119 | logger.error('Invalid input file in videosAddResumableValidator.', { err }) | 119 | logger.error('Invalid input file in videosAddResumableValidator.', { err }) |
120 | res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422) | ||
121 | .json({ error: 'Video file unreadable.' }) | ||
122 | 120 | ||
121 | res.fail({ | ||
122 | status: HttpStatusCode.UNPROCESSABLE_ENTITY_422, | ||
123 | message: 'Video file unreadable.' | ||
124 | }) | ||
123 | return cleanup() | 125 | return cleanup() |
124 | } | 126 | } |
125 | 127 | ||
@@ -146,8 +148,9 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
146 | .withMessage('Should have a valid filename'), | 148 | .withMessage('Should have a valid filename'), |
147 | body('name') | 149 | body('name') |
148 | .trim() | 150 | .trim() |
149 | .custom(isVideoNameValid) | 151 | .custom(isVideoNameValid).withMessage( |
150 | .withMessage('Should have a valid name'), | 152 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` |
153 | ), | ||
151 | body('channelId') | 154 | body('channelId') |
152 | .customSanitizer(toIntOrNull) | 155 | .customSanitizer(toIntOrNull) |
153 | .custom(isIdValid).withMessage('Should have correct video channel id'), | 156 | .custom(isIdValid).withMessage('Should have correct video channel id'), |
@@ -192,11 +195,14 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
192 | ]) | 195 | ]) |
193 | 196 | ||
194 | const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | 197 | const videosUpdateValidator = getCommonVideoEditAttributes().concat([ |
195 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 198 | isValidVideoIdParam('id'), |
199 | |||
196 | body('name') | 200 | body('name') |
197 | .optional() | 201 | .optional() |
198 | .trim() | 202 | .trim() |
199 | .custom(isVideoNameValid).withMessage('Should have a valid name'), | 203 | .custom(isVideoNameValid).withMessage( |
204 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | ||
205 | ), | ||
200 | body('channelId') | 206 | body('channelId') |
201 | .optional() | 207 | .optional() |
202 | .customSanitizer(toIntOrNull) | 208 | .customSanitizer(toIntOrNull) |
@@ -238,20 +244,22 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R | |||
238 | const serverActor = await getServerActor() | 244 | const serverActor = await getServerActor() |
239 | if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() | 245 | if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() |
240 | 246 | ||
241 | return res.status(HttpStatusCode.FORBIDDEN_403) | 247 | return res.fail({ |
242 | .json({ | 248 | status: HttpStatusCode.FORBIDDEN_403, |
243 | errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, | 249 | message: 'Cannot get this video regarding follow constraints', |
244 | error: 'Cannot get this video regarding follow constraints.', | 250 | type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, |
245 | originUrl: video.url | 251 | data: { |
246 | }) | 252 | originUrl: video.url |
253 | } | ||
254 | }) | ||
247 | } | 255 | } |
248 | 256 | ||
249 | const videosCustomGetValidator = ( | 257 | const videosCustomGetValidator = ( |
250 | fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes', | 258 | fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes', |
251 | authenticateInQuery = false | 259 | authenticateInQuery = false |
252 | ) => { | 260 | ) => { |
253 | return [ | 261 | return [ |
254 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 262 | isValidVideoIdParam('id'), |
255 | 263 | ||
256 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 264 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
257 | logger.debug('Checking videosGet parameters', { parameters: req.params }) | 265 | logger.debug('Checking videosGet parameters', { parameters: req.params }) |
@@ -262,7 +270,7 @@ const videosCustomGetValidator = ( | |||
262 | // Controllers does not need to check video rights | 270 | // Controllers does not need to check video rights |
263 | if (fetchType === 'only-immutable-attributes') return next() | 271 | if (fetchType === 'only-immutable-attributes') return next() |
264 | 272 | ||
265 | const video = getVideoWithAttributes(res) as MVideoWithRights | 273 | const video = getVideoWithAttributes(res) as MVideoFullLight |
266 | 274 | ||
267 | // Video private or blacklisted | 275 | // Video private or blacklisted |
268 | if (video.requiresAuth()) { | 276 | if (video.requiresAuth()) { |
@@ -270,10 +278,12 @@ const videosCustomGetValidator = ( | |||
270 | 278 | ||
271 | const user = res.locals.oauth ? res.locals.oauth.token.User : null | 279 | const user = res.locals.oauth ? res.locals.oauth.token.User : null |
272 | 280 | ||
273 | // Only the owner or a user that have blacklist rights can see the video | 281 | // Only the owner or a user that have blocklist rights can see the video |
274 | if (!user || !user.canGetVideo(video)) { | 282 | if (!user || !user.canGetVideo(video)) { |
275 | return res.status(HttpStatusCode.FORBIDDEN_403) | 283 | return res.fail({ |
276 | .json({ error: 'Cannot get this private/internal or blacklisted video.' }) | 284 | status: HttpStatusCode.FORBIDDEN_403, |
285 | message: 'Cannot get this private/internal or blocklisted video' | ||
286 | }) | ||
277 | } | 287 | } |
278 | 288 | ||
279 | return next() | 289 | return next() |
@@ -287,7 +297,10 @@ const videosCustomGetValidator = ( | |||
287 | if (isUUIDValid(req.params.id)) return next() | 297 | if (isUUIDValid(req.params.id)) return next() |
288 | 298 | ||
289 | // Don't leak this unlisted video | 299 | // Don't leak this unlisted video |
290 | return res.status(HttpStatusCode.NOT_FOUND_404).end() | 300 | return res.fail({ |
301 | status: HttpStatusCode.NOT_FOUND_404, | ||
302 | message: 'Video not found' | ||
303 | }) | ||
291 | } | 304 | } |
292 | } | 305 | } |
293 | ] | 306 | ] |
@@ -297,8 +310,10 @@ const videosGetValidator = videosCustomGetValidator('all') | |||
297 | const videosDownloadValidator = videosCustomGetValidator('all', true) | 310 | const videosDownloadValidator = videosCustomGetValidator('all', true) |
298 | 311 | ||
299 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | 312 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ |
300 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 313 | isValidVideoIdParam('id'), |
301 | param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'), | 314 | |
315 | param('videoFileId') | ||
316 | .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'), | ||
302 | 317 | ||
303 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 318 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
304 | logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params }) | 319 | logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params }) |
@@ -311,7 +326,7 @@ const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | |||
311 | ]) | 326 | ]) |
312 | 327 | ||
313 | const videosRemoveValidator = [ | 328 | const videosRemoveValidator = [ |
314 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | 329 | isValidVideoIdParam('id'), |
315 | 330 | ||
316 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 331 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
317 | logger.debug('Checking videosRemove parameters', { parameters: req.params }) | 332 | logger.debug('Checking videosRemove parameters', { parameters: req.params }) |
@@ -326,74 +341,6 @@ const videosRemoveValidator = [ | |||
326 | } | 341 | } |
327 | ] | 342 | ] |
328 | 343 | ||
329 | const videosChangeOwnershipValidator = [ | ||
330 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
331 | |||
332 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
333 | logger.debug('Checking changeOwnership parameters', { parameters: req.params }) | ||
334 | |||
335 | if (areValidationErrors(req, res)) return | ||
336 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
337 | |||
338 | // Check if the user who did the request is able to change the ownership of the video | ||
339 | if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return | ||
340 | |||
341 | const nextOwner = await AccountModel.loadLocalByName(req.body.username) | ||
342 | if (!nextOwner) { | ||
343 | res.status(HttpStatusCode.BAD_REQUEST_400) | ||
344 | .json({ error: 'Changing video ownership to a remote account is not supported yet' }) | ||
345 | |||
346 | return | ||
347 | } | ||
348 | res.locals.nextOwner = nextOwner | ||
349 | |||
350 | return next() | ||
351 | } | ||
352 | ] | ||
353 | |||
354 | const videosTerminateChangeOwnershipValidator = [ | ||
355 | param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), | ||
356 | |||
357 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
358 | logger.debug('Checking changeOwnership parameters', { parameters: req.params }) | ||
359 | |||
360 | if (areValidationErrors(req, res)) return | ||
361 | if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return | ||
362 | |||
363 | // Check if the user who did the request is able to change the ownership of the video | ||
364 | if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return | ||
365 | |||
366 | const videoChangeOwnership = res.locals.videoChangeOwnership | ||
367 | |||
368 | if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) { | ||
369 | res.status(HttpStatusCode.FORBIDDEN_403) | ||
370 | .json({ error: 'Ownership already accepted or refused' }) | ||
371 | return | ||
372 | } | ||
373 | |||
374 | return next() | ||
375 | } | ||
376 | ] | ||
377 | |||
378 | const videosAcceptChangeOwnershipValidator = [ | ||
379 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
380 | const body = req.body as VideoChangeOwnershipAccept | ||
381 | if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return | ||
382 | |||
383 | const user = res.locals.oauth.token.User | ||
384 | const videoChangeOwnership = res.locals.videoChangeOwnership | ||
385 | const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size) | ||
386 | if (isAble === false) { | ||
387 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | ||
388 | .json({ error: 'The user video quota is exceeded with this video.' }) | ||
389 | |||
390 | return | ||
391 | } | ||
392 | |||
393 | return next() | ||
394 | } | ||
395 | ] | ||
396 | |||
397 | const videosOverviewValidator = [ | 344 | const videosOverviewValidator = [ |
398 | query('page') | 345 | query('page') |
399 | .optional() | 346 | .optional() |
@@ -455,7 +402,11 @@ function getCommonVideoEditAttributes () { | |||
455 | body('tags') | 402 | body('tags') |
456 | .optional() | 403 | .optional() |
457 | .customSanitizer(toValueOrNull) | 404 | .customSanitizer(toValueOrNull) |
458 | .custom(isVideoTagsValid).withMessage('Should have correct tags'), | 405 | .custom(isVideoTagsValid) |
406 | .withMessage( | ||
407 | `Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` + | ||
408 | `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each` | ||
409 | ), | ||
459 | body('commentsEnabled') | 410 | body('commentsEnabled') |
460 | .optional() | 411 | .optional() |
461 | .customSanitizer(toBooleanOrNull) | 412 | .customSanitizer(toBooleanOrNull) |
@@ -473,7 +424,7 @@ function getCommonVideoEditAttributes () { | |||
473 | .customSanitizer(toValueOrNull), | 424 | .customSanitizer(toValueOrNull), |
474 | body('scheduleUpdate.updateAt') | 425 | body('scheduleUpdate.updateAt') |
475 | .optional() | 426 | .optional() |
476 | .custom(isDateValid).withMessage('Should have a valid schedule update date'), | 427 | .custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'), |
477 | body('scheduleUpdate.privacy') | 428 | body('scheduleUpdate.privacy') |
478 | .optional() | 429 | .optional() |
479 | .customSanitizer(toIntOrNull) | 430 | .customSanitizer(toIntOrNull) |
@@ -530,9 +481,10 @@ const commonVideosFiltersValidator = [ | |||
530 | (req.query.filter === 'all-local' || req.query.filter === 'all') && | 481 | (req.query.filter === 'all-local' || req.query.filter === 'all') && |
531 | (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) | 482 | (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) |
532 | ) { | 483 | ) { |
533 | res.status(HttpStatusCode.UNAUTHORIZED_401) | 484 | res.fail({ |
534 | .json({ error: 'You are not allowed to see all local videos.' }) | 485 | status: HttpStatusCode.UNAUTHORIZED_401, |
535 | 486 | message: 'You are not allowed to see all local videos.' | |
487 | }) | ||
536 | return | 488 | return |
537 | } | 489 | } |
538 | 490 | ||
@@ -555,10 +507,6 @@ export { | |||
555 | videosCustomGetValidator, | 507 | videosCustomGetValidator, |
556 | videosRemoveValidator, | 508 | videosRemoveValidator, |
557 | 509 | ||
558 | videosChangeOwnershipValidator, | ||
559 | videosTerminateChangeOwnershipValidator, | ||
560 | videosAcceptChangeOwnershipValidator, | ||
561 | |||
562 | getCommonVideoEditAttributes, | 510 | getCommonVideoEditAttributes, |
563 | 511 | ||
564 | commonVideosFiltersValidator, | 512 | commonVideosFiltersValidator, |
@@ -573,9 +521,7 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) | |||
573 | if (!req.body.scheduleUpdate.updateAt) { | 521 | if (!req.body.scheduleUpdate.updateAt) { |
574 | logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') | 522 | logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') |
575 | 523 | ||
576 | res.status(HttpStatusCode.BAD_REQUEST_400) | 524 | res.fail({ message: 'Schedule update at is mandatory.' }) |
577 | .json({ error: 'Schedule update at is mandatory.' }) | ||
578 | |||
579 | return true | 525 | return true |
580 | } | 526 | } |
581 | } | 527 | } |
@@ -597,26 +543,29 @@ async function commonVideoChecksPass (parameters: { | |||
597 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false | 543 | if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false |
598 | 544 | ||
599 | if (!isVideoFileMimeTypeValid(files)) { | 545 | if (!isVideoFileMimeTypeValid(files)) { |
600 | res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) | 546 | res.fail({ |
601 | .json({ | 547 | status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415, |
602 | error: 'This file is not supported. Please, make sure it is of the following type: ' + | 548 | message: 'This file is not supported. Please, make sure it is of the following type: ' + |
603 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') | 549 | CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') |
604 | }) | 550 | }) |
605 | |||
606 | return false | 551 | return false |
607 | } | 552 | } |
608 | 553 | ||
609 | if (!isVideoFileSizeValid(videoFileSize.toString())) { | 554 | if (!isVideoFileSizeValid(videoFileSize.toString())) { |
610 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | 555 | res.fail({ |
611 | .json({ error: 'This file is too large. It exceeds the maximum file size authorized.' }) | 556 | status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, |
612 | 557 | message: 'This file is too large. It exceeds the maximum file size authorized.', | |
558 | type: ServerErrorCode.MAX_FILE_SIZE_REACHED | ||
559 | }) | ||
613 | return false | 560 | return false |
614 | } | 561 | } |
615 | 562 | ||
616 | if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { | 563 | if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { |
617 | res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) | 564 | res.fail({ |
618 | .json({ error: 'The user video quota is exceeded with this video.' }) | 565 | status: HttpStatusCode.PAYLOAD_TOO_LARGE_413, |
619 | 566 | message: 'The user video quota is exceeded with this video.', | |
567 | type: ServerErrorCode.QUOTA_REACHED | ||
568 | }) | ||
620 | return false | 569 | return false |
621 | } | 570 | } |
622 | 571 | ||
@@ -642,9 +591,10 @@ export async function isVideoAccepted ( | |||
642 | 591 | ||
643 | if (!acceptedResult || acceptedResult.accepted !== true) { | 592 | if (!acceptedResult || acceptedResult.accepted !== true) { |
644 | logger.info('Refused local video.', { acceptedResult, acceptParameters }) | 593 | logger.info('Refused local video.', { acceptedResult, acceptParameters }) |
645 | res.status(HttpStatusCode.FORBIDDEN_403) | 594 | res.fail({ |
646 | .json({ error: acceptedResult.errorMessage || 'Refused local video' }) | 595 | status: HttpStatusCode.FORBIDDEN_403, |
647 | 596 | message: acceptedResult.errorMessage || 'Refused local video' | |
597 | }) | ||
648 | return false | 598 | return false |
649 | } | 599 | } |
650 | 600 | ||
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts index a71422ed8..bcdd136c6 100644 --- a/server/middlewares/validators/webfinger.ts +++ b/server/middlewares/validators/webfinger.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger' | 4 | import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger' |
4 | import { logger } from '../../helpers/logger' | ||
5 | import { ActorModel } from '../../models/activitypub/actor' | ||
6 | import { areValidationErrors } from './utils' | ||
7 | import { getHostWithPort } from '../../helpers/express-utils' | 5 | import { getHostWithPort } from '../../helpers/express-utils' |
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 6 | import { logger } from '../../helpers/logger' |
7 | import { ActorModel } from '../../models/actor/actor' | ||
8 | import { areValidationErrors } from './shared' | ||
9 | 9 | ||
10 | const webfingerValidator = [ | 10 | const webfingerValidator = [ |
11 | query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'), | 11 | query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'), |
@@ -21,9 +21,10 @@ const webfingerValidator = [ | |||
21 | 21 | ||
22 | const actor = await ActorModel.loadLocalUrlByName(name) | 22 | const actor = await ActorModel.loadLocalUrlByName(name) |
23 | if (!actor) { | 23 | if (!actor) { |
24 | return res.status(HttpStatusCode.NOT_FOUND_404) | 24 | return res.fail({ |
25 | .send({ error: 'Actor not found' }) | 25 | status: HttpStatusCode.NOT_FOUND_404, |
26 | .end() | 26 | message: 'Actor not found' |
27 | }) | ||
27 | } | 28 | } |
28 | 29 | ||
29 | res.locals.actorUrl = actor | 30 | res.locals.actorUrl = actor |
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 7e51b3e07..2c5987e96 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' | 2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' |
3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | 3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { AbuseMessage } from '@shared/models' | 5 | import { AbuseMessage } from '@shared/models' |
5 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
6 | import { getSort, throwIfNotValid } from '../utils' | 7 | import { getSort, throwIfNotValid } from '../utils' |
@@ -17,7 +18,7 @@ import { AbuseModel } from './abuse' | |||
17 | } | 18 | } |
18 | ] | 19 | ] |
19 | }) | 20 | }) |
20 | export class AbuseMessageModel extends Model { | 21 | export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessageModel>>> { |
21 | 22 | ||
22 | @AllowNull(false) | 23 | @AllowNull(false) |
23 | @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message')) | 24 | @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message')) |
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 262f364f1..3518f5c02 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' | 18 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' |
19 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | 19 | import { abusePredefinedReasonsMap, AttributesOnly } from '@shared/core-utils' |
20 | import { | 20 | import { |
21 | AbuseFilter, | 21 | AbuseFilter, |
22 | AbuseObject, | 22 | AbuseObject, |
@@ -187,7 +187,7 @@ export enum ScopeNames { | |||
187 | } | 187 | } |
188 | ] | 188 | ] |
189 | }) | 189 | }) |
190 | export class AbuseModel extends Model { | 190 | export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> { |
191 | 191 | ||
192 | @AllowNull(false) | 192 | @AllowNull(false) |
193 | @Default(null) | 193 | @Default(null) |
diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts index 90aa0695e..95bff50d0 100644 --- a/server/models/abuse/video-abuse.ts +++ b/server/models/abuse/video-abuse.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoDetails } from '@shared/models' | 3 | import { VideoDetails } from '@shared/models' |
3 | import { VideoModel } from '../video/video' | 4 | import { VideoModel } from '../video/video' |
4 | import { AbuseModel } from './abuse' | 5 | import { AbuseModel } from './abuse' |
@@ -14,7 +15,7 @@ import { AbuseModel } from './abuse' | |||
14 | } | 15 | } |
15 | ] | 16 | ] |
16 | }) | 17 | }) |
17 | export class VideoAbuseModel extends Model { | 18 | export class VideoAbuseModel extends Model<Partial<AttributesOnly<VideoAbuseModel>>> { |
18 | 19 | ||
19 | @CreatedAt | 20 | @CreatedAt |
20 | createdAt: Date | 21 | createdAt: Date |
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts index d3fce76a5..32cb2ca64 100644 --- a/server/models/abuse/video-comment-abuse.ts +++ b/server/models/abuse/video-comment-abuse.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoCommentModel } from '../video/video-comment' | 3 | import { VideoCommentModel } from '../video/video-comment' |
3 | import { AbuseModel } from './abuse' | 4 | import { AbuseModel } from './abuse' |
4 | 5 | ||
@@ -13,7 +14,7 @@ import { AbuseModel } from './abuse' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class VideoCommentAbuseModel extends Model { | 17 | export class VideoCommentAbuseModel extends Model<Partial<AttributesOnly<VideoCommentAbuseModel>>> { |
17 | 18 | ||
18 | @CreatedAt | 19 | @CreatedAt |
19 | createdAt: Date | 20 | createdAt: Date |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index fe9168ab8..b2375b006 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { Op } from 'sequelize' | 1 | import { Op } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' | 3 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { AccountBlock } from '../../../shared/models' | 5 | import { AccountBlock } from '../../../shared/models' |
5 | import { ActorModel } from '../activitypub/actor' | 6 | import { ActorModel } from '../actor/actor' |
6 | import { ServerModel } from '../server/server' | 7 | import { ServerModel } from '../server/server' |
7 | import { getSort, searchAttribute } from '../utils' | 8 | import { getSort, searchAttribute } from '../utils' |
8 | import { AccountModel } from './account' | 9 | import { AccountModel } from './account' |
@@ -40,7 +41,7 @@ enum ScopeNames { | |||
40 | } | 41 | } |
41 | ] | 42 | ] |
42 | }) | 43 | }) |
43 | export class AccountBlocklistModel extends Model { | 44 | export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountBlocklistModel>>> { |
44 | 45 | ||
45 | @CreatedAt | 46 | @CreatedAt |
46 | createdAt: Date | 47 | createdAt: Date |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 801f76bba..ee6dbc6da 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -7,11 +7,12 @@ import { | |||
7 | MAccountVideoRateAccountVideo, | 7 | MAccountVideoRateAccountVideo, |
8 | MAccountVideoRateFormattable | 8 | MAccountVideoRateFormattable |
9 | } from '@server/types/models/video/video-rate' | 9 | } from '@server/types/models/video/video-rate' |
10 | import { AttributesOnly } from '@shared/core-utils' | ||
10 | import { AccountVideoRate } from '../../../shared' | 11 | import { AccountVideoRate } from '../../../shared' |
11 | import { VideoRateType } from '../../../shared/models/videos' | 12 | import { VideoRateType } from '../../../shared/models/videos' |
12 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 13 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
13 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' | 14 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' |
14 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../actor/actor' |
15 | import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' | 16 | import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' |
16 | import { VideoModel } from '../video/video' | 17 | import { VideoModel } from '../video/video' |
17 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' | 18 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' |
@@ -42,7 +43,7 @@ import { AccountModel } from './account' | |||
42 | } | 43 | } |
43 | ] | 44 | ] |
44 | }) | 45 | }) |
45 | export class AccountVideoRateModel extends Model { | 46 | export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountVideoRateModel>>> { |
46 | 47 | ||
47 | @AllowNull(false) | 48 | @AllowNull(false) |
48 | @Column(DataType.ENUM(...values(VIDEO_RATE_TYPES))) | 49 | @Column(DataType.ENUM(...values(VIDEO_RATE_TYPES))) |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index d33353af7..665ecd595 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -17,10 +17,11 @@ import { | |||
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { ModelCache } from '@server/models/model-cache' | 19 | import { ModelCache } from '@server/models/model-cache' |
20 | import { AttributesOnly } from '@shared/core-utils' | ||
20 | import { Account, AccountSummary } from '../../../shared/models/actors' | 21 | import { Account, AccountSummary } from '../../../shared/models/actors' |
21 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' | 22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' |
22 | import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' | 23 | import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' |
23 | import { sendDeleteActor } from '../../lib/activitypub/send' | 24 | import { sendDeleteActor } from '../../lib/activitypub/send/send-delete' |
24 | import { | 25 | import { |
25 | MAccount, | 26 | MAccount, |
26 | MAccountActor, | 27 | MAccountActor, |
@@ -30,19 +31,19 @@ import { | |||
30 | MAccountSummaryFormattable, | 31 | MAccountSummaryFormattable, |
31 | MChannelActor | 32 | MChannelActor |
32 | } from '../../types/models' | 33 | } from '../../types/models' |
33 | import { ActorModel } from '../activitypub/actor' | 34 | import { ActorModel } from '../actor/actor' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 35 | import { ActorFollowModel } from '../actor/actor-follow' |
36 | import { ActorImageModel } from '../actor/actor-image' | ||
35 | import { ApplicationModel } from '../application/application' | 37 | import { ApplicationModel } from '../application/application' |
36 | import { ActorImageModel } from './actor-image' | ||
37 | import { ServerModel } from '../server/server' | 38 | import { ServerModel } from '../server/server' |
38 | import { ServerBlocklistModel } from '../server/server-blocklist' | 39 | import { ServerBlocklistModel } from '../server/server-blocklist' |
40 | import { UserModel } from '../user/user' | ||
39 | import { getSort, throwIfNotValid } from '../utils' | 41 | import { getSort, throwIfNotValid } from '../utils' |
40 | import { VideoModel } from '../video/video' | 42 | import { VideoModel } from '../video/video' |
41 | import { VideoChannelModel } from '../video/video-channel' | 43 | import { VideoChannelModel } from '../video/video-channel' |
42 | import { VideoCommentModel } from '../video/video-comment' | 44 | import { VideoCommentModel } from '../video/video-comment' |
43 | import { VideoPlaylistModel } from '../video/video-playlist' | 45 | import { VideoPlaylistModel } from '../video/video-playlist' |
44 | import { AccountBlocklistModel } from './account-blocklist' | 46 | import { AccountBlocklistModel } from './account-blocklist' |
45 | import { UserModel } from './user' | ||
46 | 47 | ||
47 | export enum ScopeNames { | 48 | export enum ScopeNames { |
48 | SUMMARY = 'SUMMARY' | 49 | SUMMARY = 'SUMMARY' |
@@ -141,7 +142,7 @@ export type SummaryOptions = { | |||
141 | } | 142 | } |
142 | ] | 143 | ] |
143 | }) | 144 | }) |
144 | export class AccountModel extends Model { | 145 | export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { |
145 | 146 | ||
146 | @AllowNull(false) | 147 | @AllowNull(false) |
147 | @Column | 148 | @Column |
diff --git a/server/models/account/actor-custom-page.ts b/server/models/account/actor-custom-page.ts new file mode 100644 index 000000000..893023181 --- /dev/null +++ b/server/models/account/actor-custom-page.ts | |||
@@ -0,0 +1,69 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { CustomPage } from '@shared/models' | ||
3 | import { ActorModel } from '../actor/actor' | ||
4 | import { getServerActor } from '../application/application' | ||
5 | |||
6 | @Table({ | ||
7 | tableName: 'actorCustomPage', | ||
8 | indexes: [ | ||
9 | { | ||
10 | fields: [ 'actorId', 'type' ], | ||
11 | unique: true | ||
12 | } | ||
13 | ] | ||
14 | }) | ||
15 | export class ActorCustomPageModel extends Model { | ||
16 | |||
17 | @AllowNull(true) | ||
18 | @Column(DataType.TEXT) | ||
19 | content: string | ||
20 | |||
21 | @AllowNull(false) | ||
22 | @Column | ||
23 | type: 'homepage' | ||
24 | |||
25 | @CreatedAt | ||
26 | createdAt: Date | ||
27 | |||
28 | @UpdatedAt | ||
29 | updatedAt: Date | ||
30 | |||
31 | @ForeignKey(() => ActorModel) | ||
32 | @Column | ||
33 | actorId: number | ||
34 | |||
35 | @BelongsTo(() => ActorModel, { | ||
36 | foreignKey: { | ||
37 | name: 'actorId', | ||
38 | allowNull: false | ||
39 | }, | ||
40 | onDelete: 'cascade' | ||
41 | }) | ||
42 | Actor: ActorModel | ||
43 | |||
44 | static async updateInstanceHomepage (content: string) { | ||
45 | const serverActor = await getServerActor() | ||
46 | |||
47 | return ActorCustomPageModel.upsert({ | ||
48 | content, | ||
49 | actorId: serverActor.id, | ||
50 | type: 'homepage' | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | static async loadInstanceHomepage () { | ||
55 | const serverActor = await getServerActor() | ||
56 | |||
57 | return ActorCustomPageModel.findOne({ | ||
58 | where: { | ||
59 | actorId: serverActor.id | ||
60 | } | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | toFormattedJSON (): CustomPage { | ||
65 | return { | ||
66 | content: this.content | ||
67 | } | ||
68 | } | ||
69 | } | ||
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/actor/actor-follow.ts index 4c5f37620..3a09e51d6 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -28,6 +28,7 @@ import { | |||
28 | MActorFollowFormattable, | 28 | MActorFollowFormattable, |
29 | MActorFollowSubscriptions | 29 | MActorFollowSubscriptions |
30 | } from '@server/types/models' | 30 | } from '@server/types/models' |
31 | import { AttributesOnly } from '@shared/core-utils' | ||
31 | import { ActivityPubActorType } from '@shared/models' | 32 | import { ActivityPubActorType } from '@shared/models' |
32 | import { FollowState } from '../../../shared/models/actors' | 33 | import { FollowState } from '../../../shared/models/actors' |
33 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | 34 | import { ActorFollow } from '../../../shared/models/actors/follow.model' |
@@ -61,7 +62,7 @@ import { ActorModel, unusedActorAttributesForAPI } from './actor' | |||
61 | } | 62 | } |
62 | ] | 63 | ] |
63 | }) | 64 | }) |
64 | export class ActorFollowModel extends Model { | 65 | export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowModel>>> { |
65 | 66 | ||
66 | @AllowNull(false) | 67 | @AllowNull(false) |
67 | @Column(DataType.ENUM(...values(FOLLOW_STATES))) | 68 | @Column(DataType.ENUM(...values(FOLLOW_STATES))) |
@@ -619,7 +620,7 @@ export class ActorFollowModel extends Model { | |||
619 | if (serverIds.length === 0) return | 620 | if (serverIds.length === 0) return |
620 | 621 | ||
621 | const me = await getServerActor() | 622 | const me = await getServerActor() |
622 | const serverIdsString = createSafeIn(ActorFollowModel, serverIds) | 623 | const serverIdsString = createSafeIn(ActorFollowModel.sequelize, serverIds) |
623 | 624 | ||
624 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | 625 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + |
625 | 'WHERE id IN (' + | 626 | 'WHERE id IN (' + |
diff --git a/server/models/account/actor-image.ts b/server/models/actor/actor-image.ts index ae05b4969..98a7f6fba 100644 --- a/server/models/account/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -2,6 +2,7 @@ import { remove } from 'fs-extra' | |||
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
4 | import { MActorImageFormattable } from '@server/types/models' | 4 | import { MActorImageFormattable } from '@server/types/models' |
5 | import { AttributesOnly } from '@shared/core-utils' | ||
5 | import { ActorImageType } from '@shared/models' | 6 | import { ActorImageType } from '@shared/models' |
6 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | 7 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
@@ -19,7 +20,7 @@ import { throwIfNotValid } from '../utils' | |||
19 | } | 20 | } |
20 | ] | 21 | ] |
21 | }) | 22 | }) |
22 | export class ActorImageModel extends Model { | 23 | export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageModel>>> { |
23 | 24 | ||
24 | @AllowNull(false) | 25 | @AllowNull(false) |
25 | @Column | 26 | @Column |
@@ -97,4 +98,8 @@ export class ActorImageModel extends Model { | |||
97 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | 98 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) |
98 | return remove(imagePath) | 99 | return remove(imagePath) |
99 | } | 100 | } |
101 | |||
102 | isOwned () { | ||
103 | return !this.fileUrl | ||
104 | } | ||
100 | } | 105 | } |
diff --git a/server/models/activitypub/actor.ts b/server/models/actor/actor.ts index 1af9efac2..8df49951d 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import { extname } from 'path' | ||
3 | import { literal, Op, Transaction } from 'sequelize' | 2 | import { literal, Op, Transaction } from 'sequelize' |
4 | import { | 3 | import { |
5 | AllowNull, | 4 | AllowNull, |
@@ -17,7 +16,9 @@ import { | |||
17 | Table, | 16 | Table, |
18 | UpdatedAt | 17 | UpdatedAt |
19 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { getLowercaseExtension } from '@server/helpers/core-utils' | ||
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
21 | import { AttributesOnly } from '@shared/core-utils' | ||
21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | 22 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' |
22 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | 23 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
23 | import { activityPubContextify } from '../../helpers/activitypub' | 24 | import { activityPubContextify } from '../../helpers/activitypub' |
@@ -51,12 +52,12 @@ import { | |||
51 | MActorWithInboxes | 52 | MActorWithInboxes |
52 | } from '../../types/models' | 53 | } from '../../types/models' |
53 | import { AccountModel } from '../account/account' | 54 | import { AccountModel } from '../account/account' |
54 | import { ActorImageModel } from '../account/actor-image' | ||
55 | import { ServerModel } from '../server/server' | 55 | import { ServerModel } from '../server/server' |
56 | import { isOutdated, throwIfNotValid } from '../utils' | 56 | import { isOutdated, throwIfNotValid } from '../utils' |
57 | import { VideoModel } from '../video/video' | 57 | import { VideoModel } from '../video/video' |
58 | import { VideoChannelModel } from '../video/video-channel' | 58 | import { VideoChannelModel } from '../video/video-channel' |
59 | import { ActorFollowModel } from './actor-follow' | 59 | import { ActorFollowModel } from './actor-follow' |
60 | import { ActorImageModel } from './actor-image' | ||
60 | 61 | ||
61 | enum ScopeNames { | 62 | enum ScopeNames { |
62 | FULL = 'FULL' | 63 | FULL = 'FULL' |
@@ -159,7 +160,7 @@ export const unusedActorAttributesForAPI = [ | |||
159 | } | 160 | } |
160 | ] | 161 | ] |
161 | }) | 162 | }) |
162 | export class ActorModel extends Model { | 163 | export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { |
163 | 164 | ||
164 | @AllowNull(false) | 165 | @AllowNull(false) |
165 | @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) | 166 | @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) |
@@ -495,7 +496,7 @@ export class ActorModel extends Model { | |||
495 | }, { where, transaction }) | 496 | }, { where, transaction }) |
496 | } | 497 | } |
497 | 498 | ||
498 | static loadAccountActorByVideoId (videoId: number): Promise<MActor> { | 499 | static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> { |
499 | const query = { | 500 | const query = { |
500 | include: [ | 501 | include: [ |
501 | { | 502 | { |
@@ -519,7 +520,8 @@ export class ActorModel extends Model { | |||
519 | } | 520 | } |
520 | ] | 521 | ] |
521 | } | 522 | } |
522 | ] | 523 | ], |
524 | transaction | ||
523 | } | 525 | } |
524 | 526 | ||
525 | return ActorModel.unscoped().findOne(query) | 527 | return ActorModel.unscoped().findOne(query) |
@@ -566,7 +568,7 @@ export class ActorModel extends Model { | |||
566 | let image: ActivityIconObject | 568 | let image: ActivityIconObject |
567 | 569 | ||
568 | if (this.avatarId) { | 570 | if (this.avatarId) { |
569 | const extension = extname(this.Avatar.filename) | 571 | const extension = getLowercaseExtension(this.Avatar.filename) |
570 | 572 | ||
571 | icon = { | 573 | icon = { |
572 | type: 'Image', | 574 | type: 'Image', |
@@ -579,7 +581,7 @@ export class ActorModel extends Model { | |||
579 | 581 | ||
580 | if (this.bannerId) { | 582 | if (this.bannerId) { |
581 | const banner = (this as MActorAPChannel).Banner | 583 | const banner = (this as MActorAPChannel).Banner |
582 | const extension = extname(banner.filename) | 584 | const extension = getLowercaseExtension(banner.filename) |
583 | 585 | ||
584 | image = { | 586 | image = { |
585 | type: 'Image', | 587 | type: 'Image', |
diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 21f8b1cbc..5531d134a 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import * as memoizee from 'memoizee' | ||
1 | import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' | 2 | import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { AccountModel } from '../account/account' | 4 | import { AccountModel } from '../account/account' |
3 | import * as memoizee from 'memoizee' | ||
4 | 5 | ||
5 | export const getServerActor = memoizee(async function () { | 6 | export const getServerActor = memoizee(async function () { |
6 | const application = await ApplicationModel.load() | 7 | const application = await ApplicationModel.load() |
@@ -24,7 +25,7 @@ export const getServerActor = memoizee(async function () { | |||
24 | tableName: 'application', | 25 | tableName: 'application', |
25 | timestamps: false | 26 | timestamps: false |
26 | }) | 27 | }) |
27 | export class ApplicationModel extends Model { | 28 | export class ApplicationModel extends Model<Partial<AttributesOnly<ApplicationModel>>> { |
28 | 29 | ||
29 | @AllowNull(false) | 30 | @AllowNull(false) |
30 | @Default(0) | 31 | @Default(0) |
diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts index 8dbc1c2f5..890954bdb 100644 --- a/server/models/oauth/oauth-client.ts +++ b/server/models/oauth/oauth-client.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { OAuthTokenModel } from './oauth-token' | 3 | import { OAuthTokenModel } from './oauth-token' |
3 | 4 | ||
4 | @Table({ | 5 | @Table({ |
@@ -14,7 +15,7 @@ import { OAuthTokenModel } from './oauth-token' | |||
14 | } | 15 | } |
15 | ] | 16 | ] |
16 | }) | 17 | }) |
17 | export class OAuthClientModel extends Model { | 18 | export class OAuthClientModel extends Model<Partial<AttributesOnly<OAuthClientModel>>> { |
18 | 19 | ||
19 | @AllowNull(false) | 20 | @AllowNull(false) |
20 | @Column | 21 | @Column |
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 27e643aa7..af4b0ec42 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -15,10 +15,11 @@ import { | |||
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | 15 | import { TokensCache } from '@server/lib/auth/tokens-cache' |
16 | import { MUserAccountId } from '@server/types/models' | 16 | import { MUserAccountId } from '@server/types/models' |
17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
18 | import { AttributesOnly } from '@shared/core-utils' | ||
18 | import { logger } from '../../helpers/logger' | 19 | import { logger } from '../../helpers/logger' |
19 | import { AccountModel } from '../account/account' | 20 | import { AccountModel } from '../account/account' |
20 | import { UserModel } from '../account/user' | 21 | import { ActorModel } from '../actor/actor' |
21 | import { ActorModel } from '../activitypub/actor' | 22 | import { UserModel } from '../user/user' |
22 | import { OAuthClientModel } from './oauth-client' | 23 | import { OAuthClientModel } from './oauth-client' |
23 | 24 | ||
24 | export type OAuthTokenInfo = { | 25 | export type OAuthTokenInfo = { |
@@ -78,7 +79,7 @@ enum ScopeNames { | |||
78 | } | 79 | } |
79 | ] | 80 | ] |
80 | }) | 81 | }) |
81 | export class OAuthTokenModel extends Model { | 82 | export class OAuthTokenModel extends Model<Partial<AttributesOnly<OAuthTokenModel>>> { |
82 | 83 | ||
83 | @AllowNull(false) | 84 | @AllowNull(false) |
84 | @Column | 85 | @Column |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 349dba513..ccda023e0 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -16,6 +16,7 @@ import { | |||
16 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { getServerActor } from '@server/models/application/application' | 17 | import { getServerActor } from '@server/models/application/application' |
18 | import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models' | 18 | import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models' |
19 | import { AttributesOnly } from '@shared/core-utils' | ||
19 | import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model' | 20 | import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model' |
20 | import { | 21 | import { |
21 | FileRedundancyInformation, | 22 | FileRedundancyInformation, |
@@ -29,7 +30,7 @@ import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validato | |||
29 | import { logger } from '../../helpers/logger' | 30 | import { logger } from '../../helpers/logger' |
30 | import { CONFIG } from '../../initializers/config' | 31 | import { CONFIG } from '../../initializers/config' |
31 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | 32 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
32 | import { ActorModel } from '../activitypub/actor' | 33 | import { ActorModel } from '../actor/actor' |
33 | import { ServerModel } from '../server/server' | 34 | import { ServerModel } from '../server/server' |
34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 35 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
35 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | 36 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' |
@@ -79,12 +80,15 @@ export enum ScopeNames { | |||
79 | fields: [ 'actorId' ] | 80 | fields: [ 'actorId' ] |
80 | }, | 81 | }, |
81 | { | 82 | { |
83 | fields: [ 'expiresOn' ] | ||
84 | }, | ||
85 | { | ||
82 | fields: [ 'url' ], | 86 | fields: [ 'url' ], |
83 | unique: true | 87 | unique: true |
84 | } | 88 | } |
85 | ] | 89 | ] |
86 | }) | 90 | }) |
87 | export class VideoRedundancyModel extends Model { | 91 | export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedundancyModel>>> { |
88 | 92 | ||
89 | @CreatedAt | 93 | @CreatedAt |
90 | createdAt: Date | 94 | createdAt: Date |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 80c8a6be5..a8de64dd4 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { FindAndCountOptions, json, QueryTypes } from 'sequelize' | 1 | import { FindAndCountOptions, json, QueryTypes } from 'sequelize' |
2 | import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MPlugin, MPluginFormattable } from '@server/types/models' | 3 | import { MPlugin, MPluginFormattable } from '@server/types/models' |
4 | import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model' | 4 | import { AttributesOnly } from '@shared/core-utils' |
5 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 5 | import { PeerTubePlugin, PluginType, RegisterServerSettingOptions } from '../../../shared/models' |
6 | import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model' | ||
7 | import { | 6 | import { |
8 | isPluginDescriptionValid, | 7 | isPluginDescriptionValid, |
9 | isPluginHomepage, | 8 | isPluginHomepage, |
@@ -28,7 +27,7 @@ import { getSort, throwIfNotValid } from '../utils' | |||
28 | } | 27 | } |
29 | ] | 28 | ] |
30 | }) | 29 | }) |
31 | export class PluginModel extends Model { | 30 | export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> { |
32 | 31 | ||
33 | @AllowNull(false) | 32 | @AllowNull(false) |
34 | @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name')) | 33 | @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name')) |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 4dc236537..b3579d589 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Op } from 'sequelize' | 1 | import { Op } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | 3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { ServerBlock } from '@shared/models' | 5 | import { ServerBlock } from '@shared/models' |
5 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
6 | import { getSort, searchAttribute } from '../utils' | 7 | import { getSort, searchAttribute } from '../utils' |
@@ -42,7 +43,7 @@ enum ScopeNames { | |||
42 | } | 43 | } |
43 | ] | 44 | ] |
44 | }) | 45 | }) |
45 | export class ServerBlocklistModel extends Model { | 46 | export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlocklistModel>>> { |
46 | 47 | ||
47 | @CreatedAt | 48 | @CreatedAt |
48 | createdAt: Date | 49 | createdAt: Date |
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index 0e58beeaf..0d3c092e0 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
1 | import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { MServer, MServerFormattable } from '@server/types/models/server' | 3 | import { MServer, MServerFormattable } from '@server/types/models/server' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { isHostValid } from '../../helpers/custom-validators/servers' | 5 | import { isHostValid } from '../../helpers/custom-validators/servers' |
4 | import { ActorModel } from '../activitypub/actor' | 6 | import { ActorModel } from '../actor/actor' |
5 | import { throwIfNotValid } from '../utils' | 7 | import { throwIfNotValid } from '../utils' |
6 | import { ServerBlocklistModel } from './server-blocklist' | 8 | import { ServerBlocklistModel } from './server-blocklist' |
7 | 9 | ||
@@ -14,7 +16,7 @@ import { ServerBlocklistModel } from './server-blocklist' | |||
14 | } | 16 | } |
15 | ] | 17 | ] |
16 | }) | 18 | }) |
17 | export class ServerModel extends Model { | 19 | export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> { |
18 | 20 | ||
19 | @AllowNull(false) | 21 | @AllowNull(false) |
20 | @Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host')) | 22 | @Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host')) |
@@ -50,11 +52,12 @@ export class ServerModel extends Model { | |||
50 | }) | 52 | }) |
51 | BlockedByAccounts: ServerBlocklistModel[] | 53 | BlockedByAccounts: ServerBlocklistModel[] |
52 | 54 | ||
53 | static load (id: number): Promise<MServer> { | 55 | static load (id: number, transaction?: Transaction): Promise<MServer> { |
54 | const query = { | 56 | const query = { |
55 | where: { | 57 | where: { |
56 | id | 58 | id |
57 | } | 59 | }, |
60 | transaction | ||
58 | } | 61 | } |
59 | 62 | ||
60 | return ServerModel.findOne(query) | 63 | return ServerModel.findOne(query) |
diff --git a/server/models/server/tracker.ts b/server/models/server/tracker.ts index 97520f92d..c09fdd64b 100644 --- a/server/models/server/tracker.ts +++ b/server/models/server/tracker.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { Transaction } from 'sequelize/types' | 2 | import { Transaction } from 'sequelize/types' |
3 | import { MTracker } from '@server/types/models/server/tracker' | 3 | import { MTracker } from '@server/types/models/server/tracker' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { VideoModel } from '../video/video' | 5 | import { VideoModel } from '../video/video' |
5 | import { VideoTrackerModel } from './video-tracker' | 6 | import { VideoTrackerModel } from './video-tracker' |
6 | 7 | ||
@@ -13,7 +14,7 @@ import { VideoTrackerModel } from './video-tracker' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class TrackerModel extends Model { | 17 | export class TrackerModel extends Model<Partial<AttributesOnly<TrackerModel>>> { |
17 | 18 | ||
18 | @AllowNull(false) | 19 | @AllowNull(false) |
19 | @Column | 20 | @Column |
diff --git a/server/models/server/video-tracker.ts b/server/models/server/video-tracker.ts index 367bf0117..c49fbd1c6 100644 --- a/server/models/server/video-tracker.ts +++ b/server/models/server/video-tracker.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoModel } from '../video/video' | 3 | import { VideoModel } from '../video/video' |
3 | import { TrackerModel } from './tracker' | 4 | import { TrackerModel } from './tracker' |
4 | 5 | ||
@@ -13,7 +14,7 @@ import { TrackerModel } from './tracker' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class VideoTrackerModel extends Model { | 17 | export class VideoTrackerModel extends Model<Partial<AttributesOnly<VideoTrackerModel>>> { |
17 | @CreatedAt | 18 | @CreatedAt |
18 | createdAt: Date | 19 | createdAt: Date |
19 | 20 | ||
diff --git a/server/models/account/user-notification-setting.ts b/server/models/user/user-notification-setting.ts index 138051528..bee7d7851 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/user/user-notification-setting.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | 15 | import { TokensCache } from '@server/lib/auth/tokens-cache' |
16 | import { MNotificationSettingFormattable } from '@server/types/models' | 16 | import { MNotificationSettingFormattable } from '@server/types/models' |
17 | import { AttributesOnly } from '@shared/core-utils' | ||
17 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
18 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
19 | import { throwIfNotValid } from '../utils' | 20 | import { throwIfNotValid } from '../utils' |
@@ -28,7 +29,7 @@ import { UserModel } from './user' | |||
28 | } | 29 | } |
29 | ] | 30 | ] |
30 | }) | 31 | }) |
31 | export class UserNotificationSettingModel extends Model { | 32 | export class UserNotificationSettingModel extends Model<Partial<AttributesOnly<UserNotificationSettingModel>>> { |
32 | 33 | ||
33 | @AllowNull(false) | 34 | @AllowNull(false) |
34 | @Default(null) | 35 | @Default(null) |
diff --git a/server/models/account/user-notification.ts b/server/models/user/user-notification.ts index 805095002..a7f84e9ca 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -1,14 +1,17 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | 1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { UserNotification, UserNotificationType } from '../../../shared' | 5 | import { UserNotification, UserNotificationType } from '../../../shared' |
5 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 6 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
6 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | 7 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' |
7 | import { AbuseModel } from '../abuse/abuse' | 8 | import { AbuseModel } from '../abuse/abuse' |
8 | import { VideoAbuseModel } from '../abuse/video-abuse' | 9 | import { VideoAbuseModel } from '../abuse/video-abuse' |
9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 10 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
10 | import { ActorModel } from '../activitypub/actor' | 11 | import { AccountModel } from '../account/account' |
11 | import { ActorFollowModel } from '../activitypub/actor-follow' | 12 | import { ActorModel } from '../actor/actor' |
13 | import { ActorFollowModel } from '../actor/actor-follow' | ||
14 | import { ActorImageModel } from '../actor/actor-image' | ||
12 | import { ApplicationModel } from '../application/application' | 15 | import { ApplicationModel } from '../application/application' |
13 | import { PluginModel } from '../server/plugin' | 16 | import { PluginModel } from '../server/plugin' |
14 | import { ServerModel } from '../server/server' | 17 | import { ServerModel } from '../server/server' |
@@ -18,8 +21,6 @@ import { VideoBlacklistModel } from '../video/video-blacklist' | |||
18 | import { VideoChannelModel } from '../video/video-channel' | 21 | import { VideoChannelModel } from '../video/video-channel' |
19 | import { VideoCommentModel } from '../video/video-comment' | 22 | import { VideoCommentModel } from '../video/video-comment' |
20 | import { VideoImportModel } from '../video/video-import' | 23 | import { VideoImportModel } from '../video/video-import' |
21 | import { AccountModel } from './account' | ||
22 | import { ActorImageModel } from './actor-image' | ||
23 | import { UserModel } from './user' | 24 | import { UserModel } from './user' |
24 | 25 | ||
25 | enum ScopeNames { | 26 | enum ScopeNames { |
@@ -286,7 +287,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
286 | } | 287 | } |
287 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] | 288 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] |
288 | }) | 289 | }) |
289 | export class UserNotificationModel extends Model { | 290 | export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> { |
290 | 291 | ||
291 | @AllowNull(false) | 292 | @AllowNull(false) |
292 | @Default(null) | 293 | @Default(null) |
diff --git a/server/models/account/user-video-history.ts b/server/models/user/user-video-history.ts index 6be1d65ea..e3dc4a062 100644 --- a/server/models/account/user-video-history.ts +++ b/server/models/user/user-video-history.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { DestroyOptions, Op, Transaction } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
4 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoModel } from '../video/video' | 5 | import { VideoModel } from '../video/video' |
3 | import { UserModel } from './user' | 6 | import { UserModel } from './user' |
4 | import { DestroyOptions, Op, Transaction } from 'sequelize' | ||
5 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
6 | 7 | ||
7 | @Table({ | 8 | @Table({ |
8 | tableName: 'userVideoHistory', | 9 | tableName: 'userVideoHistory', |
@@ -19,7 +20,7 @@ import { MUserAccountId, MUserId } from '@server/types/models' | |||
19 | } | 20 | } |
20 | ] | 21 | ] |
21 | }) | 22 | }) |
22 | export class UserVideoHistoryModel extends Model { | 23 | export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVideoHistoryModel>>> { |
23 | @CreatedAt | 24 | @CreatedAt |
24 | createdAt: Date | 25 | createdAt: Date |
25 | 26 | ||
diff --git a/server/models/account/user.ts b/server/models/user/user.ts index 513455773..20696b1f4 100644 --- a/server/models/account/user.ts +++ b/server/models/user/user.ts | |||
@@ -31,6 +31,7 @@ import { | |||
31 | MUserWithNotificationSetting, | 31 | MUserWithNotificationSetting, |
32 | MVideoWithRights | 32 | MVideoWithRights |
33 | } from '@server/types/models' | 33 | } from '@server/types/models' |
34 | import { AttributesOnly } from '@shared/core-utils' | ||
34 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' | 35 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' |
35 | import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' | 36 | import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' |
36 | import { User, UserRole } from '../../../shared/models/users' | 37 | import { User, UserRole } from '../../../shared/models/users' |
@@ -60,8 +61,10 @@ import { | |||
60 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 61 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
61 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 62 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | 63 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' |
63 | import { ActorModel } from '../activitypub/actor' | 64 | import { AccountModel } from '../account/account' |
64 | import { ActorFollowModel } from '../activitypub/actor-follow' | 65 | import { ActorModel } from '../actor/actor' |
66 | import { ActorFollowModel } from '../actor/actor-follow' | ||
67 | import { ActorImageModel } from '../actor/actor-image' | ||
65 | import { OAuthTokenModel } from '../oauth/oauth-token' | 68 | import { OAuthTokenModel } from '../oauth/oauth-token' |
66 | import { getSort, throwIfNotValid } from '../utils' | 69 | import { getSort, throwIfNotValid } from '../utils' |
67 | import { VideoModel } from '../video/video' | 70 | import { VideoModel } from '../video/video' |
@@ -69,9 +72,7 @@ import { VideoChannelModel } from '../video/video-channel' | |||
69 | import { VideoImportModel } from '../video/video-import' | 72 | import { VideoImportModel } from '../video/video-import' |
70 | import { VideoLiveModel } from '../video/video-live' | 73 | import { VideoLiveModel } from '../video/video-live' |
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 74 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { AccountModel } from './account' | ||
73 | import { UserNotificationSettingModel } from './user-notification-setting' | 75 | import { UserNotificationSettingModel } from './user-notification-setting' |
74 | import { ActorImageModel } from './actor-image' | ||
75 | 76 | ||
76 | enum ScopeNames { | 77 | enum ScopeNames { |
77 | FOR_ME_API = 'FOR_ME_API', | 78 | FOR_ME_API = 'FOR_ME_API', |
@@ -233,7 +234,7 @@ enum ScopeNames { | |||
233 | } | 234 | } |
234 | ] | 235 | ] |
235 | }) | 236 | }) |
236 | export class UserModel extends Model { | 237 | export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { |
237 | 238 | ||
238 | @AllowNull(true) | 239 | @AllowNull(true) |
239 | @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) | 240 | @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) |
diff --git a/server/models/utils.ts b/server/models/utils.ts index ec51c66bf..83b2b8f03 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { literal, Op, OrderItem } from 'sequelize' | 1 | import { literal, Op, OrderItem, Sequelize } from 'sequelize' |
2 | import { Model, Sequelize } from 'sequelize-typescript' | ||
3 | import { Col } from 'sequelize/types/lib/utils' | 2 | import { Col } from 'sequelize/types/lib/utils' |
4 | import validator from 'validator' | 3 | import validator from 'validator' |
5 | 4 | ||
@@ -103,6 +102,10 @@ function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): | |||
103 | } | 102 | } |
104 | 103 | ||
105 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | 104 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { |
105 | if (!model.createdAt || !model.updatedAt) { | ||
106 | throw new Error('Miss createdAt & updatedAt attribuets to model') | ||
107 | } | ||
108 | |||
106 | const now = Date.now() | 109 | const now = Date.now() |
107 | const createdAtTime = model.createdAt.getTime() | 110 | const createdAtTime = model.createdAt.getTime() |
108 | const updatedAtTime = model.updatedAt.getTime() | 111 | const updatedAtTime = model.updatedAt.getTime() |
@@ -195,11 +198,11 @@ function parseAggregateResult (result: any) { | |||
195 | return total | 198 | return total |
196 | } | 199 | } |
197 | 200 | ||
198 | const createSafeIn = (model: typeof Model, stringArr: (string | number)[]) => { | 201 | function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) { |
199 | return stringArr.map(t => { | 202 | return stringArr.map(t => { |
200 | return t === null | 203 | return t === null |
201 | ? null | 204 | ? null |
202 | : model.sequelize.escape('' + t) | 205 | : sequelize.escape('' + t) |
203 | }).join(', ') | 206 | }).join(', ') |
204 | } | 207 | } |
205 | 208 | ||
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index 551cb2842..6b1e59063 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -1,17 +1,26 @@ | |||
1 | import { uuidToShort } from '@server/helpers/uuid' | ||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | 2 | import { generateMagnetUri } from '@server/helpers/webtorrent' |
2 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths' | 3 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths' |
3 | import { VideoFile } from '@shared/models/videos/video-file.model' | 4 | import { VideoFile } from '@shared/models/videos/video-file.model' |
4 | import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' | 5 | import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects' |
5 | import { Video, VideoDetails } from '../../../shared/models/videos' | 6 | import { Video, VideoDetails } from '../../../../shared/models/videos' |
6 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | 7 | import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model' |
7 | import { isArray } from '../../helpers/custom-validators/misc' | 8 | import { isArray } from '../../../helpers/custom-validators/misc' |
8 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | 9 | import { |
10 | MIMETYPES, | ||
11 | VIDEO_CATEGORIES, | ||
12 | VIDEO_LANGUAGES, | ||
13 | VIDEO_LICENCES, | ||
14 | VIDEO_PRIVACIES, | ||
15 | VIDEO_STATES, | ||
16 | WEBSERVER | ||
17 | } from '../../../initializers/constants' | ||
9 | import { | 18 | import { |
10 | getLocalVideoCommentsActivityPubUrl, | 19 | getLocalVideoCommentsActivityPubUrl, |
11 | getLocalVideoDislikesActivityPubUrl, | 20 | getLocalVideoDislikesActivityPubUrl, |
12 | getLocalVideoLikesActivityPubUrl, | 21 | getLocalVideoLikesActivityPubUrl, |
13 | getLocalVideoSharesActivityPubUrl | 22 | getLocalVideoSharesActivityPubUrl |
14 | } from '../../lib/activitypub/url' | 23 | } from '../../../lib/activitypub/url' |
15 | import { | 24 | import { |
16 | MStreamingPlaylistRedundanciesOpt, | 25 | MStreamingPlaylistRedundanciesOpt, |
17 | MVideo, | 26 | MVideo, |
@@ -19,10 +28,9 @@ import { | |||
19 | MVideoFile, | 28 | MVideoFile, |
20 | MVideoFormattable, | 29 | MVideoFormattable, |
21 | MVideoFormattableDetails | 30 | MVideoFormattableDetails |
22 | } from '../../types/models' | 31 | } from '../../../types/models' |
23 | import { MVideoFileRedundanciesOpt } from '../../types/models/video/video-file' | 32 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' |
24 | import { VideoModel } from './video' | 33 | import { VideoCaptionModel } from '../video-caption' |
25 | import { VideoCaptionModel } from './video-caption' | ||
26 | 34 | ||
27 | export type VideoFormattingJSONOptions = { | 35 | export type VideoFormattingJSONOptions = { |
28 | completeDescription?: boolean | 36 | completeDescription?: boolean |
@@ -40,22 +48,24 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor | |||
40 | const videoObject: Video = { | 48 | const videoObject: Video = { |
41 | id: video.id, | 49 | id: video.id, |
42 | uuid: video.uuid, | 50 | uuid: video.uuid, |
51 | shortUUID: uuidToShort(video.uuid), | ||
52 | |||
43 | name: video.name, | 53 | name: video.name, |
44 | category: { | 54 | category: { |
45 | id: video.category, | 55 | id: video.category, |
46 | label: VideoModel.getCategoryLabel(video.category) | 56 | label: getCategoryLabel(video.category) |
47 | }, | 57 | }, |
48 | licence: { | 58 | licence: { |
49 | id: video.licence, | 59 | id: video.licence, |
50 | label: VideoModel.getLicenceLabel(video.licence) | 60 | label: getLicenceLabel(video.licence) |
51 | }, | 61 | }, |
52 | language: { | 62 | language: { |
53 | id: video.language, | 63 | id: video.language, |
54 | label: VideoModel.getLanguageLabel(video.language) | 64 | label: getLanguageLabel(video.language) |
55 | }, | 65 | }, |
56 | privacy: { | 66 | privacy: { |
57 | id: video.privacy, | 67 | id: video.privacy, |
58 | label: VideoModel.getPrivacyLabel(video.privacy) | 68 | label: getPrivacyLabel(video.privacy) |
59 | }, | 69 | }, |
60 | nsfw: video.nsfw, | 70 | nsfw: video.nsfw, |
61 | 71 | ||
@@ -93,7 +103,7 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor | |||
93 | if (options.additionalAttributes.state === true) { | 103 | if (options.additionalAttributes.state === true) { |
94 | videoObject.state = { | 104 | videoObject.state = { |
95 | id: video.state, | 105 | id: video.state, |
96 | label: VideoModel.getStateLabel(video.state) | 106 | label: getStateLabel(video.state) |
97 | } | 107 | } |
98 | } | 108 | } |
99 | 109 | ||
@@ -140,7 +150,7 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid | |||
140 | waitTranscoding: video.waitTranscoding, | 150 | waitTranscoding: video.waitTranscoding, |
141 | state: { | 151 | state: { |
142 | id: video.state, | 152 | id: video.state, |
143 | label: VideoModel.getStateLabel(video.state) | 153 | label: getStateLabel(video.state) |
144 | }, | 154 | }, |
145 | 155 | ||
146 | trackerUrls: video.getTrackerUrls(), | 156 | trackerUrls: video.getTrackerUrls(), |
@@ -202,7 +212,7 @@ function videoFilesModelToFormattedJSON ( | |||
202 | return { | 212 | return { |
203 | resolution: { | 213 | resolution: { |
204 | id: videoFile.resolution, | 214 | id: videoFile.resolution, |
205 | label: videoFile.resolution + 'p' | 215 | label: videoFile.resolution === 0 ? 'Audio' : `${videoFile.resolution}p` |
206 | }, | 216 | }, |
207 | 217 | ||
208 | magnetUri: includeMagnet && videoFile.hasTorrent() | 218 | magnetUri: includeMagnet && videoFile.hasTorrent() |
@@ -283,7 +293,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
283 | if (video.language) { | 293 | if (video.language) { |
284 | language = { | 294 | language = { |
285 | identifier: video.language, | 295 | identifier: video.language, |
286 | name: VideoModel.getLanguageLabel(video.language) | 296 | name: getLanguageLabel(video.language) |
287 | } | 297 | } |
288 | } | 298 | } |
289 | 299 | ||
@@ -291,7 +301,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
291 | if (video.category) { | 301 | if (video.category) { |
292 | category = { | 302 | category = { |
293 | identifier: video.category + '', | 303 | identifier: video.category + '', |
294 | name: VideoModel.getCategoryLabel(video.category) | 304 | name: getCategoryLabel(video.category) |
295 | } | 305 | } |
296 | } | 306 | } |
297 | 307 | ||
@@ -299,7 +309,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
299 | if (video.licence) { | 309 | if (video.licence) { |
300 | licence = { | 310 | licence = { |
301 | identifier: video.licence + '', | 311 | identifier: video.licence + '', |
302 | name: VideoModel.getLicenceLabel(video.licence) | 312 | name: getLicenceLabel(video.licence) |
303 | } | 313 | } |
304 | } | 314 | } |
305 | 315 | ||
@@ -425,10 +435,36 @@ function getActivityStreamDuration (duration: number) { | |||
425 | return 'PT' + duration + 'S' | 435 | return 'PT' + duration + 'S' |
426 | } | 436 | } |
427 | 437 | ||
438 | function getCategoryLabel (id: number) { | ||
439 | return VIDEO_CATEGORIES[id] || 'Misc' | ||
440 | } | ||
441 | |||
442 | function getLicenceLabel (id: number) { | ||
443 | return VIDEO_LICENCES[id] || 'Unknown' | ||
444 | } | ||
445 | |||
446 | function getLanguageLabel (id: string) { | ||
447 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
448 | } | ||
449 | |||
450 | function getPrivacyLabel (id: number) { | ||
451 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
452 | } | ||
453 | |||
454 | function getStateLabel (id: number) { | ||
455 | return VIDEO_STATES[id] || 'Unknown' | ||
456 | } | ||
457 | |||
428 | export { | 458 | export { |
429 | videoModelToFormattedJSON, | 459 | videoModelToFormattedJSON, |
430 | videoModelToFormattedDetailsJSON, | 460 | videoModelToFormattedDetailsJSON, |
431 | videoFilesModelToFormattedJSON, | 461 | videoFilesModelToFormattedJSON, |
432 | videoModelToActivityPubObject, | 462 | videoModelToActivityPubObject, |
433 | getActivityStreamDuration | 463 | getActivityStreamDuration, |
464 | |||
465 | getCategoryLabel, | ||
466 | getLicenceLabel, | ||
467 | getLanguageLabel, | ||
468 | getPrivacyLabel, | ||
469 | getStateLabel | ||
434 | } | 470 | } |
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts index 22b08e91a..d462c20c7 100644 --- a/server/models/video/schedule-video-update.ts +++ b/server/models/video/schedule-video-update.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { Op, Transaction } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' | 3 | import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdate } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { VideoPrivacy } from '../../../shared/models/videos' | 5 | import { VideoPrivacy } from '../../../shared/models/videos' |
4 | import { Op, Transaction } from 'sequelize' | 6 | import { VideoModel } from './video' |
5 | import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/types/models' | ||
6 | 7 | ||
7 | @Table({ | 8 | @Table({ |
8 | tableName: 'scheduleVideoUpdate', | 9 | tableName: 'scheduleVideoUpdate', |
@@ -16,7 +17,7 @@ import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@ | |||
16 | } | 17 | } |
17 | ] | 18 | ] |
18 | }) | 19 | }) |
19 | export class ScheduleVideoUpdateModel extends Model { | 20 | export class ScheduleVideoUpdateModel extends Model<Partial<AttributesOnly<ScheduleVideoUpdateModel>>> { |
20 | 21 | ||
21 | @AllowNull(false) | 22 | @AllowNull(false) |
22 | @Default(null) | 23 | @Default(null) |
@@ -61,31 +62,17 @@ export class ScheduleVideoUpdateModel extends Model { | |||
61 | .then(res => !!res) | 62 | .then(res => !!res) |
62 | } | 63 | } |
63 | 64 | ||
64 | static listVideosToUpdate (t: Transaction) { | 65 | static listVideosToUpdate (transaction?: Transaction) { |
65 | const query = { | 66 | const query = { |
66 | where: { | 67 | where: { |
67 | updateAt: { | 68 | updateAt: { |
68 | [Op.lte]: new Date() | 69 | [Op.lte]: new Date() |
69 | } | 70 | } |
70 | }, | 71 | }, |
71 | include: [ | 72 | transaction |
72 | { | ||
73 | model: VideoModel.scope( | ||
74 | [ | ||
75 | VideoScopeNames.WITH_WEBTORRENT_FILES, | ||
76 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, | ||
77 | VideoScopeNames.WITH_ACCOUNT_DETAILS, | ||
78 | VideoScopeNames.WITH_BLACKLISTED, | ||
79 | VideoScopeNames.WITH_THUMBNAILS, | ||
80 | VideoScopeNames.WITH_TAGS | ||
81 | ] | ||
82 | ) | ||
83 | } | ||
84 | ], | ||
85 | transaction: t | ||
86 | } | 73 | } |
87 | 74 | ||
88 | return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdateVideoAll>(query) | 75 | return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdate>(query) |
89 | } | 76 | } |
90 | 77 | ||
91 | static deleteByVideoId (videoId: number, t: Transaction) { | 78 | static deleteByVideoId (videoId: number, t: Transaction) { |
diff --git a/server/models/video/sql/shared/abstract-videos-model-query-builder.ts b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts new file mode 100644 index 000000000..0d7e64574 --- /dev/null +++ b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts | |||
@@ -0,0 +1,300 @@ | |||
1 | import validator from 'validator' | ||
2 | import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' | ||
3 | import { VideoTables } from './video-tables' | ||
4 | |||
5 | /** | ||
6 | * | ||
7 | * Abstract builder to create SQL query and fetch video models | ||
8 | * | ||
9 | */ | ||
10 | |||
11 | export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder { | ||
12 | protected attributes: { [key: string]: string } = {} | ||
13 | |||
14 | protected joins = '' | ||
15 | protected where: string | ||
16 | |||
17 | protected tables: VideoTables | ||
18 | |||
19 | constructor (protected readonly mode: 'list' | 'get') { | ||
20 | super() | ||
21 | |||
22 | this.tables = new VideoTables(this.mode) | ||
23 | } | ||
24 | |||
25 | protected buildSelect () { | ||
26 | return 'SELECT ' + Object.keys(this.attributes).map(key => { | ||
27 | const value = this.attributes[key] | ||
28 | if (value) return `${key} AS ${value}` | ||
29 | |||
30 | return key | ||
31 | }).join(', ') | ||
32 | } | ||
33 | |||
34 | protected includeChannels () { | ||
35 | this.addJoin('INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"') | ||
36 | this.addJoin('INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"') | ||
37 | |||
38 | this.addJoin( | ||
39 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"' | ||
40 | ) | ||
41 | |||
42 | this.addJoin( | ||
43 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + | ||
44 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' | ||
45 | ) | ||
46 | |||
47 | this.attributes = { | ||
48 | ...this.attributes, | ||
49 | |||
50 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), | ||
51 | ...this.buildActorInclude('VideoChannel->Actor'), | ||
52 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'), | ||
53 | ...this.buildServerInclude('VideoChannel->Actor->Server') | ||
54 | } | ||
55 | } | ||
56 | |||
57 | protected includeAccounts () { | ||
58 | this.addJoin('INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"') | ||
59 | this.addJoin( | ||
60 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"' | ||
61 | ) | ||
62 | |||
63 | this.addJoin( | ||
64 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + | ||
65 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"' | ||
66 | ) | ||
67 | |||
68 | this.addJoin( | ||
69 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + | ||
70 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' | ||
71 | ) | ||
72 | |||
73 | this.attributes = { | ||
74 | ...this.attributes, | ||
75 | |||
76 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), | ||
77 | ...this.buildActorInclude('VideoChannel->Account->Actor'), | ||
78 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'), | ||
79 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') | ||
80 | } | ||
81 | } | ||
82 | |||
83 | protected includeOwnerUser () { | ||
84 | this.addJoin('INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"') | ||
85 | this.addJoin('INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"') | ||
86 | |||
87 | this.attributes = { | ||
88 | ...this.attributes, | ||
89 | |||
90 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), | ||
91 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getUserAccountAttributes()) | ||
92 | } | ||
93 | } | ||
94 | |||
95 | protected includeThumbnails () { | ||
96 | this.addJoin('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"') | ||
97 | |||
98 | this.attributes = { | ||
99 | ...this.attributes, | ||
100 | |||
101 | ...this.buildAttributesObject('Thumbnails', this.tables.getThumbnailAttributes()) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | protected includeWebtorrentFiles () { | ||
106 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') | ||
107 | |||
108 | this.attributes = { | ||
109 | ...this.attributes, | ||
110 | |||
111 | ...this.buildAttributesObject('VideoFiles', this.tables.getFileAttributes()) | ||
112 | } | ||
113 | } | ||
114 | |||
115 | protected includeStreamingPlaylistFiles () { | ||
116 | this.addJoin( | ||
117 | 'LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"' | ||
118 | ) | ||
119 | |||
120 | this.addJoin( | ||
121 | 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' + | ||
122 | 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"' | ||
123 | ) | ||
124 | |||
125 | this.attributes = { | ||
126 | ...this.attributes, | ||
127 | |||
128 | ...this.buildAttributesObject('VideoStreamingPlaylists', this.tables.getStreamingPlaylistAttributes()), | ||
129 | ...this.buildAttributesObject('VideoStreamingPlaylists->VideoFiles', this.tables.getFileAttributes()) | ||
130 | } | ||
131 | } | ||
132 | |||
133 | protected includeUserHistory (userId: number) { | ||
134 | this.addJoin( | ||
135 | 'LEFT OUTER JOIN "userVideoHistory" ' + | ||
136 | 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId' | ||
137 | ) | ||
138 | |||
139 | this.replacements.userVideoHistoryId = userId | ||
140 | |||
141 | this.attributes = { | ||
142 | ...this.attributes, | ||
143 | |||
144 | ...this.buildAttributesObject('userVideoHistory', this.tables.getUserHistoryAttributes()) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | protected includePlaylist (playlistId: number) { | ||
149 | this.addJoin( | ||
150 | 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' + | ||
151 | 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
152 | ) | ||
153 | |||
154 | this.replacements.videoPlaylistId = playlistId | ||
155 | |||
156 | this.attributes = { | ||
157 | ...this.attributes, | ||
158 | |||
159 | ...this.buildAttributesObject('VideoPlaylistElement', this.tables.getPlaylistAttributes()) | ||
160 | } | ||
161 | } | ||
162 | |||
163 | protected includeTags () { | ||
164 | this.addJoin( | ||
165 | 'LEFT OUTER JOIN (' + | ||
166 | '"videoTag" AS "Tags->VideoTagModel" INNER JOIN "tag" AS "Tags" ON "Tags"."id" = "Tags->VideoTagModel"."tagId"' + | ||
167 | ') ' + | ||
168 | 'ON "video"."id" = "Tags->VideoTagModel"."videoId"' | ||
169 | ) | ||
170 | |||
171 | this.attributes = { | ||
172 | ...this.attributes, | ||
173 | |||
174 | ...this.buildAttributesObject('Tags', this.tables.getTagAttributes()), | ||
175 | ...this.buildAttributesObject('Tags->VideoTagModel', this.tables.getVideoTagAttributes()) | ||
176 | } | ||
177 | } | ||
178 | |||
179 | protected includeBlacklisted () { | ||
180 | this.addJoin( | ||
181 | 'LEFT OUTER JOIN "videoBlacklist" AS "VideoBlacklist" ON "video"."id" = "VideoBlacklist"."videoId"' | ||
182 | ) | ||
183 | |||
184 | this.attributes = { | ||
185 | ...this.attributes, | ||
186 | |||
187 | ...this.buildAttributesObject('VideoBlacklist', this.tables.getBlacklistedAttributes()) | ||
188 | } | ||
189 | } | ||
190 | |||
191 | protected includeScheduleUpdate () { | ||
192 | this.addJoin( | ||
193 | 'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"' | ||
194 | ) | ||
195 | |||
196 | this.attributes = { | ||
197 | ...this.attributes, | ||
198 | |||
199 | ...this.buildAttributesObject('ScheduleVideoUpdate', this.tables.getScheduleUpdateAttributes()) | ||
200 | } | ||
201 | } | ||
202 | |||
203 | protected includeLive () { | ||
204 | this.addJoin( | ||
205 | 'LEFT OUTER JOIN "videoLive" AS "VideoLive" ON "video"."id" = "VideoLive"."videoId"' | ||
206 | ) | ||
207 | |||
208 | this.attributes = { | ||
209 | ...this.attributes, | ||
210 | |||
211 | ...this.buildAttributesObject('VideoLive', this.tables.getLiveAttributes()) | ||
212 | } | ||
213 | } | ||
214 | |||
215 | protected includeTrackers () { | ||
216 | this.addJoin( | ||
217 | 'LEFT OUTER JOIN (' + | ||
218 | '"videoTracker" AS "Trackers->VideoTrackerModel" ' + | ||
219 | 'INNER JOIN "tracker" AS "Trackers" ON "Trackers"."id" = "Trackers->VideoTrackerModel"."trackerId"' + | ||
220 | ') ON "video"."id" = "Trackers->VideoTrackerModel"."videoId"' | ||
221 | ) | ||
222 | |||
223 | this.attributes = { | ||
224 | ...this.attributes, | ||
225 | |||
226 | ...this.buildAttributesObject('Trackers', this.tables.getTrackerAttributes()), | ||
227 | ...this.buildAttributesObject('Trackers->VideoTrackerModel', this.tables.getVideoTrackerAttributes()) | ||
228 | } | ||
229 | } | ||
230 | |||
231 | protected includeWebTorrentRedundancies () { | ||
232 | this.addJoin( | ||
233 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + | ||
234 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' | ||
235 | ) | ||
236 | |||
237 | this.attributes = { | ||
238 | ...this.attributes, | ||
239 | |||
240 | ...this.buildAttributesObject('VideoFiles->RedundancyVideos', this.tables.getRedundancyAttributes()) | ||
241 | } | ||
242 | } | ||
243 | |||
244 | protected includeStreamingPlaylistRedundancies () { | ||
245 | this.addJoin( | ||
246 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoStreamingPlaylists->RedundancyVideos" ' + | ||
247 | 'ON "VideoStreamingPlaylists"."id" = "VideoStreamingPlaylists->RedundancyVideos"."videoStreamingPlaylistId"' | ||
248 | ) | ||
249 | |||
250 | this.attributes = { | ||
251 | ...this.attributes, | ||
252 | |||
253 | ...this.buildAttributesObject('VideoStreamingPlaylists->RedundancyVideos', this.tables.getRedundancyAttributes()) | ||
254 | } | ||
255 | } | ||
256 | |||
257 | protected buildActorInclude (prefixKey: string) { | ||
258 | return this.buildAttributesObject(prefixKey, this.tables.getActorAttributes()) | ||
259 | } | ||
260 | |||
261 | protected buildAvatarInclude (prefixKey: string) { | ||
262 | return this.buildAttributesObject(prefixKey, this.tables.getAvatarAttributes()) | ||
263 | } | ||
264 | |||
265 | protected buildServerInclude (prefixKey: string) { | ||
266 | return this.buildAttributesObject(prefixKey, this.tables.getServerAttributes()) | ||
267 | } | ||
268 | |||
269 | protected buildAttributesObject (prefixKey: string, attributeKeys: string[]) { | ||
270 | const result: { [id: string]: string} = {} | ||
271 | |||
272 | const prefixValue = prefixKey.replace(/->/g, '.') | ||
273 | |||
274 | for (const attribute of attributeKeys) { | ||
275 | result[`"${prefixKey}"."${attribute}"`] = `"${prefixValue}.${attribute}"` | ||
276 | } | ||
277 | |||
278 | return result | ||
279 | } | ||
280 | |||
281 | protected whereId (options: { id?: string | number, url?: string }) { | ||
282 | if (options.url) { | ||
283 | this.where = 'WHERE "video"."url" = :videoUrl' | ||
284 | this.replacements.videoUrl = options.url | ||
285 | return | ||
286 | } | ||
287 | |||
288 | if (validator.isInt('' + options.id)) { | ||
289 | this.where = 'WHERE "video".id = :videoId' | ||
290 | } else { | ||
291 | this.where = 'WHERE uuid = :videoId' | ||
292 | } | ||
293 | |||
294 | this.replacements.videoId = options.id | ||
295 | } | ||
296 | |||
297 | protected addJoin (join: string) { | ||
298 | this.joins += join + ' ' | ||
299 | } | ||
300 | } | ||
diff --git a/server/models/video/sql/shared/abstract-videos-query-builder.ts b/server/models/video/sql/shared/abstract-videos-query-builder.ts new file mode 100644 index 000000000..09776bcb0 --- /dev/null +++ b/server/models/video/sql/shared/abstract-videos-query-builder.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' | ||
2 | |||
3 | /** | ||
4 | * | ||
5 | * Abstact builder to run video SQL queries | ||
6 | * | ||
7 | */ | ||
8 | |||
9 | export class AbstractVideosQueryBuilder { | ||
10 | protected sequelize: Sequelize | ||
11 | |||
12 | protected query: string | ||
13 | protected replacements: any = {} | ||
14 | |||
15 | protected runQuery (options: { transaction?: Transaction, logging?: boolean } = {}) { | ||
16 | const queryOptions = { | ||
17 | transaction: options.transaction, | ||
18 | logging: options.logging, | ||
19 | replacements: this.replacements, | ||
20 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
21 | nest: false | ||
22 | } | ||
23 | |||
24 | return this.sequelize.query<any>(this.query, queryOptions) | ||
25 | } | ||
26 | } | ||
diff --git a/server/models/video/sql/shared/video-file-query-builder.ts b/server/models/video/sql/shared/video-file-query-builder.ts new file mode 100644 index 000000000..6b15c3b69 --- /dev/null +++ b/server/models/video/sql/shared/video-file-query-builder.ts | |||
@@ -0,0 +1,69 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import { BuildVideoGetQueryOptions } from '../video-model-get-query-builder' | ||
3 | import { AbstractVideosModelQueryBuilder } from './abstract-videos-model-query-builder' | ||
4 | |||
5 | /** | ||
6 | * | ||
7 | * Fetch files (webtorrent and streaming playlist) according to a video | ||
8 | * | ||
9 | */ | ||
10 | |||
11 | export class VideoFileQueryBuilder extends AbstractVideosModelQueryBuilder { | ||
12 | protected attributes: { [key: string]: string } | ||
13 | |||
14 | constructor (protected readonly sequelize: Sequelize) { | ||
15 | super('get') | ||
16 | } | ||
17 | |||
18 | queryWebTorrentVideos (options: BuildVideoGetQueryOptions) { | ||
19 | this.buildWebtorrentFilesQuery(options) | ||
20 | |||
21 | return this.runQuery(options) | ||
22 | } | ||
23 | |||
24 | queryStreamingPlaylistVideos (options: BuildVideoGetQueryOptions) { | ||
25 | this.buildVideoStreamingPlaylistFilesQuery(options) | ||
26 | |||
27 | return this.runQuery(options) | ||
28 | } | ||
29 | |||
30 | private buildWebtorrentFilesQuery (options: BuildVideoGetQueryOptions) { | ||
31 | this.attributes = { | ||
32 | '"video"."id"': '' | ||
33 | } | ||
34 | |||
35 | this.includeWebtorrentFiles() | ||
36 | |||
37 | if (this.shouldIncludeRedundancies(options)) { | ||
38 | this.includeWebTorrentRedundancies() | ||
39 | } | ||
40 | |||
41 | this.whereId(options) | ||
42 | |||
43 | this.query = this.buildQuery() | ||
44 | } | ||
45 | |||
46 | private buildVideoStreamingPlaylistFilesQuery (options: BuildVideoGetQueryOptions) { | ||
47 | this.attributes = { | ||
48 | '"video"."id"': '' | ||
49 | } | ||
50 | |||
51 | this.includeStreamingPlaylistFiles() | ||
52 | |||
53 | if (this.shouldIncludeRedundancies(options)) { | ||
54 | this.includeStreamingPlaylistRedundancies() | ||
55 | } | ||
56 | |||
57 | this.whereId(options) | ||
58 | |||
59 | this.query = this.buildQuery() | ||
60 | } | ||
61 | |||
62 | private buildQuery () { | ||
63 | return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}` | ||
64 | } | ||
65 | |||
66 | private shouldIncludeRedundancies (options: BuildVideoGetQueryOptions) { | ||
67 | return options.type === 'api' | ||
68 | } | ||
69 | } | ||
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/shared/video-model-builder.ts new file mode 100644 index 000000000..e7e2aa1ca --- /dev/null +++ b/server/models/video/sql/shared/video-model-builder.ts | |||
@@ -0,0 +1,333 @@ | |||
1 | |||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { ActorModel } from '@server/models/actor/actor' | ||
4 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
5 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' | ||
6 | import { ServerModel } from '@server/models/server/server' | ||
7 | import { TrackerModel } from '@server/models/server/tracker' | ||
8 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | ||
9 | import { ScheduleVideoUpdateModel } from '../../schedule-video-update' | ||
10 | import { TagModel } from '../../tag' | ||
11 | import { ThumbnailModel } from '../../thumbnail' | ||
12 | import { VideoModel } from '../../video' | ||
13 | import { VideoBlacklistModel } from '../../video-blacklist' | ||
14 | import { VideoChannelModel } from '../../video-channel' | ||
15 | import { VideoFileModel } from '../../video-file' | ||
16 | import { VideoLiveModel } from '../../video-live' | ||
17 | import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' | ||
18 | import { VideoTables } from './video-tables' | ||
19 | |||
20 | type SQLRow = { [id: string]: string | number } | ||
21 | |||
22 | /** | ||
23 | * | ||
24 | * Build video models from SQL rows | ||
25 | * | ||
26 | */ | ||
27 | |||
28 | export class VideoModelBuilder { | ||
29 | private videosMemo: { [ id: number ]: VideoModel } | ||
30 | private videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } | ||
31 | private videoFileMemo: { [ id: number ]: VideoFileModel } | ||
32 | |||
33 | private thumbnailsDone: Set<any> | ||
34 | private historyDone: Set<any> | ||
35 | private blacklistDone: Set<any> | ||
36 | private liveDone: Set<any> | ||
37 | private redundancyDone: Set<any> | ||
38 | private scheduleVideoUpdateDone: Set<any> | ||
39 | |||
40 | private trackersDone: Set<string> | ||
41 | private tagsDone: Set<string> | ||
42 | |||
43 | private videos: VideoModel[] | ||
44 | |||
45 | private readonly buildOpts = { raw: true, isNewRecord: false } | ||
46 | |||
47 | constructor ( | ||
48 | readonly mode: 'get' | 'list', | ||
49 | readonly tables: VideoTables | ||
50 | ) { | ||
51 | |||
52 | } | ||
53 | |||
54 | buildVideosFromRows (rows: SQLRow[], rowsWebTorrentFiles?: SQLRow[], rowsStreamingPlaylist?: SQLRow[]) { | ||
55 | this.reinit() | ||
56 | |||
57 | for (const row of rows) { | ||
58 | this.buildVideoAndAccount(row) | ||
59 | |||
60 | const videoModel = this.videosMemo[row.id] | ||
61 | |||
62 | this.setUserHistory(row, videoModel) | ||
63 | this.addThumbnail(row, videoModel) | ||
64 | |||
65 | if (!rowsWebTorrentFiles) { | ||
66 | this.addWebTorrentFile(row, videoModel) | ||
67 | } | ||
68 | |||
69 | if (!rowsStreamingPlaylist) { | ||
70 | this.addStreamingPlaylist(row, videoModel) | ||
71 | this.addStreamingPlaylistFile(row) | ||
72 | } | ||
73 | |||
74 | if (this.mode === 'get') { | ||
75 | this.addTag(row, videoModel) | ||
76 | this.addTracker(row, videoModel) | ||
77 | this.setBlacklisted(row, videoModel) | ||
78 | this.setScheduleVideoUpdate(row, videoModel) | ||
79 | this.setLive(row, videoModel) | ||
80 | } | ||
81 | } | ||
82 | |||
83 | this.grabSeparateWebTorrentFiles(rowsWebTorrentFiles) | ||
84 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) | ||
85 | |||
86 | return this.videos | ||
87 | } | ||
88 | |||
89 | private reinit () { | ||
90 | this.videosMemo = {} | ||
91 | this.videoStreamingPlaylistMemo = {} | ||
92 | this.videoFileMemo = {} | ||
93 | |||
94 | this.thumbnailsDone = new Set<number>() | ||
95 | this.historyDone = new Set<number>() | ||
96 | this.blacklistDone = new Set<number>() | ||
97 | this.liveDone = new Set<number>() | ||
98 | this.redundancyDone = new Set<number>() | ||
99 | this.scheduleVideoUpdateDone = new Set<number>() | ||
100 | |||
101 | this.trackersDone = new Set<string>() | ||
102 | this.tagsDone = new Set<string>() | ||
103 | |||
104 | this.videos = [] | ||
105 | } | ||
106 | |||
107 | private grabSeparateWebTorrentFiles (rowsWebTorrentFiles?: SQLRow[]) { | ||
108 | if (!rowsWebTorrentFiles) return | ||
109 | |||
110 | for (const row of rowsWebTorrentFiles) { | ||
111 | const id = row['VideoFiles.id'] | ||
112 | if (!id) continue | ||
113 | |||
114 | const videoModel = this.videosMemo[row.id] | ||
115 | this.addWebTorrentFile(row, videoModel) | ||
116 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) | ||
117 | } | ||
118 | } | ||
119 | |||
120 | private grabSeparateStreamingPlaylistFiles (rowsStreamingPlaylist?: SQLRow[]) { | ||
121 | if (!rowsStreamingPlaylist) return | ||
122 | |||
123 | for (const row of rowsStreamingPlaylist || []) { | ||
124 | const id = row['VideoStreamingPlaylists.id'] | ||
125 | if (!id) continue | ||
126 | |||
127 | const videoModel = this.videosMemo[row.id] | ||
128 | |||
129 | this.addStreamingPlaylist(row, videoModel) | ||
130 | this.addStreamingPlaylistFile(row) | ||
131 | this.addRedundancy(row, 'VideoStreamingPlaylists', this.videoStreamingPlaylistMemo[id]) | ||
132 | } | ||
133 | } | ||
134 | |||
135 | private buildVideoAndAccount (row: SQLRow) { | ||
136 | if (this.videosMemo[row.id]) return | ||
137 | |||
138 | const videoModel = new VideoModel(this.grab(row, this.tables.getVideoAttributes(), ''), this.buildOpts) | ||
139 | |||
140 | videoModel.UserVideoHistories = [] | ||
141 | videoModel.Thumbnails = [] | ||
142 | videoModel.VideoFiles = [] | ||
143 | videoModel.VideoStreamingPlaylists = [] | ||
144 | videoModel.Tags = [] | ||
145 | videoModel.Trackers = [] | ||
146 | |||
147 | this.buildAccount(row, videoModel) | ||
148 | |||
149 | this.videosMemo[row.id] = videoModel | ||
150 | |||
151 | // Keep rows order | ||
152 | this.videos.push(videoModel) | ||
153 | } | ||
154 | |||
155 | private buildAccount (row: SQLRow, videoModel: VideoModel) { | ||
156 | const id = row['VideoChannel.Account.id'] | ||
157 | if (!id) return | ||
158 | |||
159 | const channelModel = new VideoChannelModel(this.grab(row, this.tables.getChannelAttributes(), 'VideoChannel'), this.buildOpts) | ||
160 | channelModel.Actor = this.buildActor(row, 'VideoChannel') | ||
161 | |||
162 | const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts) | ||
163 | accountModel.Actor = this.buildActor(row, 'VideoChannel.Account') | ||
164 | |||
165 | channelModel.Account = accountModel | ||
166 | |||
167 | videoModel.VideoChannel = channelModel | ||
168 | } | ||
169 | |||
170 | private buildActor (row: SQLRow, prefix: string) { | ||
171 | const actorPrefix = `${prefix}.Actor` | ||
172 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
173 | const serverPrefix = `${actorPrefix}.Server` | ||
174 | |||
175 | const avatarModel = row[`${avatarPrefix}.id`] !== null | ||
176 | ? new ActorImageModel(this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix), this.buildOpts) | ||
177 | : null | ||
178 | |||
179 | const serverModel = row[`${serverPrefix}.id`] !== null | ||
180 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) | ||
181 | : null | ||
182 | |||
183 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) | ||
184 | actorModel.Avatar = avatarModel | ||
185 | actorModel.Server = serverModel | ||
186 | |||
187 | return actorModel | ||
188 | } | ||
189 | |||
190 | private setUserHistory (row: SQLRow, videoModel: VideoModel) { | ||
191 | const id = row['userVideoHistory.id'] | ||
192 | if (!id || this.historyDone.has(id)) return | ||
193 | |||
194 | const attributes = this.grab(row, this.tables.getUserHistoryAttributes(), 'userVideoHistory') | ||
195 | const historyModel = new UserVideoHistoryModel(attributes, this.buildOpts) | ||
196 | videoModel.UserVideoHistories.push(historyModel) | ||
197 | |||
198 | this.historyDone.add(id) | ||
199 | } | ||
200 | |||
201 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { | ||
202 | const id = row['Thumbnails.id'] | ||
203 | if (!id || this.thumbnailsDone.has(id)) return | ||
204 | |||
205 | const attributes = this.grab(row, this.tables.getThumbnailAttributes(), 'Thumbnails') | ||
206 | const thumbnailModel = new ThumbnailModel(attributes, this.buildOpts) | ||
207 | videoModel.Thumbnails.push(thumbnailModel) | ||
208 | |||
209 | this.thumbnailsDone.add(id) | ||
210 | } | ||
211 | |||
212 | private addWebTorrentFile (row: SQLRow, videoModel: VideoModel) { | ||
213 | const id = row['VideoFiles.id'] | ||
214 | if (!id || this.videoFileMemo[id]) return | ||
215 | |||
216 | const attributes = this.grab(row, this.tables.getFileAttributes(), 'VideoFiles') | ||
217 | const videoFileModel = new VideoFileModel(attributes, this.buildOpts) | ||
218 | videoModel.VideoFiles.push(videoFileModel) | ||
219 | |||
220 | this.videoFileMemo[id] = videoFileModel | ||
221 | } | ||
222 | |||
223 | private addStreamingPlaylist (row: SQLRow, videoModel: VideoModel) { | ||
224 | const id = row['VideoStreamingPlaylists.id'] | ||
225 | if (!id || this.videoStreamingPlaylistMemo[id]) return | ||
226 | |||
227 | const attributes = this.grab(row, this.tables.getStreamingPlaylistAttributes(), 'VideoStreamingPlaylists') | ||
228 | const streamingPlaylist = new VideoStreamingPlaylistModel(attributes, this.buildOpts) | ||
229 | streamingPlaylist.VideoFiles = [] | ||
230 | |||
231 | videoModel.VideoStreamingPlaylists.push(streamingPlaylist) | ||
232 | |||
233 | this.videoStreamingPlaylistMemo[id] = streamingPlaylist | ||
234 | } | ||
235 | |||
236 | private addStreamingPlaylistFile (row: SQLRow) { | ||
237 | const id = row['VideoStreamingPlaylists.VideoFiles.id'] | ||
238 | if (!id || this.videoFileMemo[id]) return | ||
239 | |||
240 | const streamingPlaylist = this.videoStreamingPlaylistMemo[row['VideoStreamingPlaylists.id']] | ||
241 | |||
242 | const attributes = this.grab(row, this.tables.getFileAttributes(), 'VideoStreamingPlaylists.VideoFiles') | ||
243 | const videoFileModel = new VideoFileModel(attributes, this.buildOpts) | ||
244 | streamingPlaylist.VideoFiles.push(videoFileModel) | ||
245 | |||
246 | this.videoFileMemo[id] = videoFileModel | ||
247 | } | ||
248 | |||
249 | private addRedundancy (row: SQLRow, prefix: string, to: VideoFileModel | VideoStreamingPlaylistModel) { | ||
250 | if (!to.RedundancyVideos) to.RedundancyVideos = [] | ||
251 | |||
252 | const redundancyPrefix = `${prefix}.RedundancyVideos` | ||
253 | const id = row[`${redundancyPrefix}.id`] | ||
254 | |||
255 | if (!id || this.redundancyDone.has(id)) return | ||
256 | |||
257 | const attributes = this.grab(row, this.tables.getRedundancyAttributes(), redundancyPrefix) | ||
258 | const redundancyModel = new VideoRedundancyModel(attributes, this.buildOpts) | ||
259 | to.RedundancyVideos.push(redundancyModel) | ||
260 | |||
261 | this.redundancyDone.add(id) | ||
262 | } | ||
263 | |||
264 | private addTag (row: SQLRow, videoModel: VideoModel) { | ||
265 | if (!row['Tags.name']) return | ||
266 | |||
267 | const key = `${row['Tags.VideoTagModel.videoId']}-${row['Tags.VideoTagModel.tagId']}` | ||
268 | if (this.tagsDone.has(key)) return | ||
269 | |||
270 | const attributes = this.grab(row, this.tables.getTagAttributes(), 'Tags') | ||
271 | const tagModel = new TagModel(attributes, this.buildOpts) | ||
272 | videoModel.Tags.push(tagModel) | ||
273 | |||
274 | this.tagsDone.add(key) | ||
275 | } | ||
276 | |||
277 | private addTracker (row: SQLRow, videoModel: VideoModel) { | ||
278 | if (!row['Trackers.id']) return | ||
279 | |||
280 | const key = `${row['Trackers.VideoTrackerModel.videoId']}-${row['Trackers.VideoTrackerModel.trackerId']}` | ||
281 | if (this.trackersDone.has(key)) return | ||
282 | |||
283 | const attributes = this.grab(row, this.tables.getTrackerAttributes(), 'Trackers') | ||
284 | const trackerModel = new TrackerModel(attributes, this.buildOpts) | ||
285 | videoModel.Trackers.push(trackerModel) | ||
286 | |||
287 | this.trackersDone.add(key) | ||
288 | } | ||
289 | |||
290 | private setBlacklisted (row: SQLRow, videoModel: VideoModel) { | ||
291 | const id = row['VideoBlacklist.id'] | ||
292 | if (!id || this.blacklistDone.has(id)) return | ||
293 | |||
294 | const attributes = this.grab(row, this.tables.getBlacklistedAttributes(), 'VideoBlacklist') | ||
295 | videoModel.VideoBlacklist = new VideoBlacklistModel(attributes, this.buildOpts) | ||
296 | |||
297 | this.blacklistDone.add(id) | ||
298 | } | ||
299 | |||
300 | private setScheduleVideoUpdate (row: SQLRow, videoModel: VideoModel) { | ||
301 | const id = row['ScheduleVideoUpdate.id'] | ||
302 | if (!id || this.scheduleVideoUpdateDone.has(id)) return | ||
303 | |||
304 | const attributes = this.grab(row, this.tables.getScheduleUpdateAttributes(), 'ScheduleVideoUpdate') | ||
305 | videoModel.ScheduleVideoUpdate = new ScheduleVideoUpdateModel(attributes, this.buildOpts) | ||
306 | |||
307 | this.scheduleVideoUpdateDone.add(id) | ||
308 | } | ||
309 | |||
310 | private setLive (row: SQLRow, videoModel: VideoModel) { | ||
311 | const id = row['VideoLive.id'] | ||
312 | if (!id || this.liveDone.has(id)) return | ||
313 | |||
314 | const attributes = this.grab(row, this.tables.getLiveAttributes(), 'VideoLive') | ||
315 | videoModel.VideoLive = new VideoLiveModel(attributes, this.buildOpts) | ||
316 | |||
317 | this.liveDone.add(id) | ||
318 | } | ||
319 | |||
320 | private grab (row: SQLRow, attributes: string[], prefix: string) { | ||
321 | const result: { [ id: string ]: string | number } = {} | ||
322 | |||
323 | for (const a of attributes) { | ||
324 | const key = prefix | ||
325 | ? prefix + '.' + a | ||
326 | : a | ||
327 | |||
328 | result[a] = row[key] | ||
329 | } | ||
330 | |||
331 | return result | ||
332 | } | ||
333 | } | ||
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts new file mode 100644 index 000000000..abdd22188 --- /dev/null +++ b/server/models/video/sql/shared/video-tables.ts | |||
@@ -0,0 +1,263 @@ | |||
1 | |||
2 | /** | ||
3 | * | ||
4 | * Class to build video attributes/join names we want to fetch from the database | ||
5 | * | ||
6 | */ | ||
7 | export class VideoTables { | ||
8 | |||
9 | constructor (readonly mode: 'get' | 'list') { | ||
10 | |||
11 | } | ||
12 | |||
13 | getChannelAttributesForUser () { | ||
14 | return [ 'id', 'accountId' ] | ||
15 | } | ||
16 | |||
17 | getChannelAttributes () { | ||
18 | let attributeKeys = [ | ||
19 | 'id', | ||
20 | 'name', | ||
21 | 'description', | ||
22 | 'actorId' | ||
23 | ] | ||
24 | |||
25 | if (this.mode === 'get') { | ||
26 | attributeKeys = attributeKeys.concat([ | ||
27 | 'support', | ||
28 | 'createdAt', | ||
29 | 'updatedAt' | ||
30 | ]) | ||
31 | } | ||
32 | |||
33 | return attributeKeys | ||
34 | } | ||
35 | |||
36 | getUserAccountAttributes () { | ||
37 | return [ 'id', 'userId' ] | ||
38 | } | ||
39 | |||
40 | getAccountAttributes () { | ||
41 | let attributeKeys = [ 'id', 'name', 'actorId' ] | ||
42 | |||
43 | if (this.mode === 'get') { | ||
44 | attributeKeys = attributeKeys.concat([ | ||
45 | 'description', | ||
46 | 'userId', | ||
47 | 'createdAt', | ||
48 | 'updatedAt' | ||
49 | ]) | ||
50 | } | ||
51 | |||
52 | return attributeKeys | ||
53 | } | ||
54 | |||
55 | getThumbnailAttributes () { | ||
56 | let attributeKeys = [ 'id', 'type', 'filename' ] | ||
57 | |||
58 | if (this.mode === 'get') { | ||
59 | attributeKeys = attributeKeys.concat([ | ||
60 | 'height', | ||
61 | 'width', | ||
62 | 'fileUrl', | ||
63 | 'automaticallyGenerated', | ||
64 | 'videoId', | ||
65 | 'videoPlaylistId', | ||
66 | 'createdAt', | ||
67 | 'updatedAt' | ||
68 | ]) | ||
69 | } | ||
70 | |||
71 | return attributeKeys | ||
72 | } | ||
73 | |||
74 | getFileAttributes () { | ||
75 | return [ | ||
76 | 'id', | ||
77 | 'createdAt', | ||
78 | 'updatedAt', | ||
79 | 'resolution', | ||
80 | 'size', | ||
81 | 'extname', | ||
82 | 'filename', | ||
83 | 'fileUrl', | ||
84 | 'torrentFilename', | ||
85 | 'torrentUrl', | ||
86 | 'infoHash', | ||
87 | 'fps', | ||
88 | 'metadataUrl', | ||
89 | 'videoStreamingPlaylistId', | ||
90 | 'videoId' | ||
91 | ] | ||
92 | } | ||
93 | |||
94 | getStreamingPlaylistAttributes () { | ||
95 | let playlistKeys = [ 'id', 'playlistUrl', 'type' ] | ||
96 | |||
97 | if (this.mode === 'get') { | ||
98 | playlistKeys = playlistKeys.concat([ | ||
99 | 'p2pMediaLoaderInfohashes', | ||
100 | 'p2pMediaLoaderPeerVersion', | ||
101 | 'segmentsSha256Url', | ||
102 | 'videoId', | ||
103 | 'createdAt', | ||
104 | 'updatedAt' | ||
105 | ]) | ||
106 | } | ||
107 | |||
108 | return playlistKeys | ||
109 | } | ||
110 | |||
111 | getUserHistoryAttributes () { | ||
112 | return [ 'id', 'currentTime' ] | ||
113 | } | ||
114 | |||
115 | getPlaylistAttributes () { | ||
116 | return [ | ||
117 | 'createdAt', | ||
118 | 'updatedAt', | ||
119 | 'url', | ||
120 | 'position', | ||
121 | 'startTimestamp', | ||
122 | 'stopTimestamp', | ||
123 | 'videoPlaylistId' | ||
124 | ] | ||
125 | } | ||
126 | |||
127 | getTagAttributes () { | ||
128 | return [ 'id', 'name' ] | ||
129 | } | ||
130 | |||
131 | getVideoTagAttributes () { | ||
132 | return [ 'videoId', 'tagId', 'createdAt', 'updatedAt' ] | ||
133 | } | ||
134 | |||
135 | getBlacklistedAttributes () { | ||
136 | return [ 'id', 'reason', 'unfederated' ] | ||
137 | } | ||
138 | |||
139 | getScheduleUpdateAttributes () { | ||
140 | return [ | ||
141 | 'id', | ||
142 | 'updateAt', | ||
143 | 'privacy', | ||
144 | 'videoId', | ||
145 | 'createdAt', | ||
146 | 'updatedAt' | ||
147 | ] | ||
148 | } | ||
149 | |||
150 | getLiveAttributes () { | ||
151 | return [ | ||
152 | 'id', | ||
153 | 'streamKey', | ||
154 | 'saveReplay', | ||
155 | 'permanentLive', | ||
156 | 'videoId', | ||
157 | 'createdAt', | ||
158 | 'updatedAt' | ||
159 | ] | ||
160 | } | ||
161 | |||
162 | getTrackerAttributes () { | ||
163 | return [ 'id', 'url' ] | ||
164 | } | ||
165 | |||
166 | getVideoTrackerAttributes () { | ||
167 | return [ | ||
168 | 'videoId', | ||
169 | 'trackerId', | ||
170 | 'createdAt', | ||
171 | 'updatedAt' | ||
172 | ] | ||
173 | } | ||
174 | |||
175 | getRedundancyAttributes () { | ||
176 | return [ 'id', 'fileUrl' ] | ||
177 | } | ||
178 | |||
179 | getActorAttributes () { | ||
180 | let attributeKeys = [ | ||
181 | 'id', | ||
182 | 'preferredUsername', | ||
183 | 'url', | ||
184 | 'serverId', | ||
185 | 'avatarId' | ||
186 | ] | ||
187 | |||
188 | if (this.mode === 'get') { | ||
189 | attributeKeys = attributeKeys.concat([ | ||
190 | 'type', | ||
191 | 'followersCount', | ||
192 | 'followingCount', | ||
193 | 'inboxUrl', | ||
194 | 'outboxUrl', | ||
195 | 'sharedInboxUrl', | ||
196 | 'followersUrl', | ||
197 | 'followingUrl', | ||
198 | 'remoteCreatedAt', | ||
199 | 'createdAt', | ||
200 | 'updatedAt' | ||
201 | ]) | ||
202 | } | ||
203 | |||
204 | return attributeKeys | ||
205 | } | ||
206 | |||
207 | getAvatarAttributes () { | ||
208 | let attributeKeys = [ | ||
209 | 'id', | ||
210 | 'filename', | ||
211 | 'type', | ||
212 | 'fileUrl', | ||
213 | 'onDisk', | ||
214 | 'createdAt', | ||
215 | 'updatedAt' | ||
216 | ] | ||
217 | |||
218 | if (this.mode === 'get') { | ||
219 | attributeKeys = attributeKeys.concat([ | ||
220 | 'height', | ||
221 | 'width', | ||
222 | 'type' | ||
223 | ]) | ||
224 | } | ||
225 | |||
226 | return attributeKeys | ||
227 | } | ||
228 | |||
229 | getServerAttributes () { | ||
230 | return [ 'id', 'host' ] | ||
231 | } | ||
232 | |||
233 | getVideoAttributes () { | ||
234 | return [ | ||
235 | 'id', | ||
236 | 'uuid', | ||
237 | 'name', | ||
238 | 'category', | ||
239 | 'licence', | ||
240 | 'language', | ||
241 | 'privacy', | ||
242 | 'nsfw', | ||
243 | 'description', | ||
244 | 'support', | ||
245 | 'duration', | ||
246 | 'views', | ||
247 | 'likes', | ||
248 | 'dislikes', | ||
249 | 'remote', | ||
250 | 'isLive', | ||
251 | 'url', | ||
252 | 'commentsEnabled', | ||
253 | 'downloadEnabled', | ||
254 | 'waitTranscoding', | ||
255 | 'state', | ||
256 | 'publishedAt', | ||
257 | 'originallyPublishedAt', | ||
258 | 'channelId', | ||
259 | 'createdAt', | ||
260 | 'updatedAt' | ||
261 | ] | ||
262 | } | ||
263 | } | ||
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video-model-get-query-builder.ts new file mode 100644 index 000000000..f234e8778 --- /dev/null +++ b/server/models/video/sql/video-model-get-query-builder.ts | |||
@@ -0,0 +1,173 @@ | |||
1 | import { Sequelize, Transaction } from 'sequelize' | ||
2 | import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' | ||
3 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' | ||
4 | import { VideoModelBuilder } from './shared/video-model-builder' | ||
5 | import { VideoTables } from './shared/video-tables' | ||
6 | |||
7 | /** | ||
8 | * | ||
9 | * Build a GET SQL query, fetch rows and create the video model | ||
10 | * | ||
11 | */ | ||
12 | |||
13 | export type GetType = | ||
14 | 'api' | | ||
15 | 'full-light' | | ||
16 | 'account-blacklist-files' | | ||
17 | 'all-files' | | ||
18 | 'thumbnails' | | ||
19 | 'thumbnails-blacklist' | | ||
20 | 'id' | | ||
21 | 'blacklist-rights' | ||
22 | |||
23 | export type BuildVideoGetQueryOptions = { | ||
24 | id?: number | string | ||
25 | url?: string | ||
26 | |||
27 | type: GetType | ||
28 | |||
29 | userId?: number | ||
30 | transaction?: Transaction | ||
31 | |||
32 | logging?: boolean | ||
33 | } | ||
34 | |||
35 | export class VideosModelGetQueryBuilder { | ||
36 | videoQueryBuilder: VideosModelGetQuerySubBuilder | ||
37 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | ||
38 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | ||
39 | |||
40 | private readonly videoModelBuilder: VideoModelBuilder | ||
41 | |||
42 | private static readonly videoFilesInclude = new Set<GetType>([ 'api', 'full-light', 'account-blacklist-files', 'all-files' ]) | ||
43 | |||
44 | constructor (protected readonly sequelize: Sequelize) { | ||
45 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) | ||
46 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
47 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
48 | |||
49 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTables('get')) | ||
50 | } | ||
51 | |||
52 | async queryVideo (options: BuildVideoGetQueryOptions) { | ||
53 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ | ||
54 | this.videoQueryBuilder.queryVideos(options), | ||
55 | |||
56 | VideosModelGetQueryBuilder.videoFilesInclude.has(options.type) | ||
57 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options) | ||
58 | : Promise.resolve(undefined), | ||
59 | |||
60 | VideosModelGetQueryBuilder.videoFilesInclude.has(options.type) | ||
61 | ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options) | ||
62 | : Promise.resolve(undefined) | ||
63 | ]) | ||
64 | |||
65 | const videos = this.videoModelBuilder.buildVideosFromRows(videoRows, webtorrentFilesRows, streamingPlaylistFilesRows) | ||
66 | |||
67 | if (videos.length > 1) { | ||
68 | throw new Error('Video results is more than ') | ||
69 | } | ||
70 | |||
71 | if (videos.length === 0) return null | ||
72 | return videos[0] | ||
73 | } | ||
74 | } | ||
75 | |||
76 | export class VideosModelGetQuerySubBuilder extends AbstractVideosModelQueryBuilder { | ||
77 | protected attributes: { [key: string]: string } | ||
78 | |||
79 | protected webtorrentFilesQuery: string | ||
80 | protected streamingPlaylistFilesQuery: string | ||
81 | |||
82 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) | ||
83 | private static readonly liveInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
84 | private static readonly scheduleUpdateInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
85 | private static readonly tagsInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
86 | private static readonly userHistoryInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
87 | private static readonly accountInclude = new Set<GetType>([ 'api', 'full-light', 'account-blacklist-files' ]) | ||
88 | private static readonly ownerUserInclude = new Set<GetType>([ 'blacklist-rights' ]) | ||
89 | |||
90 | private static readonly blacklistedInclude = new Set<GetType>([ | ||
91 | 'api', | ||
92 | 'full-light', | ||
93 | 'account-blacklist-files', | ||
94 | 'thumbnails-blacklist', | ||
95 | 'blacklist-rights' | ||
96 | ]) | ||
97 | |||
98 | private static readonly thumbnailsInclude = new Set<GetType>([ | ||
99 | 'api', | ||
100 | 'full-light', | ||
101 | 'account-blacklist-files', | ||
102 | 'all-files', | ||
103 | 'thumbnails', | ||
104 | 'thumbnails-blacklist' | ||
105 | ]) | ||
106 | |||
107 | constructor (protected readonly sequelize: Sequelize) { | ||
108 | super('get') | ||
109 | } | ||
110 | |||
111 | queryVideos (options: BuildVideoGetQueryOptions) { | ||
112 | this.buildMainGetQuery(options) | ||
113 | |||
114 | return this.runQuery(options) | ||
115 | } | ||
116 | |||
117 | private buildMainGetQuery (options: BuildVideoGetQueryOptions) { | ||
118 | this.attributes = { | ||
119 | '"video".*': '' | ||
120 | } | ||
121 | |||
122 | if (VideosModelGetQuerySubBuilder.thumbnailsInclude.has(options.type)) { | ||
123 | this.includeThumbnails() | ||
124 | } | ||
125 | |||
126 | if (VideosModelGetQuerySubBuilder.blacklistedInclude.has(options.type)) { | ||
127 | this.includeBlacklisted() | ||
128 | } | ||
129 | |||
130 | if (VideosModelGetQuerySubBuilder.accountInclude.has(options.type)) { | ||
131 | this.includeChannels() | ||
132 | this.includeAccounts() | ||
133 | } | ||
134 | |||
135 | if (VideosModelGetQuerySubBuilder.tagsInclude.has(options.type)) { | ||
136 | this.includeTags() | ||
137 | } | ||
138 | |||
139 | if (VideosModelGetQuerySubBuilder.scheduleUpdateInclude.has(options.type)) { | ||
140 | this.includeScheduleUpdate() | ||
141 | } | ||
142 | |||
143 | if (VideosModelGetQuerySubBuilder.liveInclude.has(options.type)) { | ||
144 | this.includeLive() | ||
145 | } | ||
146 | |||
147 | if (options.userId && VideosModelGetQuerySubBuilder.userHistoryInclude.has(options.type)) { | ||
148 | this.includeUserHistory(options.userId) | ||
149 | } | ||
150 | |||
151 | if (VideosModelGetQuerySubBuilder.ownerUserInclude.has(options.type)) { | ||
152 | this.includeOwnerUser() | ||
153 | } | ||
154 | |||
155 | if (VideosModelGetQuerySubBuilder.trackersInclude.has(options.type)) { | ||
156 | this.includeTrackers() | ||
157 | } | ||
158 | |||
159 | this.whereId(options) | ||
160 | |||
161 | this.query = this.buildQuery(options) | ||
162 | } | ||
163 | |||
164 | private buildQuery (options: BuildVideoGetQueryOptions) { | ||
165 | const order = VideosModelGetQuerySubBuilder.tagsInclude.has(options.type) | ||
166 | ? 'ORDER BY "Tags"."name" ASC' | ||
167 | : '' | ||
168 | |||
169 | const from = `SELECT * FROM "video" ${this.where} LIMIT 1` | ||
170 | |||
171 | return `${this.buildSelect()} FROM (${from}) AS "video" ${this.joins} ${order}` | ||
172 | } | ||
173 | } | ||
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts new file mode 100644 index 000000000..30b251f0f --- /dev/null +++ b/server/models/video/sql/videos-id-list-query-builder.ts | |||
@@ -0,0 +1,616 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import validator from 'validator' | ||
3 | import { exists } from '@server/helpers/custom-validators/misc' | ||
4 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' | ||
5 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
6 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' | ||
7 | import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder' | ||
8 | |||
9 | /** | ||
10 | * | ||
11 | * Build videos list SQL query to fetch rows | ||
12 | * | ||
13 | */ | ||
14 | |||
15 | export type BuildVideosListQueryOptions = { | ||
16 | attributes?: string[] | ||
17 | |||
18 | serverAccountId: number | ||
19 | followerActorId: number | ||
20 | includeLocalVideos: boolean | ||
21 | |||
22 | count: number | ||
23 | start: number | ||
24 | sort: string | ||
25 | |||
26 | nsfw?: boolean | ||
27 | filter?: VideoFilter | ||
28 | isLive?: boolean | ||
29 | |||
30 | categoryOneOf?: number[] | ||
31 | licenceOneOf?: number[] | ||
32 | languageOneOf?: string[] | ||
33 | tagsOneOf?: string[] | ||
34 | tagsAllOf?: string[] | ||
35 | |||
36 | withFiles?: boolean | ||
37 | |||
38 | accountId?: number | ||
39 | videoChannelId?: number | ||
40 | |||
41 | videoPlaylistId?: number | ||
42 | |||
43 | trendingAlgorithm?: string // best, hot, or any other algorithm implemented | ||
44 | trendingDays?: number | ||
45 | |||
46 | user?: MUserAccountId | ||
47 | historyOfUser?: MUserId | ||
48 | |||
49 | startDate?: string // ISO 8601 | ||
50 | endDate?: string // ISO 8601 | ||
51 | originallyPublishedStartDate?: string | ||
52 | originallyPublishedEndDate?: string | ||
53 | |||
54 | durationMin?: number // seconds | ||
55 | durationMax?: number // seconds | ||
56 | |||
57 | search?: string | ||
58 | |||
59 | isCount?: boolean | ||
60 | |||
61 | group?: string | ||
62 | having?: string | ||
63 | } | ||
64 | |||
65 | export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | ||
66 | protected replacements: any = {} | ||
67 | |||
68 | private attributes: string[] | ||
69 | private joins: string[] = [] | ||
70 | |||
71 | private readonly and: string[] = [] | ||
72 | |||
73 | private readonly cte: string[] = [] | ||
74 | |||
75 | private group = '' | ||
76 | private having = '' | ||
77 | |||
78 | private sort = '' | ||
79 | private limit = '' | ||
80 | private offset = '' | ||
81 | |||
82 | constructor (protected readonly sequelize: Sequelize) { | ||
83 | super() | ||
84 | } | ||
85 | |||
86 | queryVideoIds (options: BuildVideosListQueryOptions) { | ||
87 | this.buildIdsListQuery(options) | ||
88 | |||
89 | return this.runQuery() | ||
90 | } | ||
91 | |||
92 | countVideoIds (countOptions: BuildVideosListQueryOptions): Promise<number> { | ||
93 | this.buildIdsListQuery(countOptions) | ||
94 | |||
95 | return this.runQuery().then(rows => rows.length !== 0 ? rows[0].total : 0) | ||
96 | } | ||
97 | |||
98 | getIdsListQueryAndSort (options: BuildVideosListQueryOptions) { | ||
99 | this.buildIdsListQuery(options) | ||
100 | return { query: this.query, sort: this.sort, replacements: this.replacements } | ||
101 | } | ||
102 | |||
103 | private buildIdsListQuery (options: BuildVideosListQueryOptions) { | ||
104 | this.attributes = options.attributes || [ '"video"."id"' ] | ||
105 | |||
106 | if (options.group) this.group = options.group | ||
107 | if (options.having) this.having = options.having | ||
108 | |||
109 | this.joins = this.joins.concat([ | ||
110 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId"', | ||
111 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId"', | ||
112 | 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"' | ||
113 | ]) | ||
114 | |||
115 | this.whereNotBlacklisted() | ||
116 | |||
117 | if (options.serverAccountId) { | ||
118 | this.whereNotBlocked(options.serverAccountId, options.user) | ||
119 | } | ||
120 | |||
121 | // Only list public/published videos | ||
122 | if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) { | ||
123 | this.whereStateAndPrivacyAvailable(options.user) | ||
124 | } | ||
125 | |||
126 | if (options.videoPlaylistId) { | ||
127 | this.joinPlaylist(options.videoPlaylistId) | ||
128 | } | ||
129 | |||
130 | if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) { | ||
131 | this.whereOnlyLocal() | ||
132 | } | ||
133 | |||
134 | if (options.accountId) { | ||
135 | this.whereAccountId(options.accountId) | ||
136 | } | ||
137 | |||
138 | if (options.videoChannelId) { | ||
139 | this.whereChannelId(options.videoChannelId) | ||
140 | } | ||
141 | |||
142 | if (options.followerActorId) { | ||
143 | this.whereFollowerActorId(options.followerActorId, options.includeLocalVideos) | ||
144 | } | ||
145 | |||
146 | if (options.withFiles === true) { | ||
147 | this.whereFileExists() | ||
148 | } | ||
149 | |||
150 | if (options.tagsOneOf) { | ||
151 | this.whereTagsOneOf(options.tagsOneOf) | ||
152 | } | ||
153 | |||
154 | if (options.tagsAllOf) { | ||
155 | this.whereTagsAllOf(options.tagsAllOf) | ||
156 | } | ||
157 | |||
158 | if (options.nsfw === true) { | ||
159 | this.whereNSFW() | ||
160 | } else if (options.nsfw === false) { | ||
161 | this.whereSFW() | ||
162 | } | ||
163 | |||
164 | if (options.isLive === true) { | ||
165 | this.whereLive() | ||
166 | } else if (options.isLive === false) { | ||
167 | this.whereVOD() | ||
168 | } | ||
169 | |||
170 | if (options.categoryOneOf) { | ||
171 | this.whereCategoryOneOf(options.categoryOneOf) | ||
172 | } | ||
173 | |||
174 | if (options.licenceOneOf) { | ||
175 | this.whereLicenceOneOf(options.licenceOneOf) | ||
176 | } | ||
177 | |||
178 | if (options.languageOneOf) { | ||
179 | this.whereLanguageOneOf(options.languageOneOf) | ||
180 | } | ||
181 | |||
182 | // We don't exclude results in this so if we do a count we don't need to add this complex clause | ||
183 | if (options.isCount !== true) { | ||
184 | if (options.trendingDays) { | ||
185 | this.groupForTrending(options.trendingDays) | ||
186 | } else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) { | ||
187 | this.groupForHotOrBest(options.trendingAlgorithm, options.user) | ||
188 | } | ||
189 | } | ||
190 | |||
191 | if (options.historyOfUser) { | ||
192 | this.joinHistory(options.historyOfUser.id) | ||
193 | } | ||
194 | |||
195 | if (options.startDate) { | ||
196 | this.whereStartDate(options.startDate) | ||
197 | } | ||
198 | |||
199 | if (options.endDate) { | ||
200 | this.whereEndDate(options.endDate) | ||
201 | } | ||
202 | |||
203 | if (options.originallyPublishedStartDate) { | ||
204 | this.whereOriginallyPublishedStartDate(options.originallyPublishedStartDate) | ||
205 | } | ||
206 | |||
207 | if (options.originallyPublishedEndDate) { | ||
208 | this.whereOriginallyPublishedEndDate(options.originallyPublishedEndDate) | ||
209 | } | ||
210 | |||
211 | if (options.durationMin) { | ||
212 | this.whereDurationMin(options.durationMin) | ||
213 | } | ||
214 | |||
215 | if (options.durationMax) { | ||
216 | this.whereDurationMax(options.durationMax) | ||
217 | } | ||
218 | |||
219 | this.whereSearch(options.search) | ||
220 | |||
221 | if (options.isCount === true) { | ||
222 | this.setCountAttribute() | ||
223 | } else { | ||
224 | if (exists(options.sort)) { | ||
225 | this.setSort(options.sort) | ||
226 | } | ||
227 | |||
228 | if (exists(options.count)) { | ||
229 | this.setLimit(options.count) | ||
230 | } | ||
231 | |||
232 | if (exists(options.start)) { | ||
233 | this.setOffset(options.start) | ||
234 | } | ||
235 | } | ||
236 | |||
237 | const cteString = this.cte.length !== 0 | ||
238 | ? `WITH ${this.cte.join(', ')} ` | ||
239 | : '' | ||
240 | |||
241 | this.query = cteString + | ||
242 | 'SELECT ' + this.attributes.join(', ') + ' ' + | ||
243 | 'FROM "video" ' + this.joins.join(' ') + ' ' + | ||
244 | 'WHERE ' + this.and.join(' AND ') + ' ' + | ||
245 | this.group + ' ' + | ||
246 | this.having + ' ' + | ||
247 | this.sort + ' ' + | ||
248 | this.limit + ' ' + | ||
249 | this.offset | ||
250 | } | ||
251 | |||
252 | private setCountAttribute () { | ||
253 | this.attributes = [ 'COUNT(*) as "total"' ] | ||
254 | } | ||
255 | |||
256 | private joinHistory (userId: number) { | ||
257 | this.joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"') | ||
258 | |||
259 | this.and.push('"userVideoHistory"."userId" = :historyOfUser') | ||
260 | |||
261 | this.replacements.historyOfUser = userId | ||
262 | } | ||
263 | |||
264 | private joinPlaylist (playlistId: number) { | ||
265 | this.joins.push( | ||
266 | 'INNER JOIN "videoPlaylistElement" "video"."id" = "videoPlaylistElement"."videoId" ' + | ||
267 | 'AND "videoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
268 | ) | ||
269 | |||
270 | this.replacements.videoPlaylistId = playlistId | ||
271 | } | ||
272 | |||
273 | private whereStateAndPrivacyAvailable (user?: MUserAccountId) { | ||
274 | this.and.push( | ||
275 | `("video"."state" = ${VideoState.PUBLISHED} OR ` + | ||
276 | `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` | ||
277 | ) | ||
278 | |||
279 | if (user) { | ||
280 | this.and.push( | ||
281 | `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})` | ||
282 | ) | ||
283 | } else { // Or only public videos | ||
284 | this.and.push( | ||
285 | `"video"."privacy" = ${VideoPrivacy.PUBLIC}` | ||
286 | ) | ||
287 | } | ||
288 | } | ||
289 | |||
290 | private whereOnlyLocal () { | ||
291 | this.and.push('"video"."remote" IS FALSE') | ||
292 | } | ||
293 | |||
294 | private whereAccountId (accountId: number) { | ||
295 | this.and.push('"account"."id" = :accountId') | ||
296 | this.replacements.accountId = accountId | ||
297 | } | ||
298 | |||
299 | private whereChannelId (channelId: number) { | ||
300 | this.and.push('"videoChannel"."id" = :videoChannelId') | ||
301 | this.replacements.videoChannelId = channelId | ||
302 | } | ||
303 | |||
304 | private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { | ||
305 | let query = | ||
306 | '(' + | ||
307 | ' EXISTS (' + | ||
308 | ' SELECT 1 FROM "videoShare" ' + | ||
309 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + | ||
310 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + | ||
311 | ' WHERE "videoShare"."videoId" = "video"."id"' + | ||
312 | ' )' + | ||
313 | ' OR' + | ||
314 | ' EXISTS (' + | ||
315 | ' SELECT 1 from "actorFollow" ' + | ||
316 | ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + | ||
317 | ' AND "actorFollow"."state" = \'accepted\'' + | ||
318 | ' )' | ||
319 | |||
320 | if (includeLocalVideos) { | ||
321 | query += ' OR "video"."remote" IS FALSE' | ||
322 | } | ||
323 | |||
324 | query += ')' | ||
325 | |||
326 | this.and.push(query) | ||
327 | this.replacements.followerActorId = followerActorId | ||
328 | } | ||
329 | |||
330 | private whereFileExists () { | ||
331 | this.and.push( | ||
332 | '(' + | ||
333 | ' EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id") ' + | ||
334 | ' OR EXISTS (' + | ||
335 | ' SELECT 1 FROM "videoStreamingPlaylist" ' + | ||
336 | ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' + | ||
337 | ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' + | ||
338 | ' )' + | ||
339 | ')' | ||
340 | ) | ||
341 | } | ||
342 | |||
343 | private whereTagsOneOf (tagsOneOf: string[]) { | ||
344 | const tagsOneOfLower = tagsOneOf.map(t => t.toLowerCase()) | ||
345 | |||
346 | this.and.push( | ||
347 | 'EXISTS (' + | ||
348 | ' SELECT 1 FROM "videoTag" ' + | ||
349 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
350 | ' WHERE lower("tag"."name") IN (' + createSafeIn(this.sequelize, tagsOneOfLower) + ') ' + | ||
351 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
352 | ')' | ||
353 | ) | ||
354 | } | ||
355 | |||
356 | private whereTagsAllOf (tagsAllOf: string[]) { | ||
357 | const tagsAllOfLower = tagsAllOf.map(t => t.toLowerCase()) | ||
358 | |||
359 | this.and.push( | ||
360 | 'EXISTS (' + | ||
361 | ' SELECT 1 FROM "videoTag" ' + | ||
362 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
363 | ' WHERE lower("tag"."name") IN (' + createSafeIn(this.sequelize, tagsAllOfLower) + ') ' + | ||
364 | ' AND "video"."id" = "videoTag"."videoId" ' + | ||
365 | ' GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length + | ||
366 | ')' | ||
367 | ) | ||
368 | } | ||
369 | |||
370 | private whereCategoryOneOf (categoryOneOf: number[]) { | ||
371 | this.and.push('"video"."category" IN (:categoryOneOf)') | ||
372 | this.replacements.categoryOneOf = categoryOneOf | ||
373 | } | ||
374 | |||
375 | private whereLicenceOneOf (licenceOneOf: number[]) { | ||
376 | this.and.push('"video"."licence" IN (:licenceOneOf)') | ||
377 | this.replacements.licenceOneOf = licenceOneOf | ||
378 | } | ||
379 | |||
380 | private whereLanguageOneOf (languageOneOf: string[]) { | ||
381 | const languages = languageOneOf.filter(l => l && l !== '_unknown') | ||
382 | const languagesQueryParts: string[] = [] | ||
383 | |||
384 | if (languages.length !== 0) { | ||
385 | languagesQueryParts.push('"video"."language" IN (:languageOneOf)') | ||
386 | this.replacements.languageOneOf = languages | ||
387 | |||
388 | languagesQueryParts.push( | ||
389 | 'EXISTS (' + | ||
390 | ' SELECT 1 FROM "videoCaption" WHERE "videoCaption"."language" ' + | ||
391 | ' IN (' + createSafeIn(this.sequelize, languages) + ') AND ' + | ||
392 | ' "videoCaption"."videoId" = "video"."id"' + | ||
393 | ')' | ||
394 | ) | ||
395 | } | ||
396 | |||
397 | if (languageOneOf.includes('_unknown')) { | ||
398 | languagesQueryParts.push('"video"."language" IS NULL') | ||
399 | } | ||
400 | |||
401 | if (languagesQueryParts.length !== 0) { | ||
402 | this.and.push('(' + languagesQueryParts.join(' OR ') + ')') | ||
403 | } | ||
404 | } | ||
405 | |||
406 | private whereNSFW () { | ||
407 | this.and.push('"video"."nsfw" IS TRUE') | ||
408 | } | ||
409 | |||
410 | private whereSFW () { | ||
411 | this.and.push('"video"."nsfw" IS FALSE') | ||
412 | } | ||
413 | |||
414 | private whereLive () { | ||
415 | this.and.push('"video"."isLive" IS TRUE') | ||
416 | } | ||
417 | |||
418 | private whereVOD () { | ||
419 | this.and.push('"video"."isLive" IS FALSE') | ||
420 | } | ||
421 | |||
422 | private whereNotBlocked (serverAccountId: number, user?: MUserAccountId) { | ||
423 | const blockerIds = [ serverAccountId ] | ||
424 | if (user) blockerIds.push(user.Account.id) | ||
425 | |||
426 | const inClause = createSafeIn(this.sequelize, blockerIds) | ||
427 | |||
428 | this.and.push( | ||
429 | 'NOT EXISTS (' + | ||
430 | ' SELECT 1 FROM "accountBlocklist" ' + | ||
431 | ' WHERE "accountBlocklist"."accountId" IN (' + inClause + ') ' + | ||
432 | ' AND "accountBlocklist"."targetAccountId" = "account"."id" ' + | ||
433 | ')' + | ||
434 | 'AND NOT EXISTS (' + | ||
435 | ' SELECT 1 FROM "serverBlocklist" WHERE "serverBlocklist"."accountId" IN (' + inClause + ') ' + | ||
436 | ' AND "serverBlocklist"."targetServerId" = "accountActor"."serverId"' + | ||
437 | ')' | ||
438 | ) | ||
439 | } | ||
440 | |||
441 | private whereSearch (search?: string) { | ||
442 | if (!search) { | ||
443 | this.attributes.push('0 as similarity') | ||
444 | return | ||
445 | } | ||
446 | |||
447 | const escapedSearch = this.sequelize.escape(search) | ||
448 | const escapedLikeSearch = this.sequelize.escape('%' + search + '%') | ||
449 | |||
450 | this.cte.push( | ||
451 | '"trigramSearch" AS (' + | ||
452 | ' SELECT "video"."id", ' + | ||
453 | ` similarity(lower(immutable_unaccent("video"."name")), lower(immutable_unaccent(${escapedSearch}))) as similarity ` + | ||
454 | ' FROM "video" ' + | ||
455 | ' WHERE lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + | ||
456 | ' lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + | ||
457 | ')' | ||
458 | ) | ||
459 | |||
460 | this.joins.push('LEFT JOIN "trigramSearch" ON "video"."id" = "trigramSearch"."id"') | ||
461 | |||
462 | let base = '(' + | ||
463 | ' "trigramSearch"."id" IS NOT NULL OR ' + | ||
464 | ' EXISTS (' + | ||
465 | ' SELECT 1 FROM "videoTag" ' + | ||
466 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
467 | ` WHERE lower("tag"."name") = ${escapedSearch} ` + | ||
468 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
469 | ' )' | ||
470 | |||
471 | if (validator.isUUID(search)) { | ||
472 | base += ` OR "video"."uuid" = ${escapedSearch}` | ||
473 | } | ||
474 | |||
475 | base += ')' | ||
476 | |||
477 | this.and.push(base) | ||
478 | this.attributes.push(`COALESCE("trigramSearch"."similarity", 0) as similarity`) | ||
479 | } | ||
480 | |||
481 | private whereNotBlacklisted () { | ||
482 | this.and.push('"video"."id" NOT IN (SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")') | ||
483 | } | ||
484 | |||
485 | private whereStartDate (startDate: string) { | ||
486 | this.and.push('"video"."publishedAt" >= :startDate') | ||
487 | this.replacements.startDate = startDate | ||
488 | } | ||
489 | |||
490 | private whereEndDate (endDate: string) { | ||
491 | this.and.push('"video"."publishedAt" <= :endDate') | ||
492 | this.replacements.endDate = endDate | ||
493 | } | ||
494 | |||
495 | private whereOriginallyPublishedStartDate (startDate: string) { | ||
496 | this.and.push('"video"."originallyPublishedAt" >= :originallyPublishedStartDate') | ||
497 | this.replacements.originallyPublishedStartDate = startDate | ||
498 | } | ||
499 | |||
500 | private whereOriginallyPublishedEndDate (endDate: string) { | ||
501 | this.and.push('"video"."originallyPublishedAt" <= :originallyPublishedEndDate') | ||
502 | this.replacements.originallyPublishedEndDate = endDate | ||
503 | } | ||
504 | |||
505 | private whereDurationMin (durationMin: number) { | ||
506 | this.and.push('"video"."duration" >= :durationMin') | ||
507 | this.replacements.durationMin = durationMin | ||
508 | } | ||
509 | |||
510 | private whereDurationMax (durationMax: number) { | ||
511 | this.and.push('"video"."duration" <= :durationMax') | ||
512 | this.replacements.durationMax = durationMax | ||
513 | } | ||
514 | |||
515 | private groupForTrending (trendingDays: number) { | ||
516 | const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) | ||
517 | |||
518 | this.joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate') | ||
519 | this.replacements.viewsGteDate = viewsGteDate | ||
520 | |||
521 | this.attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') | ||
522 | |||
523 | this.group = 'GROUP BY "video"."id"' | ||
524 | } | ||
525 | |||
526 | private groupForHotOrBest (trendingAlgorithm: string, user?: MUserAccountId) { | ||
527 | /** | ||
528 | * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, | ||
529 | * with fixed weights only applied to their log values. | ||
530 | * | ||
531 | * This algorithm gives little chance for an old video to have a good score, | ||
532 | * for which recent spikes in interactions could be a sign of "hotness" and | ||
533 | * justify a better score. However there are multiple ways to achieve that | ||
534 | * goal, which is left for later. Yes, this is a TODO :) | ||
535 | * | ||
536 | * notes: | ||
537 | * - weights and base score are in number of half-days. | ||
538 | * - all comments are counted, regardless of being written by the video author or not | ||
539 | * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58 | ||
540 | * - we have less interactions than on reddit, so multiply weights by an arbitrary factor | ||
541 | */ | ||
542 | const weights = { | ||
543 | like: 3 * 50, | ||
544 | dislike: -3 * 50, | ||
545 | view: Math.floor((1 / 3) * 50), | ||
546 | comment: 2 * 50, // a comment takes more time than a like to do, but can be done multiple times | ||
547 | history: -2 * 50 | ||
548 | } | ||
549 | |||
550 | this.joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') | ||
551 | |||
552 | let attribute = | ||
553 | `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) | ||
554 | `+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) | ||
555 | `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) | ||
556 | `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+) | ||
557 | '+ (SELECT (EXTRACT(epoch FROM "video"."publishedAt") - 1446156582) / 47000) ' // base score (in number of half-days) | ||
558 | |||
559 | if (trendingAlgorithm === 'best' && user) { | ||
560 | this.joins.push( | ||
561 | 'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser' | ||
562 | ) | ||
563 | this.replacements.bestUser = user.id | ||
564 | |||
565 | attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} ` | ||
566 | } | ||
567 | |||
568 | attribute += 'AS "score"' | ||
569 | this.attributes.push(attribute) | ||
570 | |||
571 | this.group = 'GROUP BY "video"."id"' | ||
572 | } | ||
573 | |||
574 | private setSort (sort: string) { | ||
575 | if (sort === '-originallyPublishedAt' || sort === 'originallyPublishedAt') { | ||
576 | this.attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') | ||
577 | } | ||
578 | |||
579 | this.sort = this.buildOrder(sort) | ||
580 | } | ||
581 | |||
582 | private buildOrder (value: string) { | ||
583 | const { direction, field } = buildDirectionAndField(value) | ||
584 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | ||
585 | |||
586 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | ||
587 | |||
588 | if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation | ||
589 | return `ORDER BY "score" ${direction}, "video"."views" ${direction}` | ||
590 | } | ||
591 | |||
592 | let firstSort: string | ||
593 | |||
594 | if (field.toLowerCase() === 'match') { // Search | ||
595 | firstSort = '"similarity"' | ||
596 | } else if (field === 'originallyPublishedAt') { | ||
597 | firstSort = '"publishedAtForOrder"' | ||
598 | } else if (field.includes('.')) { | ||
599 | firstSort = field | ||
600 | } else { | ||
601 | firstSort = `"video"."${field}"` | ||
602 | } | ||
603 | |||
604 | return `ORDER BY ${firstSort} ${direction}, "video"."id" ASC` | ||
605 | } | ||
606 | |||
607 | private setLimit (countArg: number) { | ||
608 | const count = parseInt(countArg + '', 10) | ||
609 | this.limit = `LIMIT ${count}` | ||
610 | } | ||
611 | |||
612 | private setOffset (startArg: number) { | ||
613 | const start = parseInt(startArg + '', 10) | ||
614 | this.offset = `OFFSET ${start}` | ||
615 | } | ||
616 | } | ||
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/videos-model-list-query-builder.ts new file mode 100644 index 000000000..e61c51de8 --- /dev/null +++ b/server/models/video/sql/videos-model-list-query-builder.ts | |||
@@ -0,0 +1,71 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' | ||
3 | import { VideoModelBuilder } from './shared/video-model-builder' | ||
4 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' | ||
5 | |||
6 | /** | ||
7 | * | ||
8 | * Build videos list SQL query and create video models | ||
9 | * | ||
10 | */ | ||
11 | |||
12 | export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder { | ||
13 | protected attributes: { [key: string]: string } | ||
14 | |||
15 | private innerQuery: string | ||
16 | private innerSort: string | ||
17 | |||
18 | private readonly videoModelBuilder: VideoModelBuilder | ||
19 | |||
20 | constructor (protected readonly sequelize: Sequelize) { | ||
21 | super('list') | ||
22 | |||
23 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) | ||
24 | } | ||
25 | |||
26 | queryVideos (options: BuildVideosListQueryOptions) { | ||
27 | this.buildInnerQuery(options) | ||
28 | this.buildListQueryFromIdsQuery(options) | ||
29 | |||
30 | return this.runQuery() | ||
31 | .then(rows => this.videoModelBuilder.buildVideosFromRows(rows)) | ||
32 | } | ||
33 | |||
34 | private buildInnerQuery (options: BuildVideosListQueryOptions) { | ||
35 | const idsQueryBuilder = new VideosIdListQueryBuilder(this.sequelize) | ||
36 | const { query, sort, replacements } = idsQueryBuilder.getIdsListQueryAndSort(options) | ||
37 | |||
38 | this.replacements = replacements | ||
39 | this.innerQuery = query | ||
40 | this.innerSort = sort | ||
41 | } | ||
42 | |||
43 | private buildListQueryFromIdsQuery (options: BuildVideosListQueryOptions) { | ||
44 | this.attributes = { | ||
45 | '"video".*': '' | ||
46 | } | ||
47 | |||
48 | this.addJoin('INNER JOIN "video" ON "tmp"."id" = "video"."id"') | ||
49 | |||
50 | this.includeChannels() | ||
51 | this.includeAccounts() | ||
52 | this.includeThumbnails() | ||
53 | |||
54 | if (options.withFiles) { | ||
55 | this.includeWebtorrentFiles() | ||
56 | this.includeStreamingPlaylistFiles() | ||
57 | } | ||
58 | |||
59 | if (options.user) { | ||
60 | this.includeUserHistory(options.user.id) | ||
61 | } | ||
62 | |||
63 | if (options.videoPlaylistId) { | ||
64 | this.includePlaylist(options.videoPlaylistId) | ||
65 | } | ||
66 | |||
67 | const select = this.buildSelect() | ||
68 | |||
69 | this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}` | ||
70 | } | ||
71 | } | ||
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index d04205703..c1eebe27f 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { col, fn, QueryTypes, Transaction } from 'sequelize' | 1 | import { col, fn, QueryTypes, Transaction } from 'sequelize' |
2 | import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MTag } from '@server/types/models' | 3 | import { MTag } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' | 5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' |
5 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' | 6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' |
6 | import { throwIfNotValid } from '../utils' | 7 | import { throwIfNotValid } from '../utils' |
@@ -21,7 +22,7 @@ import { VideoTagModel } from './video-tag' | |||
21 | } | 22 | } |
22 | ] | 23 | ] |
23 | }) | 24 | }) |
24 | export class TagModel extends Model { | 25 | export class TagModel extends Model<Partial<AttributesOnly<TagModel>>> { |
25 | 26 | ||
26 | @AllowNull(false) | 27 | @AllowNull(false) |
27 | @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag')) | 28 | @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag')) |
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index f1187c8d6..3388478d9 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -17,6 +17,7 @@ import { | |||
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | 18 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' |
19 | import { MThumbnail, MThumbnailVideo, MVideo } from '@server/types/models' | 19 | import { MThumbnail, MThumbnailVideo, MVideo } from '@server/types/models' |
20 | import { AttributesOnly } from '@shared/core-utils' | ||
20 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
21 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
22 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
@@ -40,7 +41,7 @@ import { VideoPlaylistModel } from './video-playlist' | |||
40 | } | 41 | } |
41 | ] | 42 | ] |
42 | }) | 43 | }) |
43 | export class ThumbnailModel extends Model { | 44 | export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>>> { |
44 | 45 | ||
45 | @AllowNull(false) | 46 | @AllowNull(false) |
46 | @Column | 47 | @Column |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index aa18896da..98f4ec9c5 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { FindOptions } from 'sequelize' | 1 | import { FindOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/types/models' | 3 | import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' | 5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' |
5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' | 6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' |
6 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
@@ -18,7 +19,7 @@ import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel | |||
18 | } | 19 | } |
19 | ] | 20 | ] |
20 | }) | 21 | }) |
21 | export class VideoBlacklistModel extends Model { | 22 | export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlacklistModel>>> { |
22 | 23 | ||
23 | @AllowNull(true) | 24 | @AllowNull(true) |
24 | @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason', true)) | 25 | @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason', true)) |
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index bfdec73e9..d24be56c3 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -15,8 +15,9 @@ import { | |||
15 | Table, | 15 | Table, |
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { v4 as uuidv4 } from 'uuid' | 18 | import { buildUUID } from '@server/helpers/uuid' |
19 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' | 19 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' |
20 | import { AttributesOnly } from '@shared/core-utils' | ||
20 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' | 21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' |
21 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' | 22 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' |
22 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
@@ -57,7 +58,7 @@ export enum ScopeNames { | |||
57 | } | 58 | } |
58 | ] | 59 | ] |
59 | }) | 60 | }) |
60 | export class VideoCaptionModel extends Model { | 61 | export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaptionModel>>> { |
61 | @CreatedAt | 62 | @CreatedAt |
62 | createdAt: Date | 63 | createdAt: Date |
63 | 64 | ||
@@ -90,9 +91,9 @@ export class VideoCaptionModel extends Model { | |||
90 | Video: VideoModel | 91 | Video: VideoModel |
91 | 92 | ||
92 | @BeforeDestroy | 93 | @BeforeDestroy |
93 | static async removeFiles (instance: VideoCaptionModel) { | 94 | static async removeFiles (instance: VideoCaptionModel, options) { |
94 | if (!instance.Video) { | 95 | if (!instance.Video) { |
95 | instance.Video = await instance.$get('Video') | 96 | instance.Video = await instance.$get('Video', { transaction: options.transaction }) |
96 | } | 97 | } |
97 | 98 | ||
98 | if (instance.isOwned()) { | 99 | if (instance.isOwned()) { |
@@ -108,7 +109,7 @@ export class VideoCaptionModel extends Model { | |||
108 | return undefined | 109 | return undefined |
109 | } | 110 | } |
110 | 111 | ||
111 | static loadByVideoIdAndLanguage (videoId: string | number, language: string): Promise<MVideoCaptionVideo> { | 112 | static loadByVideoIdAndLanguage (videoId: string | number, language: string, transaction?: Transaction): Promise<MVideoCaptionVideo> { |
112 | const videoInclude = { | 113 | const videoInclude = { |
113 | model: VideoModel.unscoped(), | 114 | model: VideoModel.unscoped(), |
114 | attributes: [ 'id', 'remote', 'uuid' ], | 115 | attributes: [ 'id', 'remote', 'uuid' ], |
@@ -121,7 +122,8 @@ export class VideoCaptionModel extends Model { | |||
121 | }, | 122 | }, |
122 | include: [ | 123 | include: [ |
123 | videoInclude | 124 | videoInclude |
124 | ] | 125 | ], |
126 | transaction | ||
125 | } | 127 | } |
126 | 128 | ||
127 | return VideoCaptionModel.findOne(query) | 129 | return VideoCaptionModel.findOne(query) |
@@ -144,19 +146,21 @@ export class VideoCaptionModel extends Model { | |||
144 | } | 146 | } |
145 | 147 | ||
146 | static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) { | 148 | static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) { |
147 | const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language) | 149 | const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language, transaction) |
150 | |||
148 | // Delete existing file | 151 | // Delete existing file |
149 | if (existing) await existing.destroy({ transaction }) | 152 | if (existing) await existing.destroy({ transaction }) |
150 | 153 | ||
151 | return caption.save({ transaction }) | 154 | return caption.save({ transaction }) |
152 | } | 155 | } |
153 | 156 | ||
154 | static listVideoCaptions (videoId: number): Promise<MVideoCaptionVideo[]> { | 157 | static listVideoCaptions (videoId: number, transaction?: Transaction): Promise<MVideoCaptionVideo[]> { |
155 | const query = { | 158 | const query = { |
156 | order: [ [ 'language', 'ASC' ] ] as OrderItem[], | 159 | order: [ [ 'language', 'ASC' ] ] as OrderItem[], |
157 | where: { | 160 | where: { |
158 | videoId | 161 | videoId |
159 | } | 162 | }, |
163 | transaction | ||
160 | } | 164 | } |
161 | 165 | ||
162 | return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query) | 166 | return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query) |
@@ -178,7 +182,7 @@ export class VideoCaptionModel extends Model { | |||
178 | } | 182 | } |
179 | 183 | ||
180 | static generateCaptionName (language: string) { | 184 | static generateCaptionName (language: string) { |
181 | return `${uuidv4()}-${language}.vtt` | 185 | return `${buildUUID()}-${language}.vtt` |
182 | } | 186 | } |
183 | 187 | ||
184 | isOwned () { | 188 | isOwned () { |
@@ -210,4 +214,10 @@ export class VideoCaptionModel extends Model { | |||
210 | 214 | ||
211 | return this.fileUrl | 215 | return this.fileUrl |
212 | } | 216 | } |
217 | |||
218 | isEqual (this: MVideoCaption, other: MVideoCaption) { | ||
219 | if (this.fileUrl) return this.fileUrl === other.fileUrl | ||
220 | |||
221 | return this.filename === other.filename | ||
222 | } | ||
213 | } | 223 | } |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 298e8bfe2..7d20a954d 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership' | 2 | import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' | 4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' |
4 | import { AccountModel } from '../account/account' | 5 | import { AccountModel } from '../account/account' |
5 | import { getSort } from '../utils' | 6 | import { getSort } from '../utils' |
@@ -53,7 +54,7 @@ enum ScopeNames { | |||
53 | ] | 54 | ] |
54 | } | 55 | } |
55 | })) | 56 | })) |
56 | export class VideoChangeOwnershipModel extends Model { | 57 | export class VideoChangeOwnershipModel extends Model<Partial<AttributesOnly<VideoChangeOwnershipModel>>> { |
57 | @CreatedAt | 58 | @CreatedAt |
58 | createdAt: Date | 59 | createdAt: Date |
59 | 60 | ||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 081b21f2d..183e7448c 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -19,6 +19,7 @@ import { | |||
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | 20 | import { setAsUpdated } from '@server/helpers/database-utils' |
21 | import { MAccountActor } from '@server/types/models' | 21 | import { MAccountActor } from '@server/types/models' |
22 | import { AttributesOnly } from '@shared/core-utils' | ||
22 | import { ActivityPubActor } from '../../../shared/models/activitypub' | 23 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
23 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' | 24 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' |
24 | import { | 25 | import { |
@@ -36,9 +37,9 @@ import { | |||
36 | MChannelSummaryFormattable | 37 | MChannelSummaryFormattable |
37 | } from '../../types/models/video' | 38 | } from '../../types/models/video' |
38 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 39 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
39 | import { ActorImageModel } from '../account/actor-image' | 40 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' |
40 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 41 | import { ActorFollowModel } from '../actor/actor-follow' |
41 | import { ActorFollowModel } from '../activitypub/actor-follow' | 42 | import { ActorImageModel } from '../actor/actor-image' |
42 | import { ServerModel } from '../server/server' | 43 | import { ServerModel } from '../server/server' |
43 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
44 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
@@ -246,7 +247,7 @@ export type SummaryOptions = { | |||
246 | } | 247 | } |
247 | ] | 248 | ] |
248 | }) | 249 | }) |
249 | export class VideoChannelModel extends Model { | 250 | export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> { |
250 | 251 | ||
251 | @AllowNull(false) | 252 | @AllowNull(false) |
252 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) | 253 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) |
@@ -290,8 +291,7 @@ export class VideoChannelModel extends Model { | |||
290 | @BelongsTo(() => AccountModel, { | 291 | @BelongsTo(() => AccountModel, { |
291 | foreignKey: { | 292 | foreignKey: { |
292 | allowNull: false | 293 | allowNull: false |
293 | }, | 294 | } |
294 | hooks: true | ||
295 | }) | 295 | }) |
296 | Account: AccountModel | 296 | Account: AccountModel |
297 | 297 | ||
@@ -433,8 +433,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
433 | sort: string | 433 | sort: string |
434 | }) { | 434 | }) { |
435 | const attributesInclude = [] | 435 | const attributesInclude = [] |
436 | const escapedSearch = VideoModel.sequelize.escape(options.search) | 436 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) |
437 | const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') | 437 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') |
438 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | 438 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) |
439 | 439 | ||
440 | const query = { | 440 | const query = { |
@@ -521,10 +521,10 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
521 | }) | 521 | }) |
522 | } | 522 | } |
523 | 523 | ||
524 | static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> { | 524 | static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> { |
525 | return VideoChannelModel.unscoped() | 525 | return VideoChannelModel.unscoped() |
526 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) | 526 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
527 | .findByPk(id) | 527 | .findByPk(id, { transaction }) |
528 | } | 528 | } |
529 | 529 | ||
530 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { | 530 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 151c2bc81..e933989ae 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -16,10 +16,11 @@ import { | |||
16 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { getServerActor } from '@server/models/application/application' | 17 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { AttributesOnly } from '@shared/core-utils' | ||
19 | import { VideoPrivacy } from '@shared/models' | 20 | import { VideoPrivacy } from '@shared/models' |
20 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
21 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
22 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/video-comment.model' | 23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' |
23 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | 24 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' |
24 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 25 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
25 | import { regexpCapture } from '../../helpers/regexp' | 26 | import { regexpCapture } from '../../helpers/regexp' |
@@ -39,7 +40,7 @@ import { | |||
39 | } from '../../types/models/video' | 40 | } from '../../types/models/video' |
40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 41 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
41 | import { AccountModel } from '../account/account' | 42 | import { AccountModel } from '../account/account' |
42 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 43 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' |
43 | import { | 44 | import { |
44 | buildBlockedAccountSQL, | 45 | buildBlockedAccountSQL, |
45 | buildBlockedAccountSQLOptimized, | 46 | buildBlockedAccountSQLOptimized, |
@@ -68,14 +69,10 @@ export enum ScopeNames { | |||
68 | Sequelize.literal( | 69 | Sequelize.literal( |
69 | '(' + | 70 | '(' + |
70 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + | 71 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + |
71 | 'SELECT COUNT("replies"."id") - (' + | 72 | 'SELECT COUNT("replies"."id") ' + |
72 | 'SELECT COUNT("replies"."id") ' + | ||
73 | 'FROM "videoComment" AS "replies" ' + | ||
74 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
75 | 'AND "accountId" IN (SELECT "id" FROM "blocklist")' + | ||
76 | ')' + | ||
77 | 'FROM "videoComment" AS "replies" ' + | 73 | 'FROM "videoComment" AS "replies" ' + |
78 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | 74 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + |
75 | 'AND "deletedAt" IS NULL ' + | ||
79 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | 76 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + |
80 | ')' | 77 | ')' |
81 | ), | 78 | ), |
@@ -173,7 +170,7 @@ export enum ScopeNames { | |||
173 | } | 170 | } |
174 | ] | 171 | ] |
175 | }) | 172 | }) |
176 | export class VideoCommentModel extends Model { | 173 | export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoCommentModel>>> { |
177 | @CreatedAt | 174 | @CreatedAt |
178 | createdAt: Date | 175 | createdAt: Date |
179 | 176 | ||
@@ -742,6 +739,12 @@ export class VideoCommentModel extends Model { | |||
742 | return this.Account.isOwned() | 739 | return this.Account.isOwned() |
743 | } | 740 | } |
744 | 741 | ||
742 | markAsDeleted () { | ||
743 | this.text = '' | ||
744 | this.deletedAt = new Date() | ||
745 | this.accountId = null | ||
746 | } | ||
747 | |||
745 | isDeleted () { | 748 | isDeleted () { |
746 | return this.deletedAt !== null | 749 | return this.deletedAt !== null |
747 | } | 750 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 0b5946149..22cf63804 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -25,6 +25,7 @@ import { logger } from '@server/helpers/logger' | |||
25 | import { extractVideo } from '@server/helpers/video' | 25 | import { extractVideo } from '@server/helpers/video' |
26 | import { getTorrentFilePath } from '@server/lib/video-paths' | 26 | import { getTorrentFilePath } from '@server/lib/video-paths' |
27 | import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' | 27 | import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' |
28 | import { AttributesOnly } from '@shared/core-utils' | ||
28 | import { | 29 | import { |
29 | isVideoFileExtnameValid, | 30 | isVideoFileExtnameValid, |
30 | isVideoFileInfoHashValid, | 31 | isVideoFileInfoHashValid, |
@@ -149,7 +150,7 @@ export enum ScopeNames { | |||
149 | } | 150 | } |
150 | ] | 151 | ] |
151 | }) | 152 | }) |
152 | export class VideoFileModel extends Model { | 153 | export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>>> { |
153 | @CreatedAt | 154 | @CreatedAt |
154 | createdAt: Date | 155 | createdAt: Date |
155 | 156 | ||
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8324166cc..5c73fb07c 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -13,15 +13,16 @@ import { | |||
13 | Table, | 13 | Table, |
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
16 | import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import' | 17 | import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import' |
18 | import { AttributesOnly } from '@shared/core-utils' | ||
17 | import { VideoImport, VideoImportState } from '../../../shared' | 19 | import { VideoImport, VideoImportState } from '../../../shared' |
18 | import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' | 20 | import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' |
19 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' | 21 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' |
20 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' | 22 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' |
21 | import { UserModel } from '../account/user' | 23 | import { UserModel } from '../user/user' |
22 | import { getSort, throwIfNotValid } from '../utils' | 24 | import { getSort, throwIfNotValid } from '../utils' |
23 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' | 25 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' |
24 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
25 | 26 | ||
26 | @DefaultScope(() => ({ | 27 | @DefaultScope(() => ({ |
27 | include: [ | 28 | include: [ |
@@ -52,7 +53,7 @@ import { afterCommitIfTransaction } from '@server/helpers/database-utils' | |||
52 | } | 53 | } |
53 | ] | 54 | ] |
54 | }) | 55 | }) |
55 | export class VideoImportModel extends Model { | 56 | export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportModel>>> { |
56 | @CreatedAt | 57 | @CreatedAt |
57 | createdAt: Date | 58 | createdAt: Date |
58 | 59 | ||
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts index cb4a9b896..014491d50 100644 --- a/server/models/video/video-live.ts +++ b/server/models/video/video-live.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { WEBSERVER } from '@server/initializers/constants' | 2 | import { WEBSERVER } from '@server/initializers/constants' |
3 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' | 3 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { LiveVideo, VideoState } from '@shared/models' | 5 | import { LiveVideo, VideoState } from '@shared/models' |
5 | import { VideoModel } from './video' | 6 | import { VideoModel } from './video' |
6 | import { VideoBlacklistModel } from './video-blacklist' | 7 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -28,7 +29,7 @@ import { VideoBlacklistModel } from './video-blacklist' | |||
28 | } | 29 | } |
29 | ] | 30 | ] |
30 | }) | 31 | }) |
31 | export class VideoLiveModel extends Model { | 32 | export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>>> { |
32 | 33 | ||
33 | @AllowNull(true) | 34 | @AllowNull(true) |
34 | @Column(DataType.STRING) | 35 | @Column(DataType.STRING) |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index d2d7e2740..e6906cb19 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -32,6 +32,7 @@ import { AccountModel } from '../account/account' | |||
32 | import { getSort, throwIfNotValid } from '../utils' | 32 | import { getSort, throwIfNotValid } from '../utils' |
33 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 33 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
34 | import { VideoPlaylistModel } from './video-playlist' | 34 | import { VideoPlaylistModel } from './video-playlist' |
35 | import { AttributesOnly } from '@shared/core-utils' | ||
35 | 36 | ||
36 | @Table({ | 37 | @Table({ |
37 | tableName: 'videoPlaylistElement', | 38 | tableName: 'videoPlaylistElement', |
@@ -48,7 +49,7 @@ import { VideoPlaylistModel } from './video-playlist' | |||
48 | } | 49 | } |
49 | ] | 50 | ] |
50 | }) | 51 | }) |
51 | export class VideoPlaylistElementModel extends Model { | 52 | export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> { |
52 | @CreatedAt | 53 | @CreatedAt |
53 | createdAt: Date | 54 | createdAt: Date |
54 | 55 | ||
@@ -274,7 +275,8 @@ export class VideoPlaylistElementModel extends Model { | |||
274 | validate: false // We use a literal to update the position | 275 | validate: false // We use a literal to update the position |
275 | } | 276 | } |
276 | 277 | ||
277 | return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query) | 278 | const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) |
279 | return VideoPlaylistElementModel.update({ position: positionQuery as any }, query) | ||
278 | } | 280 | } |
279 | 281 | ||
280 | static increasePositionOf ( | 282 | static increasePositionOf ( |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index efe5be36d..af81c9906 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -17,8 +17,10 @@ import { | |||
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { v4 as uuidv4 } from 'uuid' | 20 | import { setAsUpdated } from '@server/helpers/database-utils' |
21 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' | ||
21 | import { MAccountId, MChannelId } from '@server/types/models' | 22 | import { MAccountId, MChannelId } from '@server/types/models' |
23 | import { AttributesOnly } from '@shared/core-utils' | ||
22 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | 24 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' |
23 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 25 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
24 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 26 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
@@ -50,11 +52,19 @@ import { | |||
50 | MVideoPlaylistIdWithElements | 52 | MVideoPlaylistIdWithElements |
51 | } from '../../types/models/video/video-playlist' | 53 | } from '../../types/models/video/video-playlist' |
52 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
53 | import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' | 55 | import { ActorModel } from '../actor/actor' |
56 | import { | ||
57 | buildServerIdsFollowedBy, | ||
58 | buildTrigramSearchIndex, | ||
59 | buildWhereIdOrUUID, | ||
60 | createSimilarityAttribute, | ||
61 | getPlaylistSort, | ||
62 | isOutdated, | ||
63 | throwIfNotValid | ||
64 | } from '../utils' | ||
54 | import { ThumbnailModel } from './thumbnail' | 65 | import { ThumbnailModel } from './thumbnail' |
55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 66 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
56 | import { VideoPlaylistElementModel } from './video-playlist-element' | 67 | import { VideoPlaylistElementModel } from './video-playlist-element' |
57 | import { ActorModel } from '../activitypub/actor' | ||
58 | 68 | ||
59 | enum ScopeNames { | 69 | enum ScopeNames { |
60 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 70 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
@@ -72,6 +82,11 @@ type AvailableForListOptions = { | |||
72 | videoChannelId?: number | 82 | videoChannelId?: number |
73 | listMyPlaylists?: boolean | 83 | listMyPlaylists?: boolean |
74 | search?: string | 84 | search?: string |
85 | withVideos?: boolean | ||
86 | } | ||
87 | |||
88 | function getVideoLengthSelect () { | ||
89 | return 'SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id"' | ||
75 | } | 90 | } |
76 | 91 | ||
77 | @Scopes(() => ({ | 92 | @Scopes(() => ({ |
@@ -87,7 +102,7 @@ type AvailableForListOptions = { | |||
87 | attributes: { | 102 | attributes: { |
88 | include: [ | 103 | include: [ |
89 | [ | 104 | [ |
90 | literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), | 105 | literal(`(${getVideoLengthSelect()})`), |
91 | 'videosLength' | 106 | 'videosLength' |
92 | ] | 107 | ] |
93 | ] | 108 | ] |
@@ -176,11 +191,28 @@ type AvailableForListOptions = { | |||
176 | }) | 191 | }) |
177 | } | 192 | } |
178 | 193 | ||
194 | if (options.withVideos === true) { | ||
195 | whereAnd.push( | ||
196 | literal(`(${getVideoLengthSelect()}) != 0`) | ||
197 | ) | ||
198 | } | ||
199 | |||
200 | const attributesInclude = [] | ||
201 | |||
179 | if (options.search) { | 202 | if (options.search) { |
203 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) | ||
204 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') | ||
205 | attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search)) | ||
206 | |||
180 | whereAnd.push({ | 207 | whereAnd.push({ |
181 | name: { | 208 | [Op.or]: [ |
182 | [Op.iLike]: '%' + options.search + '%' | 209 | Sequelize.literal( |
183 | } | 210 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' |
211 | ), | ||
212 | Sequelize.literal( | ||
213 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | ||
214 | ) | ||
215 | ] | ||
184 | }) | 216 | }) |
185 | } | 217 | } |
186 | 218 | ||
@@ -189,6 +221,9 @@ type AvailableForListOptions = { | |||
189 | } | 221 | } |
190 | 222 | ||
191 | return { | 223 | return { |
224 | attributes: { | ||
225 | include: attributesInclude | ||
226 | }, | ||
192 | where, | 227 | where, |
193 | include: [ | 228 | include: [ |
194 | { | 229 | { |
@@ -209,6 +244,8 @@ type AvailableForListOptions = { | |||
209 | @Table({ | 244 | @Table({ |
210 | tableName: 'videoPlaylist', | 245 | tableName: 'videoPlaylist', |
211 | indexes: [ | 246 | indexes: [ |
247 | buildTrigramSearchIndex('video_playlist_name_trigram', 'name'), | ||
248 | |||
212 | { | 249 | { |
213 | fields: [ 'ownerAccountId' ] | 250 | fields: [ 'ownerAccountId' ] |
214 | }, | 251 | }, |
@@ -221,7 +258,7 @@ type AvailableForListOptions = { | |||
221 | } | 258 | } |
222 | ] | 259 | ] |
223 | }) | 260 | }) |
224 | export class VideoPlaylistModel extends Model { | 261 | export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlaylistModel>>> { |
225 | @CreatedAt | 262 | @CreatedAt |
226 | createdAt: Date | 263 | createdAt: Date |
227 | 264 | ||
@@ -312,6 +349,7 @@ export class VideoPlaylistModel extends Model { | |||
312 | videoChannelId?: number | 349 | videoChannelId?: number |
313 | listMyPlaylists?: boolean | 350 | listMyPlaylists?: boolean |
314 | search?: string | 351 | search?: string |
352 | withVideos?: boolean // false by default | ||
315 | }) { | 353 | }) { |
316 | const query = { | 354 | const query = { |
317 | offset: options.start, | 355 | offset: options.start, |
@@ -329,7 +367,8 @@ export class VideoPlaylistModel extends Model { | |||
329 | accountId: options.accountId, | 367 | accountId: options.accountId, |
330 | videoChannelId: options.videoChannelId, | 368 | videoChannelId: options.videoChannelId, |
331 | listMyPlaylists: options.listMyPlaylists, | 369 | listMyPlaylists: options.listMyPlaylists, |
332 | search: options.search | 370 | search: options.search, |
371 | withVideos: options.withVideos || false | ||
333 | } as AvailableForListOptions | 372 | } as AvailableForListOptions |
334 | ] | 373 | ] |
335 | }, | 374 | }, |
@@ -345,6 +384,21 @@ export class VideoPlaylistModel extends Model { | |||
345 | }) | 384 | }) |
346 | } | 385 | } |
347 | 386 | ||
387 | static searchForApi (options: { | ||
388 | followerActorId: number | ||
389 | start: number | ||
390 | count: number | ||
391 | sort: string | ||
392 | search?: string | ||
393 | }) { | ||
394 | return VideoPlaylistModel.listForApi({ | ||
395 | ...options, | ||
396 | type: VideoPlaylistType.REGULAR, | ||
397 | listMyPlaylists: false, | ||
398 | withVideos: true | ||
399 | }) | ||
400 | } | ||
401 | |||
348 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { | 402 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { |
349 | const where = { | 403 | const where = { |
350 | privacy: VideoPlaylistPrivacy.PUBLIC | 404 | privacy: VideoPlaylistPrivacy.PUBLIC |
@@ -443,6 +497,18 @@ export class VideoPlaylistModel extends Model { | |||
443 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) | 497 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) |
444 | } | 498 | } |
445 | 499 | ||
500 | static loadByUrlWithAccountAndChannelSummary (url: string): Promise<MVideoPlaylistFullSummary> { | ||
501 | const query = { | ||
502 | where: { | ||
503 | url | ||
504 | } | ||
505 | } | ||
506 | |||
507 | return VideoPlaylistModel | ||
508 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) | ||
509 | .findOne(query) | ||
510 | } | ||
511 | |||
446 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { | 512 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
447 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' | 513 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' |
448 | } | 514 | } |
@@ -479,7 +545,7 @@ export class VideoPlaylistModel extends Model { | |||
479 | generateThumbnailName () { | 545 | generateThumbnailName () { |
480 | const extension = '.jpg' | 546 | const extension = '.jpg' |
481 | 547 | ||
482 | return 'playlist-' + uuidv4() + extension | 548 | return 'playlist-' + buildUUID() + extension |
483 | } | 549 | } |
484 | 550 | ||
485 | getThumbnailUrl () { | 551 | getThumbnailUrl () { |
@@ -495,7 +561,7 @@ export class VideoPlaylistModel extends Model { | |||
495 | } | 561 | } |
496 | 562 | ||
497 | getWatchUrl () { | 563 | getWatchUrl () { |
498 | return WEBSERVER.URL + '/videos/watch/playlist/' + this.uuid | 564 | return WEBSERVER.URL + '/w/p/' + this.uuid |
499 | } | 565 | } |
500 | 566 | ||
501 | getEmbedStaticPath () { | 567 | getEmbedStaticPath () { |
@@ -530,9 +596,11 @@ export class VideoPlaylistModel extends Model { | |||
530 | } | 596 | } |
531 | 597 | ||
532 | setAsRefreshed () { | 598 | setAsRefreshed () { |
533 | this.changed('updatedAt', true) | 599 | return setAsUpdated('videoPlaylist', this.id) |
600 | } | ||
534 | 601 | ||
535 | return this.save() | 602 | setVideosLength (videosLength: number) { |
603 | this.set('videosLength' as any, videosLength, { raw: true }) | ||
536 | } | 604 | } |
537 | 605 | ||
538 | isOwned () { | 606 | isOwned () { |
@@ -549,8 +617,12 @@ export class VideoPlaylistModel extends Model { | |||
549 | return { | 617 | return { |
550 | id: this.id, | 618 | id: this.id, |
551 | uuid: this.uuid, | 619 | uuid: this.uuid, |
620 | shortUUID: uuidToShort(this.uuid), | ||
621 | |||
552 | isLocal: this.isOwned(), | 622 | isLocal: this.isOwned(), |
553 | 623 | ||
624 | url: this.url, | ||
625 | |||
554 | displayName: this.name, | 626 | displayName: this.name, |
555 | description: this.description, | 627 | description: this.description, |
556 | privacy: { | 628 | privacy: { |
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts deleted file mode 100644 index 155afe64b..000000000 --- a/server/models/video/video-query-builder.ts +++ /dev/null | |||
@@ -1,599 +0,0 @@ | |||
1 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' | ||
2 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' | ||
3 | import { Model } from 'sequelize-typescript' | ||
4 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
5 | import validator from 'validator' | ||
6 | import { exists } from '@server/helpers/custom-validators/misc' | ||
7 | |||
8 | export type BuildVideosQueryOptions = { | ||
9 | attributes?: string[] | ||
10 | |||
11 | serverAccountId: number | ||
12 | followerActorId: number | ||
13 | includeLocalVideos: boolean | ||
14 | |||
15 | count: number | ||
16 | start: number | ||
17 | sort: string | ||
18 | |||
19 | nsfw?: boolean | ||
20 | filter?: VideoFilter | ||
21 | isLive?: boolean | ||
22 | |||
23 | categoryOneOf?: number[] | ||
24 | licenceOneOf?: number[] | ||
25 | languageOneOf?: string[] | ||
26 | tagsOneOf?: string[] | ||
27 | tagsAllOf?: string[] | ||
28 | |||
29 | withFiles?: boolean | ||
30 | |||
31 | accountId?: number | ||
32 | videoChannelId?: number | ||
33 | |||
34 | videoPlaylistId?: number | ||
35 | |||
36 | trendingAlgorithm?: string // best, hot, or any other algorithm implemented | ||
37 | trendingDays?: number | ||
38 | |||
39 | user?: MUserAccountId | ||
40 | historyOfUser?: MUserId | ||
41 | |||
42 | startDate?: string // ISO 8601 | ||
43 | endDate?: string // ISO 8601 | ||
44 | originallyPublishedStartDate?: string | ||
45 | originallyPublishedEndDate?: string | ||
46 | |||
47 | durationMin?: number // seconds | ||
48 | durationMax?: number // seconds | ||
49 | |||
50 | search?: string | ||
51 | |||
52 | isCount?: boolean | ||
53 | |||
54 | group?: string | ||
55 | having?: string | ||
56 | } | ||
57 | |||
58 | function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) { | ||
59 | const and: string[] = [] | ||
60 | const joins: string[] = [] | ||
61 | const replacements: any = {} | ||
62 | const cte: string[] = [] | ||
63 | |||
64 | let attributes: string[] = options.attributes || [ '"video"."id"' ] | ||
65 | let group = options.group || '' | ||
66 | const having = options.having || '' | ||
67 | |||
68 | joins.push( | ||
69 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId"' + | ||
70 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId"' + | ||
71 | 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"' | ||
72 | ) | ||
73 | |||
74 | and.push('"video"."id" NOT IN (SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")') | ||
75 | |||
76 | if (options.serverAccountId) { | ||
77 | const blockerIds = [ options.serverAccountId ] | ||
78 | if (options.user) blockerIds.push(options.user.Account.id) | ||
79 | |||
80 | const inClause = createSafeIn(model, blockerIds) | ||
81 | |||
82 | and.push( | ||
83 | 'NOT EXISTS (' + | ||
84 | ' SELECT 1 FROM "accountBlocklist" ' + | ||
85 | ' WHERE "accountBlocklist"."accountId" IN (' + inClause + ') ' + | ||
86 | ' AND "accountBlocklist"."targetAccountId" = "account"."id" ' + | ||
87 | ')' + | ||
88 | 'AND NOT EXISTS (' + | ||
89 | ' SELECT 1 FROM "serverBlocklist" WHERE "serverBlocklist"."accountId" IN (' + inClause + ') ' + | ||
90 | ' AND "serverBlocklist"."targetServerId" = "accountActor"."serverId"' + | ||
91 | ')' | ||
92 | ) | ||
93 | } | ||
94 | |||
95 | // Only list public/published videos | ||
96 | if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) { | ||
97 | and.push( | ||
98 | `("video"."state" = ${VideoState.PUBLISHED} OR ` + | ||
99 | `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` | ||
100 | ) | ||
101 | |||
102 | if (options.user) { | ||
103 | and.push( | ||
104 | `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})` | ||
105 | ) | ||
106 | } else { // Or only public videos | ||
107 | and.push( | ||
108 | `"video"."privacy" = ${VideoPrivacy.PUBLIC}` | ||
109 | ) | ||
110 | } | ||
111 | } | ||
112 | |||
113 | if (options.videoPlaylistId) { | ||
114 | joins.push( | ||
115 | 'INNER JOIN "videoPlaylistElement" "video"."id" = "videoPlaylistElement"."videoId" ' + | ||
116 | 'AND "videoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
117 | ) | ||
118 | |||
119 | replacements.videoPlaylistId = options.videoPlaylistId | ||
120 | } | ||
121 | |||
122 | if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) { | ||
123 | and.push('"video"."remote" IS FALSE') | ||
124 | } | ||
125 | |||
126 | if (options.accountId) { | ||
127 | and.push('"account"."id" = :accountId') | ||
128 | replacements.accountId = options.accountId | ||
129 | } | ||
130 | |||
131 | if (options.videoChannelId) { | ||
132 | and.push('"videoChannel"."id" = :videoChannelId') | ||
133 | replacements.videoChannelId = options.videoChannelId | ||
134 | } | ||
135 | |||
136 | if (options.followerActorId) { | ||
137 | let query = | ||
138 | '(' + | ||
139 | ' EXISTS (' + | ||
140 | ' SELECT 1 FROM "videoShare" ' + | ||
141 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + | ||
142 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + | ||
143 | ' WHERE "videoShare"."videoId" = "video"."id"' + | ||
144 | ' )' + | ||
145 | ' OR' + | ||
146 | ' EXISTS (' + | ||
147 | ' SELECT 1 from "actorFollow" ' + | ||
148 | ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + | ||
149 | ' AND "actorFollow"."state" = \'accepted\'' + | ||
150 | ' )' | ||
151 | |||
152 | if (options.includeLocalVideos) { | ||
153 | query += ' OR "video"."remote" IS FALSE' | ||
154 | } | ||
155 | |||
156 | query += ')' | ||
157 | |||
158 | and.push(query) | ||
159 | replacements.followerActorId = options.followerActorId | ||
160 | } | ||
161 | |||
162 | if (options.withFiles === true) { | ||
163 | and.push( | ||
164 | '(' + | ||
165 | ' EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id") ' + | ||
166 | ' OR EXISTS (' + | ||
167 | ' SELECT 1 FROM "videoStreamingPlaylist" ' + | ||
168 | ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' + | ||
169 | ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' + | ||
170 | ' )' + | ||
171 | ')' | ||
172 | ) | ||
173 | } | ||
174 | |||
175 | if (options.tagsOneOf) { | ||
176 | const tagsOneOfLower = options.tagsOneOf.map(t => t.toLowerCase()) | ||
177 | |||
178 | and.push( | ||
179 | 'EXISTS (' + | ||
180 | ' SELECT 1 FROM "videoTag" ' + | ||
181 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
182 | ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsOneOfLower) + ') ' + | ||
183 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
184 | ')' | ||
185 | ) | ||
186 | } | ||
187 | |||
188 | if (options.tagsAllOf) { | ||
189 | const tagsAllOfLower = options.tagsAllOf.map(t => t.toLowerCase()) | ||
190 | |||
191 | and.push( | ||
192 | 'EXISTS (' + | ||
193 | ' SELECT 1 FROM "videoTag" ' + | ||
194 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
195 | ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsAllOfLower) + ') ' + | ||
196 | ' AND "video"."id" = "videoTag"."videoId" ' + | ||
197 | ' GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length + | ||
198 | ')' | ||
199 | ) | ||
200 | } | ||
201 | |||
202 | if (options.nsfw === true) { | ||
203 | and.push('"video"."nsfw" IS TRUE') | ||
204 | } else if (options.nsfw === false) { | ||
205 | and.push('"video"."nsfw" IS FALSE') | ||
206 | } | ||
207 | |||
208 | if (options.isLive === true) { | ||
209 | and.push('"video"."isLive" IS TRUE') | ||
210 | } else if (options.isLive === false) { | ||
211 | and.push('"video"."isLive" IS FALSE') | ||
212 | } | ||
213 | |||
214 | if (options.categoryOneOf) { | ||
215 | and.push('"video"."category" IN (:categoryOneOf)') | ||
216 | replacements.categoryOneOf = options.categoryOneOf | ||
217 | } | ||
218 | |||
219 | if (options.licenceOneOf) { | ||
220 | and.push('"video"."licence" IN (:licenceOneOf)') | ||
221 | replacements.licenceOneOf = options.licenceOneOf | ||
222 | } | ||
223 | |||
224 | if (options.languageOneOf) { | ||
225 | const languages = options.languageOneOf.filter(l => l && l !== '_unknown') | ||
226 | const languagesQueryParts: string[] = [] | ||
227 | |||
228 | if (languages.length !== 0) { | ||
229 | languagesQueryParts.push('"video"."language" IN (:languageOneOf)') | ||
230 | replacements.languageOneOf = languages | ||
231 | |||
232 | languagesQueryParts.push( | ||
233 | 'EXISTS (' + | ||
234 | ' SELECT 1 FROM "videoCaption" WHERE "videoCaption"."language" ' + | ||
235 | ' IN (' + createSafeIn(model, languages) + ') AND ' + | ||
236 | ' "videoCaption"."videoId" = "video"."id"' + | ||
237 | ')' | ||
238 | ) | ||
239 | } | ||
240 | |||
241 | if (options.languageOneOf.includes('_unknown')) { | ||
242 | languagesQueryParts.push('"video"."language" IS NULL') | ||
243 | } | ||
244 | |||
245 | if (languagesQueryParts.length !== 0) { | ||
246 | and.push('(' + languagesQueryParts.join(' OR ') + ')') | ||
247 | } | ||
248 | } | ||
249 | |||
250 | // We don't exclude results in this so if we do a count we don't need to add this complex clause | ||
251 | if (options.isCount !== true) { | ||
252 | if (options.trendingDays) { | ||
253 | const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays) | ||
254 | |||
255 | joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate') | ||
256 | replacements.viewsGteDate = viewsGteDate | ||
257 | |||
258 | attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') | ||
259 | |||
260 | group = 'GROUP BY "video"."id"' | ||
261 | } else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) { | ||
262 | /** | ||
263 | * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, | ||
264 | * with fixed weights only applied to their log values. | ||
265 | * | ||
266 | * This algorithm gives little chance for an old video to have a good score, | ||
267 | * for which recent spikes in interactions could be a sign of "hotness" and | ||
268 | * justify a better score. However there are multiple ways to achieve that | ||
269 | * goal, which is left for later. Yes, this is a TODO :) | ||
270 | * | ||
271 | * notes: | ||
272 | * - weights and base score are in number of half-days. | ||
273 | * - all comments are counted, regardless of being written by the video author or not | ||
274 | * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58 | ||
275 | * - we have less interactions than on reddit, so multiply weights by an arbitrary factor | ||
276 | */ | ||
277 | const weights = { | ||
278 | like: 3 * 50, | ||
279 | dislike: -3 * 50, | ||
280 | view: Math.floor((1 / 3) * 50), | ||
281 | comment: 2 * 50, // a comment takes more time than a like to do, but can be done multiple times | ||
282 | history: -2 * 50 | ||
283 | } | ||
284 | |||
285 | joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') | ||
286 | |||
287 | let attribute = | ||
288 | `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) | ||
289 | `+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) | ||
290 | `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) | ||
291 | `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+) | ||
292 | '+ (SELECT (EXTRACT(epoch FROM "video"."publishedAt") - 1446156582) / 47000) ' // base score (in number of half-days) | ||
293 | |||
294 | if (options.trendingAlgorithm === 'best' && options.user) { | ||
295 | joins.push( | ||
296 | 'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser' | ||
297 | ) | ||
298 | replacements.bestUser = options.user.id | ||
299 | |||
300 | attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} ` | ||
301 | } | ||
302 | |||
303 | attribute += 'AS "score"' | ||
304 | attributes.push(attribute) | ||
305 | |||
306 | group = 'GROUP BY "video"."id"' | ||
307 | } | ||
308 | } | ||
309 | |||
310 | if (options.historyOfUser) { | ||
311 | joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"') | ||
312 | |||
313 | and.push('"userVideoHistory"."userId" = :historyOfUser') | ||
314 | replacements.historyOfUser = options.historyOfUser.id | ||
315 | } | ||
316 | |||
317 | if (options.startDate) { | ||
318 | and.push('"video"."publishedAt" >= :startDate') | ||
319 | replacements.startDate = options.startDate | ||
320 | } | ||
321 | |||
322 | if (options.endDate) { | ||
323 | and.push('"video"."publishedAt" <= :endDate') | ||
324 | replacements.endDate = options.endDate | ||
325 | } | ||
326 | |||
327 | if (options.originallyPublishedStartDate) { | ||
328 | and.push('"video"."originallyPublishedAt" >= :originallyPublishedStartDate') | ||
329 | replacements.originallyPublishedStartDate = options.originallyPublishedStartDate | ||
330 | } | ||
331 | |||
332 | if (options.originallyPublishedEndDate) { | ||
333 | and.push('"video"."originallyPublishedAt" <= :originallyPublishedEndDate') | ||
334 | replacements.originallyPublishedEndDate = options.originallyPublishedEndDate | ||
335 | } | ||
336 | |||
337 | if (options.durationMin) { | ||
338 | and.push('"video"."duration" >= :durationMin') | ||
339 | replacements.durationMin = options.durationMin | ||
340 | } | ||
341 | |||
342 | if (options.durationMax) { | ||
343 | and.push('"video"."duration" <= :durationMax') | ||
344 | replacements.durationMax = options.durationMax | ||
345 | } | ||
346 | |||
347 | if (options.search) { | ||
348 | const escapedSearch = model.sequelize.escape(options.search) | ||
349 | const escapedLikeSearch = model.sequelize.escape('%' + options.search + '%') | ||
350 | |||
351 | cte.push( | ||
352 | '"trigramSearch" AS (' + | ||
353 | ' SELECT "video"."id", ' + | ||
354 | ` similarity(lower(immutable_unaccent("video"."name")), lower(immutable_unaccent(${escapedSearch}))) as similarity ` + | ||
355 | ' FROM "video" ' + | ||
356 | ' WHERE lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + | ||
357 | ' lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + | ||
358 | ')' | ||
359 | ) | ||
360 | |||
361 | joins.push('LEFT JOIN "trigramSearch" ON "video"."id" = "trigramSearch"."id"') | ||
362 | |||
363 | let base = '(' + | ||
364 | ' "trigramSearch"."id" IS NOT NULL OR ' + | ||
365 | ' EXISTS (' + | ||
366 | ' SELECT 1 FROM "videoTag" ' + | ||
367 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
368 | ` WHERE lower("tag"."name") = ${escapedSearch} ` + | ||
369 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
370 | ' )' | ||
371 | |||
372 | if (validator.isUUID(options.search)) { | ||
373 | base += ` OR "video"."uuid" = ${escapedSearch}` | ||
374 | } | ||
375 | |||
376 | base += ')' | ||
377 | and.push(base) | ||
378 | |||
379 | attributes.push(`COALESCE("trigramSearch"."similarity", 0) as similarity`) | ||
380 | } else { | ||
381 | attributes.push('0 as similarity') | ||
382 | } | ||
383 | |||
384 | if (options.isCount === true) attributes = [ 'COUNT(*) as "total"' ] | ||
385 | |||
386 | let suffix = '' | ||
387 | let order = '' | ||
388 | if (options.isCount !== true) { | ||
389 | |||
390 | if (exists(options.sort)) { | ||
391 | if (options.sort === '-originallyPublishedAt' || options.sort === 'originallyPublishedAt') { | ||
392 | attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') | ||
393 | } | ||
394 | |||
395 | order = buildOrder(options.sort) | ||
396 | suffix += `${order} ` | ||
397 | } | ||
398 | |||
399 | if (exists(options.count)) { | ||
400 | const count = parseInt(options.count + '', 10) | ||
401 | suffix += `LIMIT ${count} ` | ||
402 | } | ||
403 | |||
404 | if (exists(options.start)) { | ||
405 | const start = parseInt(options.start + '', 10) | ||
406 | suffix += `OFFSET ${start} ` | ||
407 | } | ||
408 | } | ||
409 | |||
410 | const cteString = cte.length !== 0 | ||
411 | ? `WITH ${cte.join(', ')} ` | ||
412 | : '' | ||
413 | |||
414 | const query = cteString + | ||
415 | 'SELECT ' + attributes.join(', ') + ' ' + | ||
416 | 'FROM "video" ' + joins.join(' ') + ' ' + | ||
417 | 'WHERE ' + and.join(' AND ') + ' ' + | ||
418 | group + ' ' + | ||
419 | having + ' ' + | ||
420 | suffix | ||
421 | |||
422 | return { query, replacements, order } | ||
423 | } | ||
424 | |||
425 | function buildOrder (value: string) { | ||
426 | const { direction, field } = buildDirectionAndField(value) | ||
427 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | ||
428 | |||
429 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | ||
430 | |||
431 | if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation | ||
432 | return `ORDER BY "score" ${direction}, "video"."views" ${direction}` | ||
433 | } | ||
434 | |||
435 | let firstSort: string | ||
436 | |||
437 | if (field.toLowerCase() === 'match') { // Search | ||
438 | firstSort = '"similarity"' | ||
439 | } else if (field === 'originallyPublishedAt') { | ||
440 | firstSort = '"publishedAtForOrder"' | ||
441 | } else if (field.includes('.')) { | ||
442 | firstSort = field | ||
443 | } else { | ||
444 | firstSort = `"video"."${field}"` | ||
445 | } | ||
446 | |||
447 | return `ORDER BY ${firstSort} ${direction}, "video"."id" ASC` | ||
448 | } | ||
449 | |||
450 | function wrapForAPIResults (baseQuery: string, replacements: any, options: BuildVideosQueryOptions, order: string) { | ||
451 | const attributes = { | ||
452 | '"video".*': '', | ||
453 | '"VideoChannel"."id"': '"VideoChannel.id"', | ||
454 | '"VideoChannel"."name"': '"VideoChannel.name"', | ||
455 | '"VideoChannel"."description"': '"VideoChannel.description"', | ||
456 | '"VideoChannel"."actorId"': '"VideoChannel.actorId"', | ||
457 | '"VideoChannel->Actor"."id"': '"VideoChannel.Actor.id"', | ||
458 | '"VideoChannel->Actor"."preferredUsername"': '"VideoChannel.Actor.preferredUsername"', | ||
459 | '"VideoChannel->Actor"."url"': '"VideoChannel.Actor.url"', | ||
460 | '"VideoChannel->Actor"."serverId"': '"VideoChannel.Actor.serverId"', | ||
461 | '"VideoChannel->Actor"."avatarId"': '"VideoChannel.Actor.avatarId"', | ||
462 | '"VideoChannel->Account"."id"': '"VideoChannel.Account.id"', | ||
463 | '"VideoChannel->Account"."name"': '"VideoChannel.Account.name"', | ||
464 | '"VideoChannel->Account->Actor"."id"': '"VideoChannel.Account.Actor.id"', | ||
465 | '"VideoChannel->Account->Actor"."preferredUsername"': '"VideoChannel.Account.Actor.preferredUsername"', | ||
466 | '"VideoChannel->Account->Actor"."url"': '"VideoChannel.Account.Actor.url"', | ||
467 | '"VideoChannel->Account->Actor"."serverId"': '"VideoChannel.Account.Actor.serverId"', | ||
468 | '"VideoChannel->Account->Actor"."avatarId"': '"VideoChannel.Account.Actor.avatarId"', | ||
469 | '"VideoChannel->Actor->Server"."id"': '"VideoChannel.Actor.Server.id"', | ||
470 | '"VideoChannel->Actor->Server"."host"': '"VideoChannel.Actor.Server.host"', | ||
471 | '"VideoChannel->Actor->Avatar"."id"': '"VideoChannel.Actor.Avatar.id"', | ||
472 | '"VideoChannel->Actor->Avatar"."filename"': '"VideoChannel.Actor.Avatar.filename"', | ||
473 | '"VideoChannel->Actor->Avatar"."fileUrl"': '"VideoChannel.Actor.Avatar.fileUrl"', | ||
474 | '"VideoChannel->Actor->Avatar"."onDisk"': '"VideoChannel.Actor.Avatar.onDisk"', | ||
475 | '"VideoChannel->Actor->Avatar"."createdAt"': '"VideoChannel.Actor.Avatar.createdAt"', | ||
476 | '"VideoChannel->Actor->Avatar"."updatedAt"': '"VideoChannel.Actor.Avatar.updatedAt"', | ||
477 | '"VideoChannel->Account->Actor->Server"."id"': '"VideoChannel.Account.Actor.Server.id"', | ||
478 | '"VideoChannel->Account->Actor->Server"."host"': '"VideoChannel.Account.Actor.Server.host"', | ||
479 | '"VideoChannel->Account->Actor->Avatar"."id"': '"VideoChannel.Account.Actor.Avatar.id"', | ||
480 | '"VideoChannel->Account->Actor->Avatar"."filename"': '"VideoChannel.Account.Actor.Avatar.filename"', | ||
481 | '"VideoChannel->Account->Actor->Avatar"."fileUrl"': '"VideoChannel.Account.Actor.Avatar.fileUrl"', | ||
482 | '"VideoChannel->Account->Actor->Avatar"."onDisk"': '"VideoChannel.Account.Actor.Avatar.onDisk"', | ||
483 | '"VideoChannel->Account->Actor->Avatar"."createdAt"': '"VideoChannel.Account.Actor.Avatar.createdAt"', | ||
484 | '"VideoChannel->Account->Actor->Avatar"."updatedAt"': '"VideoChannel.Account.Actor.Avatar.updatedAt"', | ||
485 | '"Thumbnails"."id"': '"Thumbnails.id"', | ||
486 | '"Thumbnails"."type"': '"Thumbnails.type"', | ||
487 | '"Thumbnails"."filename"': '"Thumbnails.filename"' | ||
488 | } | ||
489 | |||
490 | const joins = [ | ||
491 | 'INNER JOIN "video" ON "tmp"."id" = "video"."id"', | ||
492 | |||
493 | 'INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"', | ||
494 | 'INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"', | ||
495 | 'INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"', | ||
496 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', | ||
497 | |||
498 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', | ||
499 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + | ||
500 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | ||
501 | |||
502 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + | ||
503 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', | ||
504 | |||
505 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + | ||
506 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', | ||
507 | |||
508 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' | ||
509 | ] | ||
510 | |||
511 | if (options.withFiles) { | ||
512 | joins.push('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') | ||
513 | |||
514 | joins.push('LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"') | ||
515 | joins.push( | ||
516 | 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' + | ||
517 | 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"' | ||
518 | ) | ||
519 | |||
520 | Object.assign(attributes, { | ||
521 | '"VideoFiles"."id"': '"VideoFiles.id"', | ||
522 | '"VideoFiles"."createdAt"': '"VideoFiles.createdAt"', | ||
523 | '"VideoFiles"."updatedAt"': '"VideoFiles.updatedAt"', | ||
524 | '"VideoFiles"."resolution"': '"VideoFiles.resolution"', | ||
525 | '"VideoFiles"."size"': '"VideoFiles.size"', | ||
526 | '"VideoFiles"."extname"': '"VideoFiles.extname"', | ||
527 | '"VideoFiles"."filename"': '"VideoFiles.filename"', | ||
528 | '"VideoFiles"."fileUrl"': '"VideoFiles.fileUrl"', | ||
529 | '"VideoFiles"."torrentFilename"': '"VideoFiles.torrentFilename"', | ||
530 | '"VideoFiles"."torrentUrl"': '"VideoFiles.torrentUrl"', | ||
531 | '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"', | ||
532 | '"VideoFiles"."fps"': '"VideoFiles.fps"', | ||
533 | '"VideoFiles"."videoId"': '"VideoFiles.videoId"', | ||
534 | |||
535 | '"VideoStreamingPlaylists"."id"': '"VideoStreamingPlaylists.id"', | ||
536 | '"VideoStreamingPlaylists"."playlistUrl"': '"VideoStreamingPlaylists.playlistUrl"', | ||
537 | '"VideoStreamingPlaylists"."type"': '"VideoStreamingPlaylists.type"', | ||
538 | '"VideoStreamingPlaylists->VideoFiles"."id"': '"VideoStreamingPlaylists.VideoFiles.id"', | ||
539 | '"VideoStreamingPlaylists->VideoFiles"."createdAt"': '"VideoStreamingPlaylists.VideoFiles.createdAt"', | ||
540 | '"VideoStreamingPlaylists->VideoFiles"."updatedAt"': '"VideoStreamingPlaylists.VideoFiles.updatedAt"', | ||
541 | '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"', | ||
542 | '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"', | ||
543 | '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"', | ||
544 | '"VideoStreamingPlaylists->VideoFiles"."filename"': '"VideoStreamingPlaylists.VideoFiles.filename"', | ||
545 | '"VideoStreamingPlaylists->VideoFiles"."fileUrl"': '"VideoStreamingPlaylists.VideoFiles.fileUrl"', | ||
546 | '"VideoStreamingPlaylists->VideoFiles"."torrentFilename"': '"VideoStreamingPlaylists.VideoFiles.torrentFilename"', | ||
547 | '"VideoStreamingPlaylists->VideoFiles"."torrentUrl"': '"VideoStreamingPlaylists.VideoFiles.torrentUrl"', | ||
548 | '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"', | ||
549 | '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"', | ||
550 | '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"', | ||
551 | '"VideoStreamingPlaylists->VideoFiles"."videoId"': '"VideoStreamingPlaylists.VideoFiles.videoId"' | ||
552 | }) | ||
553 | } | ||
554 | |||
555 | if (options.user) { | ||
556 | joins.push( | ||
557 | 'LEFT OUTER JOIN "userVideoHistory" ' + | ||
558 | 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId' | ||
559 | ) | ||
560 | replacements.userVideoHistoryId = options.user.id | ||
561 | |||
562 | Object.assign(attributes, { | ||
563 | '"userVideoHistory"."id"': '"userVideoHistory.id"', | ||
564 | '"userVideoHistory"."currentTime"': '"userVideoHistory.currentTime"' | ||
565 | }) | ||
566 | } | ||
567 | |||
568 | if (options.videoPlaylistId) { | ||
569 | joins.push( | ||
570 | 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' + | ||
571 | 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
572 | ) | ||
573 | replacements.videoPlaylistId = options.videoPlaylistId | ||
574 | |||
575 | Object.assign(attributes, { | ||
576 | '"VideoPlaylistElement"."createdAt"': '"VideoPlaylistElement.createdAt"', | ||
577 | '"VideoPlaylistElement"."updatedAt"': '"VideoPlaylistElement.updatedAt"', | ||
578 | '"VideoPlaylistElement"."url"': '"VideoPlaylistElement.url"', | ||
579 | '"VideoPlaylistElement"."position"': '"VideoPlaylistElement.position"', | ||
580 | '"VideoPlaylistElement"."startTimestamp"': '"VideoPlaylistElement.startTimestamp"', | ||
581 | '"VideoPlaylistElement"."stopTimestamp"': '"VideoPlaylistElement.stopTimestamp"', | ||
582 | '"VideoPlaylistElement"."videoPlaylistId"': '"VideoPlaylistElement.videoPlaylistId"' | ||
583 | }) | ||
584 | } | ||
585 | |||
586 | const select = 'SELECT ' + Object.keys(attributes).map(key => { | ||
587 | const value = attributes[key] | ||
588 | if (value) return `${key} AS ${value}` | ||
589 | |||
590 | return key | ||
591 | }).join(', ') | ||
592 | |||
593 | return `${select} FROM (${baseQuery}) AS "tmp" ${joins.join(' ')} ${order}` | ||
594 | } | ||
595 | |||
596 | export { | ||
597 | buildListQuery, | ||
598 | wrapForAPIResults | ||
599 | } | ||
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index 5059c1fa6..505c305e2 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import { literal, Op, QueryTypes, Transaction } from 'sequelize' | 1 | import { literal, Op, QueryTypes, Transaction } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 4 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 5 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
5 | import { MActorDefault } from '../../types/models' | 6 | import { MActorDefault } from '../../types/models' |
6 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' | 7 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' |
7 | import { ActorModel } from '../activitypub/actor' | 8 | import { ActorModel } from '../actor/actor' |
8 | import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' | 9 | import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' |
9 | import { VideoModel } from './video' | 10 | import { VideoModel } from './video' |
10 | 11 | ||
@@ -50,7 +51,7 @@ enum ScopeNames { | |||
50 | } | 51 | } |
51 | ] | 52 | ] |
52 | }) | 53 | }) |
53 | export class VideoShareModel extends Model { | 54 | export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareModel>>> { |
54 | 55 | ||
55 | @AllowNull(false) | 56 | @AllowNull(false) |
56 | @Is('VideoShareUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | 57 | @Is('VideoShareUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index c9375b433..d627e8c9d 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -13,6 +13,7 @@ import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_ | |||
13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
14 | import { throwIfNotValid } from '../utils' | 14 | import { throwIfNotValid } from '../utils' |
15 | import { VideoModel } from './video' | 15 | import { VideoModel } from './video' |
16 | import { AttributesOnly } from '@shared/core-utils' | ||
16 | 17 | ||
17 | @Table({ | 18 | @Table({ |
18 | tableName: 'videoStreamingPlaylist', | 19 | tableName: 'videoStreamingPlaylist', |
@@ -30,7 +31,7 @@ import { VideoModel } from './video' | |||
30 | } | 31 | } |
31 | ] | 32 | ] |
32 | }) | 33 | }) |
33 | export class VideoStreamingPlaylistModel extends Model { | 34 | export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<VideoStreamingPlaylistModel>>> { |
34 | @CreatedAt | 35 | @CreatedAt |
35 | createdAt: Date | 36 | createdAt: Date |
36 | 37 | ||
diff --git a/server/models/video/video-tag.ts b/server/models/video/video-tag.ts index 5052b8c4d..1285d375b 100644 --- a/server/models/video/video-tag.ts +++ b/server/models/video/video-tag.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { TagModel } from './tag' | 3 | import { TagModel } from './tag' |
3 | import { VideoModel } from './video' | 4 | import { VideoModel } from './video' |
4 | 5 | ||
@@ -13,7 +14,7 @@ import { VideoModel } from './video' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class VideoTagModel extends Model { | 17 | export class VideoTagModel extends Model<Partial<AttributesOnly<VideoTagModel>>> { |
17 | @CreatedAt | 18 | @CreatedAt |
18 | createdAt: Date | 19 | createdAt: Date |
19 | 20 | ||
diff --git a/server/models/video/video-view.ts b/server/models/video/video-view.ts index 992cf258a..dfc6296ce 100644 --- a/server/models/video/video-view.ts +++ b/server/models/video/video-view.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table } from 'sequelize-typescript' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoModel } from './video' | 4 | import { VideoModel } from './video' |
3 | import * as Sequelize from 'sequelize' | ||
4 | 5 | ||
5 | @Table({ | 6 | @Table({ |
6 | tableName: 'videoView', | 7 | tableName: 'videoView', |
@@ -14,7 +15,7 @@ import * as Sequelize from 'sequelize' | |||
14 | } | 15 | } |
15 | ] | 16 | ] |
16 | }) | 17 | }) |
17 | export class VideoViewModel extends Model { | 18 | export class VideoViewModel extends Model<Partial<AttributesOnly<VideoViewModel>>> { |
18 | @CreatedAt | 19 | @CreatedAt |
19 | createdAt: Date | 20 | createdAt: Date |
20 | 21 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 18afba1ba..1e5648a36 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { remove } from 'fs-extra' | 2 | import { remove } from 'fs-extra' |
3 | import { maxBy, minBy, pick } from 'lodash' | 3 | import { maxBy, minBy } from 'lodash' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 5 | import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
6 | import { | 6 | import { |
@@ -27,10 +27,11 @@ import { | |||
27 | import { setAsUpdated } from '@server/helpers/database-utils' | 27 | import { setAsUpdated } from '@server/helpers/database-utils' |
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 28 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { LiveManager } from '@server/lib/live-manager' | 30 | import { LiveManager } from '@server/lib/live/live-manager' |
31 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' | 31 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' |
32 | import { getServerActor } from '@server/models/application/application' | 32 | import { getServerActor } from '@server/models/application/application' |
33 | import { ModelCache } from '@server/models/model-cache' | 33 | import { ModelCache } from '@server/models/model-cache' |
34 | import { AttributesOnly } from '@shared/core-utils' | ||
34 | import { VideoFile } from '@shared/models/videos/video-file.model' | 35 | import { VideoFile } from '@shared/models/videos/video-file.model' |
35 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' | 36 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' |
36 | import { VideoObject } from '../../../shared/models/activitypub/objects' | 37 | import { VideoObject } from '../../../shared/models/activitypub/objects' |
@@ -42,11 +43,8 @@ import { peertubeTruncate } from '../../helpers/core-utils' | |||
42 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 43 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
43 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 44 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
44 | import { | 45 | import { |
45 | isVideoCategoryValid, | ||
46 | isVideoDescriptionValid, | 46 | isVideoDescriptionValid, |
47 | isVideoDurationValid, | 47 | isVideoDurationValid, |
48 | isVideoLanguageValid, | ||
49 | isVideoLicenceValid, | ||
50 | isVideoNameValid, | 48 | isVideoNameValid, |
51 | isVideoPrivacyValid, | 49 | isVideoPrivacyValid, |
52 | isVideoStateValid, | 50 | isVideoStateValid, |
@@ -55,19 +53,7 @@ import { | |||
55 | import { getVideoFileResolution } from '../../helpers/ffprobe-utils' | 53 | import { getVideoFileResolution } from '../../helpers/ffprobe-utils' |
56 | import { logger } from '../../helpers/logger' | 54 | import { logger } from '../../helpers/logger' |
57 | import { CONFIG } from '../../initializers/config' | 55 | import { CONFIG } from '../../initializers/config' |
58 | import { | 56 | import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' |
59 | ACTIVITY_PUB, | ||
60 | API_VERSION, | ||
61 | CONSTRAINTS_FIELDS, | ||
62 | LAZY_STATIC_PATHS, | ||
63 | STATIC_PATHS, | ||
64 | VIDEO_CATEGORIES, | ||
65 | VIDEO_LANGUAGES, | ||
66 | VIDEO_LICENCES, | ||
67 | VIDEO_PRIVACIES, | ||
68 | VIDEO_STATES, | ||
69 | WEBSERVER | ||
70 | } from '../../initializers/constants' | ||
71 | import { sendDeleteVideo } from '../../lib/activitypub/send' | 57 | import { sendDeleteVideo } from '../../lib/activitypub/send' |
72 | import { | 58 | import { |
73 | MChannel, | 59 | MChannel, |
@@ -87,29 +73,38 @@ import { | |||
87 | MVideoFormattableDetails, | 73 | MVideoFormattableDetails, |
88 | MVideoForUser, | 74 | MVideoForUser, |
89 | MVideoFullLight, | 75 | MVideoFullLight, |
90 | MVideoIdThumbnail, | 76 | MVideoId, |
91 | MVideoImmutable, | 77 | MVideoImmutable, |
92 | MVideoThumbnail, | 78 | MVideoThumbnail, |
93 | MVideoThumbnailBlacklist, | 79 | MVideoThumbnailBlacklist, |
94 | MVideoWithAllFiles, | 80 | MVideoWithAllFiles, |
95 | MVideoWithFile, | 81 | MVideoWithFile |
96 | MVideoWithRights | ||
97 | } from '../../types/models' | 82 | } from '../../types/models' |
98 | import { MThumbnail } from '../../types/models/video/thumbnail' | 83 | import { MThumbnail } from '../../types/models/video/thumbnail' |
99 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' | 84 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' |
100 | import { VideoAbuseModel } from '../abuse/video-abuse' | 85 | import { VideoAbuseModel } from '../abuse/video-abuse' |
101 | import { AccountModel } from '../account/account' | 86 | import { AccountModel } from '../account/account' |
102 | import { AccountVideoRateModel } from '../account/account-video-rate' | 87 | import { AccountVideoRateModel } from '../account/account-video-rate' |
103 | import { ActorImageModel } from '../account/actor-image' | 88 | import { ActorModel } from '../actor/actor' |
104 | import { UserModel } from '../account/user' | 89 | import { ActorImageModel } from '../actor/actor-image' |
105 | import { UserVideoHistoryModel } from '../account/user-video-history' | ||
106 | import { ActorModel } from '../activitypub/actor' | ||
107 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 90 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
108 | import { ServerModel } from '../server/server' | 91 | import { ServerModel } from '../server/server' |
109 | import { TrackerModel } from '../server/tracker' | 92 | import { TrackerModel } from '../server/tracker' |
110 | import { VideoTrackerModel } from '../server/video-tracker' | 93 | import { VideoTrackerModel } from '../server/video-tracker' |
94 | import { UserModel } from '../user/user' | ||
95 | import { UserVideoHistoryModel } from '../user/user-video-history' | ||
111 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | 96 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' |
97 | import { | ||
98 | videoFilesModelToFormattedJSON, | ||
99 | VideoFormattingJSONOptions, | ||
100 | videoModelToActivityPubObject, | ||
101 | videoModelToFormattedDetailsJSON, | ||
102 | videoModelToFormattedJSON | ||
103 | } from './formatter/video-format-utils' | ||
112 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 104 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
105 | import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder' | ||
106 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' | ||
107 | import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' | ||
113 | import { TagModel } from './tag' | 108 | import { TagModel } from './tag' |
114 | import { ThumbnailModel } from './thumbnail' | 109 | import { ThumbnailModel } from './thumbnail' |
115 | import { VideoBlacklistModel } from './video-blacklist' | 110 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -117,37 +112,25 @@ import { VideoCaptionModel } from './video-caption' | |||
117 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | 112 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
118 | import { VideoCommentModel } from './video-comment' | 113 | import { VideoCommentModel } from './video-comment' |
119 | import { VideoFileModel } from './video-file' | 114 | import { VideoFileModel } from './video-file' |
120 | import { | ||
121 | videoFilesModelToFormattedJSON, | ||
122 | VideoFormattingJSONOptions, | ||
123 | videoModelToActivityPubObject, | ||
124 | videoModelToFormattedDetailsJSON, | ||
125 | videoModelToFormattedJSON | ||
126 | } from './video-format-utils' | ||
127 | import { VideoImportModel } from './video-import' | 115 | import { VideoImportModel } from './video-import' |
128 | import { VideoLiveModel } from './video-live' | 116 | import { VideoLiveModel } from './video-live' |
129 | import { VideoPlaylistElementModel } from './video-playlist-element' | 117 | import { VideoPlaylistElementModel } from './video-playlist-element' |
130 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' | ||
131 | import { VideoShareModel } from './video-share' | 118 | import { VideoShareModel } from './video-share' |
132 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 119 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
133 | import { VideoTagModel } from './video-tag' | 120 | import { VideoTagModel } from './video-tag' |
134 | import { VideoViewModel } from './video-view' | 121 | import { VideoViewModel } from './video-view' |
135 | 122 | ||
136 | export enum ScopeNames { | 123 | export enum ScopeNames { |
137 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | ||
138 | FOR_API = 'FOR_API', | 124 | FOR_API = 'FOR_API', |
139 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', | 125 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', |
140 | WITH_TAGS = 'WITH_TAGS', | 126 | WITH_TAGS = 'WITH_TAGS', |
141 | WITH_TRACKERS = 'WITH_TRACKERS', | ||
142 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', | 127 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', |
143 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 128 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
144 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 129 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
145 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', | ||
146 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 130 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
147 | WITH_USER_ID = 'WITH_USER_ID', | ||
148 | WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES', | 131 | WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES', |
149 | WITH_THUMBNAILS = 'WITH_THUMBNAILS', | 132 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
150 | WITH_LIVE = 'WITH_LIVE' | 133 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' |
151 | } | 134 | } |
152 | 135 | ||
153 | export type ForAPIOptions = { | 136 | export type ForAPIOptions = { |
@@ -243,30 +226,6 @@ export type AvailableForListIDsOptions = { | |||
243 | } | 226 | } |
244 | ] | 227 | ] |
245 | }, | 228 | }, |
246 | [ScopeNames.WITH_LIVE]: { | ||
247 | include: [ | ||
248 | { | ||
249 | model: VideoLiveModel.unscoped(), | ||
250 | required: false | ||
251 | } | ||
252 | ] | ||
253 | }, | ||
254 | [ScopeNames.WITH_USER_ID]: { | ||
255 | include: [ | ||
256 | { | ||
257 | attributes: [ 'accountId' ], | ||
258 | model: VideoChannelModel.unscoped(), | ||
259 | required: true, | ||
260 | include: [ | ||
261 | { | ||
262 | attributes: [ 'userId' ], | ||
263 | model: AccountModel.unscoped(), | ||
264 | required: true | ||
265 | } | ||
266 | ] | ||
267 | } | ||
268 | ] | ||
269 | }, | ||
270 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { | 229 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { |
271 | include: [ | 230 | include: [ |
272 | { | 231 | { |
@@ -324,14 +283,6 @@ export type AvailableForListIDsOptions = { | |||
324 | [ScopeNames.WITH_TAGS]: { | 283 | [ScopeNames.WITH_TAGS]: { |
325 | include: [ TagModel ] | 284 | include: [ TagModel ] |
326 | }, | 285 | }, |
327 | [ScopeNames.WITH_TRACKERS]: { | ||
328 | include: [ | ||
329 | { | ||
330 | attributes: [ 'id', 'url' ], | ||
331 | model: TrackerModel | ||
332 | } | ||
333 | ] | ||
334 | }, | ||
335 | [ScopeNames.WITH_BLACKLISTED]: { | 286 | [ScopeNames.WITH_BLACKLISTED]: { |
336 | include: [ | 287 | include: [ |
337 | { | 288 | { |
@@ -489,7 +440,7 @@ export type AvailableForListIDsOptions = { | |||
489 | } | 440 | } |
490 | ] | 441 | ] |
491 | }) | 442 | }) |
492 | export class VideoModel extends Model { | 443 | export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { |
493 | 444 | ||
494 | @AllowNull(false) | 445 | @AllowNull(false) |
495 | @Default(DataType.UUIDV4) | 446 | @Default(DataType.UUIDV4) |
@@ -504,19 +455,16 @@ export class VideoModel extends Model { | |||
504 | 455 | ||
505 | @AllowNull(true) | 456 | @AllowNull(true) |
506 | @Default(null) | 457 | @Default(null) |
507 | @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category', true)) | ||
508 | @Column | 458 | @Column |
509 | category: number | 459 | category: number |
510 | 460 | ||
511 | @AllowNull(true) | 461 | @AllowNull(true) |
512 | @Default(null) | 462 | @Default(null) |
513 | @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence', true)) | ||
514 | @Column | 463 | @Column |
515 | licence: number | 464 | licence: number |
516 | 465 | ||
517 | @AllowNull(true) | 466 | @AllowNull(true) |
518 | @Default(null) | 467 | @Default(null) |
519 | @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language', true)) | ||
520 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max)) | 468 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max)) |
521 | language: string | 469 | language: string |
522 | 470 | ||
@@ -624,7 +572,7 @@ export class VideoModel extends Model { | |||
624 | foreignKey: { | 572 | foreignKey: { |
625 | allowNull: true | 573 | allowNull: true |
626 | }, | 574 | }, |
627 | hooks: true | 575 | onDelete: 'cascade' |
628 | }) | 576 | }) |
629 | VideoChannel: VideoChannelModel | 577 | VideoChannel: VideoChannelModel |
630 | 578 | ||
@@ -802,14 +750,14 @@ export class VideoModel extends Model { | |||
802 | } | 750 | } |
803 | 751 | ||
804 | @BeforeDestroy | 752 | @BeforeDestroy |
805 | static async removeFiles (instance: VideoModel) { | 753 | static async removeFiles (instance: VideoModel, options) { |
806 | const tasks: Promise<any>[] = [] | 754 | const tasks: Promise<any>[] = [] |
807 | 755 | ||
808 | logger.info('Removing files of video %s.', instance.url) | 756 | logger.info('Removing files of video %s.', instance.url) |
809 | 757 | ||
810 | if (instance.isOwned()) { | 758 | if (instance.isOwned()) { |
811 | if (!Array.isArray(instance.VideoFiles)) { | 759 | if (!Array.isArray(instance.VideoFiles)) { |
812 | instance.VideoFiles = await instance.$get('VideoFiles') | 760 | instance.VideoFiles = await instance.$get('VideoFiles', { transaction: options.transaction }) |
813 | } | 761 | } |
814 | 762 | ||
815 | // Remove physical files and torrents | 763 | // Remove physical files and torrents |
@@ -820,7 +768,7 @@ export class VideoModel extends Model { | |||
820 | 768 | ||
821 | // Remove playlists file | 769 | // Remove playlists file |
822 | if (!Array.isArray(instance.VideoStreamingPlaylists)) { | 770 | if (!Array.isArray(instance.VideoStreamingPlaylists)) { |
823 | instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists') | 771 | instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists', { transaction: options.transaction }) |
824 | } | 772 | } |
825 | 773 | ||
826 | for (const p of instance.VideoStreamingPlaylists) { | 774 | for (const p of instance.VideoStreamingPlaylists) { |
@@ -843,7 +791,7 @@ export class VideoModel extends Model { | |||
843 | 791 | ||
844 | logger.info('Stopping live of video %s after video deletion.', instance.uuid) | 792 | logger.info('Stopping live of video %s after video deletion.', instance.uuid) |
845 | 793 | ||
846 | return LiveManager.Instance.stopSessionOf(instance.id) | 794 | LiveManager.Instance.stopSessionOf(instance.id) |
847 | } | 795 | } |
848 | 796 | ||
849 | @BeforeDestroy | 797 | @BeforeDestroy |
@@ -856,7 +804,7 @@ export class VideoModel extends Model { | |||
856 | const tasks: Promise<any>[] = [] | 804 | const tasks: Promise<any>[] = [] |
857 | 805 | ||
858 | if (!Array.isArray(instance.VideoAbuses)) { | 806 | if (!Array.isArray(instance.VideoAbuses)) { |
859 | instance.VideoAbuses = await instance.$get('VideoAbuses') | 807 | instance.VideoAbuses = await instance.$get('VideoAbuses', { transaction: options.transaction }) |
860 | 808 | ||
861 | if (instance.VideoAbuses.length === 0) return undefined | 809 | if (instance.VideoAbuses.length === 0) return undefined |
862 | } | 810 | } |
@@ -871,12 +819,7 @@ export class VideoModel extends Model { | |||
871 | tasks.push(abuse.save({ transaction: options.transaction })) | 819 | tasks.push(abuse.save({ transaction: options.transaction })) |
872 | } | 820 | } |
873 | 821 | ||
874 | Promise.all(tasks) | 822 | await Promise.all(tasks) |
875 | .catch(err => { | ||
876 | logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err }) | ||
877 | }) | ||
878 | |||
879 | return undefined | ||
880 | } | 823 | } |
881 | 824 | ||
882 | static listLocal (): Promise<MVideo[]> { | 825 | static listLocal (): Promise<MVideo[]> { |
@@ -1003,9 +946,9 @@ export class VideoModel extends Model { | |||
1003 | }) | 946 | }) |
1004 | } | 947 | } |
1005 | 948 | ||
1006 | static async listPublishedLiveIds () { | 949 | static async listPublishedLiveUUIDs () { |
1007 | const options = { | 950 | const options = { |
1008 | attributes: [ 'id' ], | 951 | attributes: [ 'uuid' ], |
1009 | where: { | 952 | where: { |
1010 | isLive: true, | 953 | isLive: true, |
1011 | remote: false, | 954 | remote: false, |
@@ -1015,7 +958,7 @@ export class VideoModel extends Model { | |||
1015 | 958 | ||
1016 | const result = await VideoModel.findAll(options) | 959 | const result = await VideoModel.findAll(options) |
1017 | 960 | ||
1018 | return result.map(v => v.id) | 961 | return result.map(v => v.uuid) |
1019 | } | 962 | } |
1020 | 963 | ||
1021 | static listUserVideosForApi (options: { | 964 | static listUserVideosForApi (options: { |
@@ -1298,27 +1241,16 @@ export class VideoModel extends Model { | |||
1298 | return VideoModel.count(options) | 1241 | return VideoModel.count(options) |
1299 | } | 1242 | } |
1300 | 1243 | ||
1301 | static load (id: number | string, t?: Transaction): Promise<MVideoThumbnail> { | 1244 | static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> { |
1302 | const where = buildWhereIdOrUUID(id) | 1245 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1303 | const options = { | ||
1304 | where, | ||
1305 | transaction: t | ||
1306 | } | ||
1307 | 1246 | ||
1308 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) | 1247 | return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' }) |
1309 | } | 1248 | } |
1310 | 1249 | ||
1311 | static loadWithBlacklist (id: number | string, t?: Transaction): Promise<MVideoThumbnailBlacklist> { | 1250 | static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> { |
1312 | const where = buildWhereIdOrUUID(id) | 1251 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1313 | const options = { | ||
1314 | where, | ||
1315 | transaction: t | ||
1316 | } | ||
1317 | 1252 | ||
1318 | return VideoModel.scope([ | 1253 | return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' }) |
1319 | ScopeNames.WITH_THUMBNAILS, | ||
1320 | ScopeNames.WITH_BLACKLISTED | ||
1321 | ]).findOne(options) | ||
1322 | } | 1254 | } |
1323 | 1255 | ||
1324 | static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> { | 1256 | static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> { |
@@ -1339,68 +1271,6 @@ export class VideoModel extends Model { | |||
1339 | }) | 1271 | }) |
1340 | } | 1272 | } |
1341 | 1273 | ||
1342 | static loadWithRights (id: number | string, t?: Transaction): Promise<MVideoWithRights> { | ||
1343 | const where = buildWhereIdOrUUID(id) | ||
1344 | const options = { | ||
1345 | where, | ||
1346 | transaction: t | ||
1347 | } | ||
1348 | |||
1349 | return VideoModel.scope([ | ||
1350 | ScopeNames.WITH_BLACKLISTED, | ||
1351 | ScopeNames.WITH_USER_ID | ||
1352 | ]).findOne(options) | ||
1353 | } | ||
1354 | |||
1355 | static loadOnlyId (id: number | string, t?: Transaction): Promise<MVideoIdThumbnail> { | ||
1356 | const where = buildWhereIdOrUUID(id) | ||
1357 | |||
1358 | const options = { | ||
1359 | attributes: [ 'id' ], | ||
1360 | where, | ||
1361 | transaction: t | ||
1362 | } | ||
1363 | |||
1364 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) | ||
1365 | } | ||
1366 | |||
1367 | static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> { | ||
1368 | const where = buildWhereIdOrUUID(id) | ||
1369 | |||
1370 | const query = { | ||
1371 | where, | ||
1372 | transaction: t, | ||
1373 | logging | ||
1374 | } | ||
1375 | |||
1376 | return VideoModel.scope([ | ||
1377 | ScopeNames.WITH_WEBTORRENT_FILES, | ||
1378 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1379 | ScopeNames.WITH_THUMBNAILS | ||
1380 | ]).findOne(query) | ||
1381 | } | ||
1382 | |||
1383 | static loadByUUID (uuid: string): Promise<MVideoThumbnail> { | ||
1384 | const options = { | ||
1385 | where: { | ||
1386 | uuid | ||
1387 | } | ||
1388 | } | ||
1389 | |||
1390 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) | ||
1391 | } | ||
1392 | |||
1393 | static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> { | ||
1394 | const query: FindOptions = { | ||
1395 | where: { | ||
1396 | url | ||
1397 | }, | ||
1398 | transaction | ||
1399 | } | ||
1400 | |||
1401 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) | ||
1402 | } | ||
1403 | |||
1404 | static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> { | 1274 | static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> { |
1405 | const fun = () => { | 1275 | const fun = () => { |
1406 | const query: FindOptions = { | 1276 | const query: FindOptions = { |
@@ -1421,85 +1291,45 @@ export class VideoModel extends Model { | |||
1421 | }) | 1291 | }) |
1422 | } | 1292 | } |
1423 | 1293 | ||
1424 | static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> { | 1294 | static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> { |
1425 | const query: FindOptions = { | 1295 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1426 | where: { | ||
1427 | url | ||
1428 | }, | ||
1429 | transaction | ||
1430 | } | ||
1431 | 1296 | ||
1432 | return VideoModel.scope([ | 1297 | return queryBuilder.queryVideo({ id, transaction, type: 'id' }) |
1433 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1434 | ScopeNames.WITH_WEBTORRENT_FILES, | ||
1435 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1436 | ScopeNames.WITH_THUMBNAILS, | ||
1437 | ScopeNames.WITH_BLACKLISTED | ||
1438 | ]).findOne(query) | ||
1439 | } | 1298 | } |
1440 | 1299 | ||
1441 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> { | 1300 | static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> { |
1442 | const where = buildWhereIdOrUUID(id) | 1301 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1443 | 1302 | ||
1444 | const options = { | 1303 | return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging }) |
1445 | order: [ [ 'Tags', 'name', 'ASC' ] ] as any, | 1304 | } |
1446 | where, | ||
1447 | transaction: t | ||
1448 | } | ||
1449 | 1305 | ||
1450 | const scopes: (string | ScopeOptions)[] = [ | 1306 | static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> { |
1451 | ScopeNames.WITH_TAGS, | 1307 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1452 | ScopeNames.WITH_BLACKLISTED, | ||
1453 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1454 | ScopeNames.WITH_SCHEDULED_UPDATE, | ||
1455 | ScopeNames.WITH_WEBTORRENT_FILES, | ||
1456 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1457 | ScopeNames.WITH_THUMBNAILS, | ||
1458 | ScopeNames.WITH_LIVE | ||
1459 | ] | ||
1460 | 1308 | ||
1461 | if (userId) { | 1309 | return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' }) |
1462 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] }) | 1310 | } |
1463 | } | 1311 | |
1312 | static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> { | ||
1313 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) | ||
1314 | |||
1315 | return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' }) | ||
1316 | } | ||
1464 | 1317 | ||
1465 | return VideoModel | 1318 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> { |
1466 | .scope(scopes) | 1319 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1467 | .findOne(options) | 1320 | |
1321 | return queryBuilder.queryVideo({ id, transaction: t, type: 'full-light', userId }) | ||
1468 | } | 1322 | } |
1469 | 1323 | ||
1470 | static loadForGetAPI (parameters: { | 1324 | static loadForGetAPI (parameters: { |
1471 | id: number | string | 1325 | id: number | string |
1472 | t?: Transaction | 1326 | transaction?: Transaction |
1473 | userId?: number | 1327 | userId?: number |
1474 | }): Promise<MVideoDetails> { | 1328 | }): Promise<MVideoDetails> { |
1475 | const { id, t, userId } = parameters | 1329 | const { id, transaction, userId } = parameters |
1476 | const where = buildWhereIdOrUUID(id) | 1330 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1477 | 1331 | ||
1478 | const options = { | 1332 | return queryBuilder.queryVideo({ id, transaction, type: 'api', userId }) |
1479 | order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings | ||
1480 | where, | ||
1481 | transaction: t | ||
1482 | } | ||
1483 | |||
1484 | const scopes: (string | ScopeOptions)[] = [ | ||
1485 | ScopeNames.WITH_TAGS, | ||
1486 | ScopeNames.WITH_BLACKLISTED, | ||
1487 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1488 | ScopeNames.WITH_SCHEDULED_UPDATE, | ||
1489 | ScopeNames.WITH_THUMBNAILS, | ||
1490 | ScopeNames.WITH_LIVE, | ||
1491 | ScopeNames.WITH_TRACKERS, | ||
1492 | { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] }, | ||
1493 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } | ||
1494 | ] | ||
1495 | |||
1496 | if (userId) { | ||
1497 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] }) | ||
1498 | } | ||
1499 | |||
1500 | return VideoModel | ||
1501 | .scope(scopes) | ||
1502 | .findOne(options) | ||
1503 | } | 1333 | } |
1504 | 1334 | ||
1505 | static async getStats () { | 1335 | static async getStats () { |
@@ -1550,7 +1380,7 @@ export class VideoModel extends Model { | |||
1550 | 1380 | ||
1551 | const rawQuery = `UPDATE "video" SET "${field}" = ` + | 1381 | const rawQuery = `UPDATE "video" SET "${field}" = ` + |
1552 | '(' + | 1382 | '(' + |
1553 | 'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' + | 1383 | 'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' + |
1554 | ') ' + | 1384 | ') ' + |
1555 | 'WHERE "video"."id" = :videoId' | 1385 | 'WHERE "video"."id" = :videoId' |
1556 | 1386 | ||
@@ -1578,15 +1408,15 @@ export class VideoModel extends Model { | |||
1578 | .then(results => results.length === 1) | 1408 | .then(results => results.length === 1) |
1579 | } | 1409 | } |
1580 | 1410 | ||
1581 | static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) { | 1411 | static bulkUpdateSupportField (ofChannel: MChannel, t: Transaction) { |
1582 | const options = { | 1412 | const options = { |
1583 | where: { | 1413 | where: { |
1584 | channelId: videoChannel.id | 1414 | channelId: ofChannel.id |
1585 | }, | 1415 | }, |
1586 | transaction: t | 1416 | transaction: t |
1587 | } | 1417 | } |
1588 | 1418 | ||
1589 | return VideoModel.update({ support: videoChannel.support }, options) | 1419 | return VideoModel.update({ support: ofChannel.support }, options) |
1590 | } | 1420 | } |
1591 | 1421 | ||
1592 | static getAllIdsFromChannel (videoChannel: MChannelId): Promise<number[]> { | 1422 | static getAllIdsFromChannel (videoChannel: MChannelId): Promise<number[]> { |
@@ -1606,7 +1436,7 @@ export class VideoModel extends Model { | |||
1606 | const serverActor = await getServerActor() | 1436 | const serverActor = await getServerActor() |
1607 | const followerActorId = serverActor.id | 1437 | const followerActorId = serverActor.id |
1608 | 1438 | ||
1609 | const queryOptions: BuildVideosQueryOptions = { | 1439 | const queryOptions: BuildVideosListQueryOptions = { |
1610 | attributes: [ `"${field}"` ], | 1440 | attributes: [ `"${field}"` ], |
1611 | group: `GROUP BY "${field}"`, | 1441 | group: `GROUP BY "${field}"`, |
1612 | having: `HAVING COUNT("${field}") >= ${threshold}`, | 1442 | having: `HAVING COUNT("${field}") >= ${threshold}`, |
@@ -1618,10 +1448,10 @@ export class VideoModel extends Model { | |||
1618 | includeLocalVideos: true | 1448 | includeLocalVideos: true |
1619 | } | 1449 | } |
1620 | 1450 | ||
1621 | const { query, replacements } = buildListQuery(VideoModel, queryOptions) | 1451 | const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) |
1622 | 1452 | ||
1623 | return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT }) | 1453 | return queryBuilder.queryVideoIds(queryOptions) |
1624 | .then(rows => rows.map(r => r[field])) | 1454 | .then(rows => rows.map(r => r[field])) |
1625 | } | 1455 | } |
1626 | 1456 | ||
1627 | static buildTrendingQuery (trendingDays: number) { | 1457 | static buildTrendingQuery (trendingDays: number) { |
@@ -1639,27 +1469,24 @@ export class VideoModel extends Model { | |||
1639 | } | 1469 | } |
1640 | 1470 | ||
1641 | private static async getAvailableForApi ( | 1471 | private static async getAvailableForApi ( |
1642 | options: BuildVideosQueryOptions, | 1472 | options: BuildVideosListQueryOptions, |
1643 | countVideos = true | 1473 | countVideos = true |
1644 | ): Promise<ResultList<VideoModel>> { | 1474 | ): Promise<ResultList<VideoModel>> { |
1645 | function getCount () { | 1475 | function getCount () { |
1646 | if (countVideos !== true) return Promise.resolve(undefined) | 1476 | if (countVideos !== true) return Promise.resolve(undefined) |
1647 | 1477 | ||
1648 | const countOptions = Object.assign({}, options, { isCount: true }) | 1478 | const countOptions = Object.assign({}, options, { isCount: true }) |
1649 | const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions) | 1479 | const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) |
1650 | 1480 | ||
1651 | return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT }) | 1481 | return queryBuilder.countVideoIds(countOptions) |
1652 | .then(rows => rows.length !== 0 ? rows[0].total : 0) | ||
1653 | } | 1482 | } |
1654 | 1483 | ||
1655 | function getModels () { | 1484 | function getModels () { |
1656 | if (options.count === 0) return Promise.resolve([]) | 1485 | if (options.count === 0) return Promise.resolve([]) |
1657 | 1486 | ||
1658 | const { query, replacements, order } = buildListQuery(VideoModel, options) | 1487 | const queryBuilder = new VideosModelListQueryBuilder(VideoModel.sequelize) |
1659 | const queryModels = wrapForAPIResults(query, replacements, options, order) | ||
1660 | 1488 | ||
1661 | return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true }) | 1489 | return queryBuilder.queryVideos(options) |
1662 | .then(rows => VideoModel.buildAPIResult(rows)) | ||
1663 | } | 1490 | } |
1664 | 1491 | ||
1665 | const [ count, rows ] = await Promise.all([ getCount(), getModels() ]) | 1492 | const [ count, rows ] = await Promise.all([ getCount(), getModels() ]) |
@@ -1670,173 +1497,6 @@ export class VideoModel extends Model { | |||
1670 | } | 1497 | } |
1671 | } | 1498 | } |
1672 | 1499 | ||
1673 | private static buildAPIResult (rows: any[]) { | ||
1674 | const videosMemo: { [ id: number ]: VideoModel } = {} | ||
1675 | const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {} | ||
1676 | |||
1677 | const thumbnailsDone = new Set<number>() | ||
1678 | const historyDone = new Set<number>() | ||
1679 | const videoFilesDone = new Set<number>() | ||
1680 | |||
1681 | const videos: VideoModel[] = [] | ||
1682 | |||
1683 | const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ] | ||
1684 | const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ] | ||
1685 | const serverKeys = [ 'id', 'host' ] | ||
1686 | const videoFileKeys = [ | ||
1687 | 'id', | ||
1688 | 'createdAt', | ||
1689 | 'updatedAt', | ||
1690 | 'resolution', | ||
1691 | 'size', | ||
1692 | 'extname', | ||
1693 | 'filename', | ||
1694 | 'fileUrl', | ||
1695 | 'torrentFilename', | ||
1696 | 'torrentUrl', | ||
1697 | 'infoHash', | ||
1698 | 'fps', | ||
1699 | 'videoId', | ||
1700 | 'videoStreamingPlaylistId' | ||
1701 | ] | ||
1702 | const videoStreamingPlaylistKeys = [ 'id', 'type', 'playlistUrl' ] | ||
1703 | const videoKeys = [ | ||
1704 | 'id', | ||
1705 | 'uuid', | ||
1706 | 'name', | ||
1707 | 'category', | ||
1708 | 'licence', | ||
1709 | 'language', | ||
1710 | 'privacy', | ||
1711 | 'nsfw', | ||
1712 | 'description', | ||
1713 | 'support', | ||
1714 | 'duration', | ||
1715 | 'views', | ||
1716 | 'likes', | ||
1717 | 'dislikes', | ||
1718 | 'remote', | ||
1719 | 'isLive', | ||
1720 | 'url', | ||
1721 | 'commentsEnabled', | ||
1722 | 'downloadEnabled', | ||
1723 | 'waitTranscoding', | ||
1724 | 'state', | ||
1725 | 'publishedAt', | ||
1726 | 'originallyPublishedAt', | ||
1727 | 'channelId', | ||
1728 | 'createdAt', | ||
1729 | 'updatedAt' | ||
1730 | ] | ||
1731 | const buildOpts = { raw: true } | ||
1732 | |||
1733 | function buildActor (rowActor: any) { | ||
1734 | const avatarModel = rowActor.Avatar.id !== null | ||
1735 | ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts) | ||
1736 | : null | ||
1737 | |||
1738 | const serverModel = rowActor.Server.id !== null | ||
1739 | ? new ServerModel(pick(rowActor.Server, serverKeys), buildOpts) | ||
1740 | : null | ||
1741 | |||
1742 | const actorModel = new ActorModel(pick(rowActor, actorKeys), buildOpts) | ||
1743 | actorModel.Avatar = avatarModel | ||
1744 | actorModel.Server = serverModel | ||
1745 | |||
1746 | return actorModel | ||
1747 | } | ||
1748 | |||
1749 | for (const row of rows) { | ||
1750 | if (!videosMemo[row.id]) { | ||
1751 | // Build Channel | ||
1752 | const channel = row.VideoChannel | ||
1753 | const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]), buildOpts) | ||
1754 | channelModel.Actor = buildActor(channel.Actor) | ||
1755 | |||
1756 | const account = row.VideoChannel.Account | ||
1757 | const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]), buildOpts) | ||
1758 | accountModel.Actor = buildActor(account.Actor) | ||
1759 | |||
1760 | channelModel.Account = accountModel | ||
1761 | |||
1762 | const videoModel = new VideoModel(pick(row, videoKeys), buildOpts) | ||
1763 | videoModel.VideoChannel = channelModel | ||
1764 | |||
1765 | videoModel.UserVideoHistories = [] | ||
1766 | videoModel.Thumbnails = [] | ||
1767 | videoModel.VideoFiles = [] | ||
1768 | videoModel.VideoStreamingPlaylists = [] | ||
1769 | |||
1770 | videosMemo[row.id] = videoModel | ||
1771 | // Don't take object value to have a sorted array | ||
1772 | videos.push(videoModel) | ||
1773 | } | ||
1774 | |||
1775 | const videoModel = videosMemo[row.id] | ||
1776 | |||
1777 | if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) { | ||
1778 | const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]), buildOpts) | ||
1779 | videoModel.UserVideoHistories.push(historyModel) | ||
1780 | |||
1781 | historyDone.add(row.userVideoHistory.id) | ||
1782 | } | ||
1783 | |||
1784 | if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) { | ||
1785 | const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]), buildOpts) | ||
1786 | videoModel.Thumbnails.push(thumbnailModel) | ||
1787 | |||
1788 | thumbnailsDone.add(row.Thumbnails.id) | ||
1789 | } | ||
1790 | |||
1791 | if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) { | ||
1792 | const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys), buildOpts) | ||
1793 | videoModel.VideoFiles.push(videoFileModel) | ||
1794 | |||
1795 | videoFilesDone.add(row.VideoFiles.id) | ||
1796 | } | ||
1797 | |||
1798 | if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) { | ||
1799 | const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys), buildOpts) | ||
1800 | streamingPlaylist.VideoFiles = [] | ||
1801 | |||
1802 | videoModel.VideoStreamingPlaylists.push(streamingPlaylist) | ||
1803 | |||
1804 | videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist | ||
1805 | } | ||
1806 | |||
1807 | if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) { | ||
1808 | const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id] | ||
1809 | |||
1810 | const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys), buildOpts) | ||
1811 | streamingPlaylist.VideoFiles.push(videoFileModel) | ||
1812 | |||
1813 | videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id) | ||
1814 | } | ||
1815 | } | ||
1816 | |||
1817 | return videos | ||
1818 | } | ||
1819 | |||
1820 | static getCategoryLabel (id: number) { | ||
1821 | return VIDEO_CATEGORIES[id] || 'Misc' | ||
1822 | } | ||
1823 | |||
1824 | static getLicenceLabel (id: number) { | ||
1825 | return VIDEO_LICENCES[id] || 'Unknown' | ||
1826 | } | ||
1827 | |||
1828 | static getLanguageLabel (id: string) { | ||
1829 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
1830 | } | ||
1831 | |||
1832 | static getPrivacyLabel (id: number) { | ||
1833 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
1834 | } | ||
1835 | |||
1836 | static getStateLabel (id: number) { | ||
1837 | return VIDEO_STATES[id] || 'Unknown' | ||
1838 | } | ||
1839 | |||
1840 | isBlacklisted () { | 1500 | isBlacklisted () { |
1841 | return !!this.VideoBlacklist | 1501 | return !!this.VideoBlacklist |
1842 | } | 1502 | } |
@@ -1885,7 +1545,7 @@ export class VideoModel extends Model { | |||
1885 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 | 1545 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 |
1886 | } | 1546 | } |
1887 | 1547 | ||
1888 | async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) { | 1548 | async addAndSaveThumbnail (thumbnail: MThumbnail, transaction?: Transaction) { |
1889 | thumbnail.videoId = this.id | 1549 | thumbnail.videoId = this.id |
1890 | 1550 | ||
1891 | const savedThumbnail = await thumbnail.save({ transaction }) | 1551 | const savedThumbnail = await thumbnail.save({ transaction }) |
@@ -1919,7 +1579,7 @@ export class VideoModel extends Model { | |||
1919 | } | 1579 | } |
1920 | 1580 | ||
1921 | getWatchStaticPath () { | 1581 | getWatchStaticPath () { |
1922 | return '/videos/watch/' + this.uuid | 1582 | return '/w/' + this.uuid |
1923 | } | 1583 | } |
1924 | 1584 | ||
1925 | getEmbedStaticPath () { | 1585 | getEmbedStaticPath () { |
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts index b6c538e19..be94e219c 100644 --- a/server/tests/api/activitypub/client.ts +++ b/server/tests/api/activitypub/client.ts | |||
@@ -1,23 +1,65 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { VideoPlaylistPrivacy } from '@shared/models' | ||
6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { | 7 | import { |
6 | cleanupTests, | 8 | cleanupTests, |
9 | createVideoPlaylist, | ||
7 | doubleFollow, | 10 | doubleFollow, |
8 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
9 | makeActivityPubGetRequest, | 12 | makeActivityPubGetRequest, |
10 | ServerInfo, | 13 | ServerInfo, |
11 | setAccessTokensToServers, | 14 | setAccessTokensToServers, |
12 | uploadVideo | 15 | setDefaultVideoChannel, |
16 | uploadVideoAndGetId | ||
13 | } from '../../../../shared/extra-utils' | 17 | } from '../../../../shared/extra-utils' |
14 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
15 | 18 | ||
16 | const expect = chai.expect | 19 | const expect = chai.expect |
17 | 20 | ||
18 | describe('Test activitypub', function () { | 21 | describe('Test activitypub', function () { |
19 | let servers: ServerInfo[] = [] | 22 | let servers: ServerInfo[] = [] |
20 | let videoUUID: string | 23 | let video: { id: number, uuid: string, shortUUID: string } |
24 | let playlist: { id: number, uuid: string, shortUUID: string } | ||
25 | |||
26 | async function testAccount (path: string) { | ||
27 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
28 | const object = res.body | ||
29 | |||
30 | expect(object.type).to.equal('Person') | ||
31 | expect(object.id).to.equal('http://localhost:' + servers[0].port + '/accounts/root') | ||
32 | expect(object.name).to.equal('root') | ||
33 | expect(object.preferredUsername).to.equal('root') | ||
34 | } | ||
35 | |||
36 | async function testChannel (path: string) { | ||
37 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
38 | const object = res.body | ||
39 | |||
40 | expect(object.type).to.equal('Group') | ||
41 | expect(object.id).to.equal('http://localhost:' + servers[0].port + '/video-channels/root_channel') | ||
42 | expect(object.name).to.equal('Main root channel') | ||
43 | expect(object.preferredUsername).to.equal('root_channel') | ||
44 | } | ||
45 | |||
46 | async function testVideo (path: string) { | ||
47 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
48 | const object = res.body | ||
49 | |||
50 | expect(object.type).to.equal('Video') | ||
51 | expect(object.id).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + video.uuid) | ||
52 | expect(object.name).to.equal('video') | ||
53 | } | ||
54 | |||
55 | async function testPlaylist (path: string) { | ||
56 | const res = await makeActivityPubGetRequest(servers[0].url, path) | ||
57 | const object = res.body | ||
58 | |||
59 | expect(object.type).to.equal('Playlist') | ||
60 | expect(object.id).to.equal('http://localhost:' + servers[0].port + '/video-playlists/' + playlist.uuid) | ||
61 | expect(object.name).to.equal('playlist') | ||
62 | } | ||
21 | 63 | ||
22 | before(async function () { | 64 | before(async function () { |
23 | this.timeout(30000) | 65 | this.timeout(30000) |
@@ -25,38 +67,56 @@ describe('Test activitypub', function () { | |||
25 | servers = await flushAndRunMultipleServers(2) | 67 | servers = await flushAndRunMultipleServers(2) |
26 | 68 | ||
27 | await setAccessTokensToServers(servers) | 69 | await setAccessTokensToServers(servers) |
70 | await setDefaultVideoChannel(servers) | ||
28 | 71 | ||
29 | { | 72 | { |
30 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' }) | 73 | video = await uploadVideoAndGetId({ server: servers[0], videoName: 'video' }) |
31 | videoUUID = res.body.video.uuid | 74 | } |
75 | |||
76 | { | ||
77 | const playlistAttrs = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].videoChannel.id } | ||
78 | const resCreate = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs }) | ||
79 | playlist = resCreate.body.videoPlaylist | ||
32 | } | 80 | } |
33 | 81 | ||
34 | await doubleFollow(servers[0], servers[1]) | 82 | await doubleFollow(servers[0], servers[1]) |
35 | }) | 83 | }) |
36 | 84 | ||
37 | it('Should return the account object', async function () { | 85 | it('Should return the account object', async function () { |
38 | const res = await makeActivityPubGetRequest(servers[0].url, '/accounts/root') | 86 | await testAccount('/accounts/root') |
39 | const object = res.body | 87 | await testAccount('/a/root') |
88 | }) | ||
40 | 89 | ||
41 | expect(object.type).to.equal('Person') | 90 | it('Should return the channel object', async function () { |
42 | expect(object.id).to.equal('http://localhost:' + servers[0].port + '/accounts/root') | 91 | await testChannel('/video-channels/root_channel') |
43 | expect(object.name).to.equal('root') | 92 | await testChannel('/c/root_channel') |
44 | expect(object.preferredUsername).to.equal('root') | ||
45 | }) | 93 | }) |
46 | 94 | ||
47 | it('Should return the video object', async function () { | 95 | it('Should return the video object', async function () { |
48 | const res = await makeActivityPubGetRequest(servers[0].url, '/videos/watch/' + videoUUID) | 96 | await testVideo('/videos/watch/' + video.id) |
49 | const object = res.body | 97 | await testVideo('/videos/watch/' + video.uuid) |
98 | await testVideo('/videos/watch/' + video.shortUUID) | ||
99 | await testVideo('/w/' + video.id) | ||
100 | await testVideo('/w/' + video.uuid) | ||
101 | await testVideo('/w/' + video.shortUUID) | ||
102 | }) | ||
50 | 103 | ||
51 | expect(object.type).to.equal('Video') | 104 | it('Should return the playlist object', async function () { |
52 | expect(object.id).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + videoUUID) | 105 | await testPlaylist('/video-playlists/' + playlist.id) |
53 | expect(object.name).to.equal('video') | 106 | await testPlaylist('/video-playlists/' + playlist.uuid) |
107 | await testPlaylist('/video-playlists/' + playlist.shortUUID) | ||
108 | await testPlaylist('/w/p/' + playlist.id) | ||
109 | await testPlaylist('/w/p/' + playlist.uuid) | ||
110 | await testPlaylist('/w/p/' + playlist.shortUUID) | ||
111 | await testPlaylist('/videos/watch/playlist/' + playlist.id) | ||
112 | await testPlaylist('/videos/watch/playlist/' + playlist.uuid) | ||
113 | await testPlaylist('/videos/watch/playlist/' + playlist.shortUUID) | ||
54 | }) | 114 | }) |
55 | 115 | ||
56 | it('Should redirect to the origin video object', async function () { | 116 | it('Should redirect to the origin video object', async function () { |
57 | const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + videoUUID, HttpStatusCode.FOUND_302) | 117 | const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + video.uuid, HttpStatusCode.FOUND_302) |
58 | 118 | ||
59 | expect(res.header.location).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + videoUUID) | 119 | expect(res.header.location).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + video.uuid) |
60 | }) | 120 | }) |
61 | 121 | ||
62 | after(async function () { | 122 | after(async function () { |
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts index 60d95b823..66d7631b7 100644 --- a/server/tests/api/activitypub/helpers.ts +++ b/server/tests/api/activitypub/helpers.ts | |||
@@ -6,13 +6,14 @@ import { buildRequestStub } from '../../../../shared/extra-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' |
9 | import { buildAbsoluteFixturePath } from '@shared/extra-utils' | ||
9 | 10 | ||
10 | describe('Test activity pub helpers', function () { | 11 | describe('Test activity pub helpers', function () { |
11 | describe('When checking the Linked Signature', function () { | 12 | describe('When checking the Linked Signature', function () { |
12 | 13 | ||
13 | it('Should fail with an invalid Mastodon signature', async function () { | 14 | it('Should fail with an invalid Mastodon signature', async function () { |
14 | const body = require('./json/mastodon/create-bad-signature.json') | 15 | const body = require(buildAbsoluteFixturePath('./ap-json/mastodon/create-bad-signature.json')) |
15 | const publicKey = require('./json/mastodon/public-key.json').publicKey | 16 | const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey |
16 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } | 17 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } |
17 | 18 | ||
18 | const result = await isJsonLDSignatureVerified(fromActor as any, body) | 19 | const result = await isJsonLDSignatureVerified(fromActor as any, body) |
@@ -21,8 +22,8 @@ describe('Test activity pub helpers', function () { | |||
21 | }) | 22 | }) |
22 | 23 | ||
23 | it('Should fail with an invalid public key', async function () { | 24 | it('Should fail with an invalid public key', async function () { |
24 | const body = require('./json/mastodon/create.json') | 25 | const body = require(buildAbsoluteFixturePath('./ap-json/mastodon/create.json')) |
25 | const publicKey = require('./json/mastodon/bad-public-key.json').publicKey | 26 | const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey |
26 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } | 27 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } |
27 | 28 | ||
28 | const result = await isJsonLDSignatureVerified(fromActor as any, body) | 29 | const result = await isJsonLDSignatureVerified(fromActor as any, body) |
@@ -31,8 +32,8 @@ describe('Test activity pub helpers', function () { | |||
31 | }) | 32 | }) |
32 | 33 | ||
33 | it('Should succeed with a valid Mastodon signature', async function () { | 34 | it('Should succeed with a valid Mastodon signature', async function () { |
34 | const body = require('./json/mastodon/create.json') | 35 | const body = require(buildAbsoluteFixturePath('./ap-json/mastodon/create.json')) |
35 | const publicKey = require('./json/mastodon/public-key.json').publicKey | 36 | const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey |
36 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } | 37 | const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } |
37 | 38 | ||
38 | const result = await isJsonLDSignatureVerified(fromActor as any, body) | 39 | const result = await isJsonLDSignatureVerified(fromActor as any, body) |
@@ -41,8 +42,8 @@ describe('Test activity pub helpers', function () { | |||
41 | }) | 42 | }) |
42 | 43 | ||
43 | it('Should fail with an invalid PeerTube signature', async function () { | 44 | it('Should fail with an invalid PeerTube signature', async function () { |
44 | const keys = require('./json/peertube/invalid-keys.json') | 45 | const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json')) |
45 | const body = require('./json/peertube/announce-without-context.json') | 46 | const body = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) |
46 | 47 | ||
47 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | 48 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } |
48 | const signedBody = await buildSignedActivity(actorSignature as any, body) | 49 | const signedBody = await buildSignedActivity(actorSignature as any, body) |
@@ -54,8 +55,8 @@ describe('Test activity pub helpers', function () { | |||
54 | }) | 55 | }) |
55 | 56 | ||
56 | it('Should succeed with a valid PeerTube signature', async function () { | 57 | it('Should succeed with a valid PeerTube signature', async function () { |
57 | const keys = require('./json/peertube/keys.json') | 58 | const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json')) |
58 | const body = require('./json/peertube/announce-without-context.json') | 59 | const body = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) |
59 | 60 | ||
60 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } | 61 | const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } |
61 | const signedBody = await buildSignedActivity(actorSignature as any, body) | 62 | const signedBody = await buildSignedActivity(actorSignature as any, body) |
@@ -73,12 +74,12 @@ describe('Test activity pub helpers', function () { | |||
73 | req.method = 'POST' | 74 | req.method = 'POST' |
74 | req.url = '/accounts/ronan/inbox' | 75 | req.url = '/accounts/ronan/inbox' |
75 | 76 | ||
76 | const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json')) | 77 | const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/bad-http-signature.json'))) |
77 | req.body = mastodonObject.body | 78 | req.body = mastodonObject.body |
78 | req.headers = mastodonObject.headers | 79 | req.headers = mastodonObject.headers |
79 | 80 | ||
80 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | 81 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) |
81 | const publicKey = require('./json/mastodon/public-key.json').publicKey | 82 | const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey |
82 | 83 | ||
83 | const actor = { publicKey } | 84 | const actor = { publicKey } |
84 | const verified = isHTTPSignatureVerified(parsed, actor as any) | 85 | const verified = isHTTPSignatureVerified(parsed, actor as any) |
@@ -91,12 +92,12 @@ describe('Test activity pub helpers', function () { | |||
91 | req.method = 'POST' | 92 | req.method = 'POST' |
92 | req.url = '/accounts/ronan/inbox' | 93 | req.url = '/accounts/ronan/inbox' |
93 | 94 | ||
94 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | 95 | const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) |
95 | req.body = mastodonObject.body | 96 | req.body = mastodonObject.body |
96 | req.headers = mastodonObject.headers | 97 | req.headers = mastodonObject.headers |
97 | 98 | ||
98 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | 99 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) |
99 | const publicKey = require('./json/mastodon/bad-public-key.json').publicKey | 100 | const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey |
100 | 101 | ||
101 | const actor = { publicKey } | 102 | const actor = { publicKey } |
102 | const verified = isHTTPSignatureVerified(parsed, actor as any) | 103 | const verified = isHTTPSignatureVerified(parsed, actor as any) |
@@ -109,7 +110,7 @@ describe('Test activity pub helpers', function () { | |||
109 | req.method = 'POST' | 110 | req.method = 'POST' |
110 | req.url = '/accounts/ronan/inbox' | 111 | req.url = '/accounts/ronan/inbox' |
111 | 112 | ||
112 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | 113 | const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) |
113 | req.body = mastodonObject.body | 114 | req.body = mastodonObject.body |
114 | req.headers = mastodonObject.headers | 115 | req.headers = mastodonObject.headers |
115 | 116 | ||
@@ -128,7 +129,7 @@ describe('Test activity pub helpers', function () { | |||
128 | req.method = 'POST' | 129 | req.method = 'POST' |
129 | req.url = '/accounts/ronan/inbox' | 130 | req.url = '/accounts/ronan/inbox' |
130 | 131 | ||
131 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | 132 | const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) |
132 | req.body = mastodonObject.body | 133 | req.body = mastodonObject.body |
133 | req.headers = mastodonObject.headers | 134 | req.headers = mastodonObject.headers |
134 | req.headers = 'Signature ' + mastodonObject.headers | 135 | req.headers = 'Signature ' + mastodonObject.headers |
@@ -148,12 +149,12 @@ describe('Test activity pub helpers', function () { | |||
148 | req.method = 'POST' | 149 | req.method = 'POST' |
149 | req.url = '/accounts/ronan/inbox' | 150 | req.url = '/accounts/ronan/inbox' |
150 | 151 | ||
151 | const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) | 152 | const mastodonObject = cloneDeep(require(buildAbsoluteFixturePath('./ap-json/mastodon/http-signature.json'))) |
152 | req.body = mastodonObject.body | 153 | req.body = mastodonObject.body |
153 | req.headers = mastodonObject.headers | 154 | req.headers = mastodonObject.headers |
154 | 155 | ||
155 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) | 156 | const parsed = parseHTTPSignature(req, 3600 * 1000 * 365 * 10) |
156 | const publicKey = require('./json/mastodon/public-key.json').publicKey | 157 | const publicKey = require(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey |
157 | 158 | ||
158 | const actor = { publicKey } | 159 | const actor = { publicKey } |
159 | const verified = isHTTPSignatureVerified(parsed, actor as any) | 160 | const verified = isHTTPSignatureVerified(parsed, actor as any) |
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts index 364b53e0f..61db272f6 100644 --- a/server/tests/api/activitypub/security.ts +++ b/server/tests/api/activitypub/security.ts | |||
@@ -5,6 +5,7 @@ import * as chai from 'chai' | |||
5 | import { buildDigest } from '@server/helpers/peertube-crypto' | 5 | import { buildDigest } from '@server/helpers/peertube-crypto' |
6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
7 | import { | 7 | import { |
8 | buildAbsoluteFixturePath, | ||
8 | cleanupTests, | 9 | cleanupTests, |
9 | closeAllSequelize, | 10 | closeAllSequelize, |
10 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
@@ -40,7 +41,7 @@ function setUpdatedAtOfServer (onServer: ServerInfo, ofServer: ServerInfo, updat | |||
40 | } | 41 | } |
41 | 42 | ||
42 | function getAnnounceWithoutContext (server: ServerInfo) { | 43 | function getAnnounceWithoutContext (server: ServerInfo) { |
43 | const json = require('./json/peertube/announce-without-context.json') | 44 | const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) |
44 | const result: typeof json = {} | 45 | const result: typeof json = {} |
45 | 46 | ||
46 | for (const key of Object.keys(json)) { | 47 | for (const key of Object.keys(json)) { |
@@ -58,8 +59,8 @@ describe('Test ActivityPub security', function () { | |||
58 | let servers: ServerInfo[] | 59 | let servers: ServerInfo[] |
59 | let url: string | 60 | let url: string |
60 | 61 | ||
61 | const keys = require('./json/peertube/keys.json') | 62 | const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json')) |
62 | const invalidKeys = require('./json/peertube/invalid-keys.json') | 63 | const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json')) |
63 | const baseHttpSignature = () => ({ | 64 | const baseHttpSignature = () => ({ |
64 | algorithm: HTTP_SIGNATURE.ALGORITHM, | 65 | algorithm: HTTP_SIGNATURE.ALGORITHM, |
65 | authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, | 66 | authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, |
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts index 2aa09334c..2054776cc 100644 --- a/server/tests/api/check-params/abuses.ts +++ b/server/tests/api/check-params/abuses.ts | |||
@@ -258,7 +258,7 @@ describe('Test abuses API validators', function () { | |||
258 | }) | 258 | }) |
259 | 259 | ||
260 | it('Should succeed with the correct parameters (basic)', async function () { | 260 | it('Should succeed with the correct parameters (basic)', async function () { |
261 | const fields: AbuseCreate = { video: { id: server.video.id }, reason: 'my super reason' } | 261 | const fields: AbuseCreate = { video: { id: server.video.shortUUID }, reason: 'my super reason' } |
262 | 262 | ||
263 | const res = await makePostBodyRequest({ | 263 | const res = await makePostBodyRequest({ |
264 | url: server.url, | 264 | url: server.url, |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 004aa65b3..9549070ef 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -73,7 +73,8 @@ describe('Test config API validators', function () { | |||
73 | signup: { | 73 | signup: { |
74 | enabled: false, | 74 | enabled: false, |
75 | limit: 5, | 75 | limit: 5, |
76 | requiresEmailVerification: false | 76 | requiresEmailVerification: false, |
77 | minimumAge: 16 | ||
77 | }, | 78 | }, |
78 | admin: { | 79 | admin: { |
79 | email: 'superadmin1@example.com' | 80 | email: 'superadmin1@example.com' |
diff --git a/server/tests/api/check-params/custom-pages.ts b/server/tests/api/check-params/custom-pages.ts new file mode 100644 index 000000000..74ca3384c --- /dev/null +++ b/server/tests/api/check-params/custom-pages.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createUser, | ||
8 | flushAndRunServer, | ||
9 | ServerInfo, | ||
10 | setAccessTokensToServers, | ||
11 | userLogin | ||
12 | } from '../../../../shared/extra-utils' | ||
13 | import { makeGetRequest, makePutBodyRequest } from '../../../../shared/extra-utils/requests/requests' | ||
14 | |||
15 | describe('Test custom pages validators', function () { | ||
16 | const path = '/api/v1/custom-pages/homepage/instance' | ||
17 | |||
18 | let server: ServerInfo | ||
19 | let userAccessToken: string | ||
20 | |||
21 | // --------------------------------------------------------------- | ||
22 | |||
23 | before(async function () { | ||
24 | this.timeout(120000) | ||
25 | |||
26 | server = await flushAndRunServer(1) | ||
27 | await setAccessTokensToServers([ server ]) | ||
28 | |||
29 | const user = { username: 'user1', password: 'password' } | ||
30 | await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) | ||
31 | |||
32 | userAccessToken = await userLogin(server, user) | ||
33 | }) | ||
34 | |||
35 | describe('When updating instance homepage', function () { | ||
36 | |||
37 | it('Should fail with an unauthenticated user', async function () { | ||
38 | await makePutBodyRequest({ | ||
39 | url: server.url, | ||
40 | path, | ||
41 | fields: { content: 'super content' }, | ||
42 | statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 | ||
43 | }) | ||
44 | }) | ||
45 | |||
46 | it('Should fail with a non admin user', async function () { | ||
47 | await makePutBodyRequest({ | ||
48 | url: server.url, | ||
49 | path, | ||
50 | token: userAccessToken, | ||
51 | fields: { content: 'super content' }, | ||
52 | statusCodeExpected: HttpStatusCode.FORBIDDEN_403 | ||
53 | }) | ||
54 | }) | ||
55 | |||
56 | it('Should succeed with the correct params', async function () { | ||
57 | await makePutBodyRequest({ | ||
58 | url: server.url, | ||
59 | path, | ||
60 | token: server.accessToken, | ||
61 | fields: { content: 'super content' }, | ||
62 | statusCodeExpected: HttpStatusCode.NO_CONTENT_204 | ||
63 | }) | ||
64 | }) | ||
65 | }) | ||
66 | |||
67 | describe('When getting instance homapage', function () { | ||
68 | |||
69 | it('Should succeed with the correct params', async function () { | ||
70 | await makeGetRequest({ | ||
71 | url: server.url, | ||
72 | path, | ||
73 | statusCodeExpected: HttpStatusCode.OK_200 | ||
74 | }) | ||
75 | }) | ||
76 | }) | ||
77 | |||
78 | after(async function () { | ||
79 | await cleanupTests([ server ]) | ||
80 | }) | ||
81 | }) | ||
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 143515838..ce2335e42 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -3,6 +3,7 @@ import './accounts' | |||
3 | import './blocklist' | 3 | import './blocklist' |
4 | import './bulk' | 4 | import './bulk' |
5 | import './config' | 5 | import './config' |
6 | import './custom-pages' | ||
6 | import './contact-form' | 7 | import './contact-form' |
7 | import './debug' | 8 | import './debug' |
8 | import './follows' | 9 | import './follows' |
diff --git a/server/tests/api/check-params/live.ts b/server/tests/api/check-params/live.ts index c171b1f81..933d8abf2 100644 --- a/server/tests/api/check-params/live.ts +++ b/server/tests/api/check-params/live.ts | |||
@@ -2,9 +2,10 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
5 | import { join } from 'path' | 5 | import { LiveVideo, VideoCreateResult, VideoPrivacy } from '@shared/models' |
6 | import { LiveVideo, VideoPrivacy } from '@shared/models' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
7 | import { | 7 | import { |
8 | buildAbsoluteFixturePath, | ||
8 | cleanupTests, | 9 | cleanupTests, |
9 | createUser, | 10 | createUser, |
10 | flushAndRunServer, | 11 | flushAndRunServer, |
@@ -24,14 +25,13 @@ import { | |||
24 | userLogin, | 25 | userLogin, |
25 | waitUntilLivePublished | 26 | waitUntilLivePublished |
26 | } from '../../../../shared/extra-utils' | 27 | } from '../../../../shared/extra-utils' |
27 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
28 | 28 | ||
29 | describe('Test video lives API validator', function () { | 29 | describe('Test video lives API validator', function () { |
30 | const path = '/api/v1/videos/live' | 30 | const path = '/api/v1/videos/live' |
31 | let server: ServerInfo | 31 | let server: ServerInfo |
32 | let userAccessToken = '' | 32 | let userAccessToken = '' |
33 | let channelId: number | 33 | let channelId: number |
34 | let videoId: number | 34 | let video: VideoCreateResult |
35 | let videoIdNotLive: number | 35 | let videoIdNotLive: number |
36 | 36 | ||
37 | // --------------------------------------------------------------- | 37 | // --------------------------------------------------------------- |
@@ -180,7 +180,7 @@ describe('Test video lives API validator', function () { | |||
180 | it('Should fail with an incorrect thumbnail file', async function () { | 180 | it('Should fail with an incorrect thumbnail file', async function () { |
181 | const fields = baseCorrectParams | 181 | const fields = baseCorrectParams |
182 | const attaches = { | 182 | const attaches = { |
183 | thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 183 | thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') |
184 | } | 184 | } |
185 | 185 | ||
186 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 186 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -189,7 +189,7 @@ describe('Test video lives API validator', function () { | |||
189 | it('Should fail with a big thumbnail file', async function () { | 189 | it('Should fail with a big thumbnail file', async function () { |
190 | const fields = baseCorrectParams | 190 | const fields = baseCorrectParams |
191 | const attaches = { | 191 | const attaches = { |
192 | thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'preview-big.png') | 192 | thumbnailfile: buildAbsoluteFixturePath('preview-big.png') |
193 | } | 193 | } |
194 | 194 | ||
195 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 195 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -198,7 +198,7 @@ describe('Test video lives API validator', function () { | |||
198 | it('Should fail with an incorrect preview file', async function () { | 198 | it('Should fail with an incorrect preview file', async function () { |
199 | const fields = baseCorrectParams | 199 | const fields = baseCorrectParams |
200 | const attaches = { | 200 | const attaches = { |
201 | previewfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 201 | previewfile: buildAbsoluteFixturePath('video_short.mp4') |
202 | } | 202 | } |
203 | 203 | ||
204 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 204 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -207,7 +207,7 @@ describe('Test video lives API validator', function () { | |||
207 | it('Should fail with a big preview file', async function () { | 207 | it('Should fail with a big preview file', async function () { |
208 | const fields = baseCorrectParams | 208 | const fields = baseCorrectParams |
209 | const attaches = { | 209 | const attaches = { |
210 | previewfile: join(__dirname, '..', '..', 'fixtures', 'preview-big.png') | 210 | previewfile: buildAbsoluteFixturePath('preview-big.png') |
211 | } | 211 | } |
212 | 212 | ||
213 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 213 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -230,7 +230,7 @@ describe('Test video lives API validator', function () { | |||
230 | statusCodeExpected: HttpStatusCode.OK_200 | 230 | statusCodeExpected: HttpStatusCode.OK_200 |
231 | }) | 231 | }) |
232 | 232 | ||
233 | videoId = res.body.video.id | 233 | video = res.body.video |
234 | }) | 234 | }) |
235 | 235 | ||
236 | it('Should forbid if live is disabled', async function () { | 236 | it('Should forbid if live is disabled', async function () { |
@@ -326,15 +326,15 @@ describe('Test video lives API validator', function () { | |||
326 | describe('When getting live information', function () { | 326 | describe('When getting live information', function () { |
327 | 327 | ||
328 | it('Should fail without access token', async function () { | 328 | it('Should fail without access token', async function () { |
329 | await getLive(server.url, '', videoId, HttpStatusCode.UNAUTHORIZED_401) | 329 | await getLive(server.url, '', video.id, HttpStatusCode.UNAUTHORIZED_401) |
330 | }) | 330 | }) |
331 | 331 | ||
332 | it('Should fail with a bad access token', async function () { | 332 | it('Should fail with a bad access token', async function () { |
333 | await getLive(server.url, 'toto', videoId, HttpStatusCode.UNAUTHORIZED_401) | 333 | await getLive(server.url, 'toto', video.id, HttpStatusCode.UNAUTHORIZED_401) |
334 | }) | 334 | }) |
335 | 335 | ||
336 | it('Should fail with access token of another user', async function () { | 336 | it('Should fail with access token of another user', async function () { |
337 | await getLive(server.url, userAccessToken, videoId, HttpStatusCode.FORBIDDEN_403) | 337 | await getLive(server.url, userAccessToken, video.id, HttpStatusCode.FORBIDDEN_403) |
338 | }) | 338 | }) |
339 | 339 | ||
340 | it('Should fail with a bad video id', async function () { | 340 | it('Should fail with a bad video id', async function () { |
@@ -350,22 +350,23 @@ describe('Test video lives API validator', function () { | |||
350 | }) | 350 | }) |
351 | 351 | ||
352 | it('Should succeed with the correct params', async function () { | 352 | it('Should succeed with the correct params', async function () { |
353 | await getLive(server.url, server.accessToken, videoId) | 353 | await getLive(server.url, server.accessToken, video.id) |
354 | await getLive(server.url, server.accessToken, video.shortUUID) | ||
354 | }) | 355 | }) |
355 | }) | 356 | }) |
356 | 357 | ||
357 | describe('When updating live information', async function () { | 358 | describe('When updating live information', async function () { |
358 | 359 | ||
359 | it('Should fail without access token', async function () { | 360 | it('Should fail without access token', async function () { |
360 | await updateLive(server.url, '', videoId, {}, HttpStatusCode.UNAUTHORIZED_401) | 361 | await updateLive(server.url, '', video.id, {}, HttpStatusCode.UNAUTHORIZED_401) |
361 | }) | 362 | }) |
362 | 363 | ||
363 | it('Should fail with a bad access token', async function () { | 364 | it('Should fail with a bad access token', async function () { |
364 | await updateLive(server.url, 'toto', videoId, {}, HttpStatusCode.UNAUTHORIZED_401) | 365 | await updateLive(server.url, 'toto', video.id, {}, HttpStatusCode.UNAUTHORIZED_401) |
365 | }) | 366 | }) |
366 | 367 | ||
367 | it('Should fail with access token of another user', async function () { | 368 | it('Should fail with access token of another user', async function () { |
368 | await updateLive(server.url, userAccessToken, videoId, {}, HttpStatusCode.FORBIDDEN_403) | 369 | await updateLive(server.url, userAccessToken, video.id, {}, HttpStatusCode.FORBIDDEN_403) |
369 | }) | 370 | }) |
370 | 371 | ||
371 | it('Should fail with a bad video id', async function () { | 372 | it('Should fail with a bad video id', async function () { |
@@ -383,11 +384,12 @@ describe('Test video lives API validator', function () { | |||
383 | it('Should fail with save replay and permanent live set to true', async function () { | 384 | it('Should fail with save replay and permanent live set to true', async function () { |
384 | const fields = { saveReplay: true, permanentLive: true } | 385 | const fields = { saveReplay: true, permanentLive: true } |
385 | 386 | ||
386 | await updateLive(server.url, server.accessToken, videoId, fields, HttpStatusCode.BAD_REQUEST_400) | 387 | await updateLive(server.url, server.accessToken, video.id, fields, HttpStatusCode.BAD_REQUEST_400) |
387 | }) | 388 | }) |
388 | 389 | ||
389 | it('Should succeed with the correct params', async function () { | 390 | it('Should succeed with the correct params', async function () { |
390 | await updateLive(server.url, server.accessToken, videoId, { saveReplay: false }) | 391 | await updateLive(server.url, server.accessToken, video.id, { saveReplay: false }) |
392 | await updateLive(server.url, server.accessToken, video.shortUUID, { saveReplay: false }) | ||
391 | }) | 393 | }) |
392 | 394 | ||
393 | it('Should fail to update replay status if replay is not allowed on the instance', async function () { | 395 | it('Should fail to update replay status if replay is not allowed on the instance', async function () { |
@@ -398,19 +400,19 @@ describe('Test video lives API validator', function () { | |||
398 | } | 400 | } |
399 | }) | 401 | }) |
400 | 402 | ||
401 | await updateLive(server.url, server.accessToken, videoId, { saveReplay: true }, HttpStatusCode.FORBIDDEN_403) | 403 | await updateLive(server.url, server.accessToken, video.id, { saveReplay: true }, HttpStatusCode.FORBIDDEN_403) |
402 | }) | 404 | }) |
403 | 405 | ||
404 | it('Should fail to update a live if it has already started', async function () { | 406 | it('Should fail to update a live if it has already started', async function () { |
405 | this.timeout(40000) | 407 | this.timeout(40000) |
406 | 408 | ||
407 | const resLive = await getLive(server.url, server.accessToken, videoId) | 409 | const resLive = await getLive(server.url, server.accessToken, video.id) |
408 | const live: LiveVideo = resLive.body | 410 | const live: LiveVideo = resLive.body |
409 | 411 | ||
410 | const command = sendRTMPStream(live.rtmpUrl, live.streamKey) | 412 | const command = sendRTMPStream(live.rtmpUrl, live.streamKey) |
411 | 413 | ||
412 | await waitUntilLivePublished(server.url, server.accessToken, videoId) | 414 | await waitUntilLivePublished(server.url, server.accessToken, video.id) |
413 | await updateLive(server.url, server.accessToken, videoId, {}, HttpStatusCode.BAD_REQUEST_400) | 415 | await updateLive(server.url, server.accessToken, video.id, {}, HttpStatusCode.BAD_REQUEST_400) |
414 | 416 | ||
415 | await stopFfmpeg(command) | 417 | await stopFfmpeg(command) |
416 | }) | 418 | }) |
@@ -418,14 +420,14 @@ describe('Test video lives API validator', function () { | |||
418 | it('Should fail to stream twice in the save live', async function () { | 420 | it('Should fail to stream twice in the save live', async function () { |
419 | this.timeout(40000) | 421 | this.timeout(40000) |
420 | 422 | ||
421 | const resLive = await getLive(server.url, server.accessToken, videoId) | 423 | const resLive = await getLive(server.url, server.accessToken, video.id) |
422 | const live: LiveVideo = resLive.body | 424 | const live: LiveVideo = resLive.body |
423 | 425 | ||
424 | const command = sendRTMPStream(live.rtmpUrl, live.streamKey) | 426 | const command = sendRTMPStream(live.rtmpUrl, live.streamKey) |
425 | 427 | ||
426 | await waitUntilLivePublished(server.url, server.accessToken, videoId) | 428 | await waitUntilLivePublished(server.url, server.accessToken, video.id) |
427 | 429 | ||
428 | await runAndTestFfmpegStreamError(server.url, server.accessToken, videoId, true) | 430 | await runAndTestFfmpegStreamError(server.url, server.accessToken, video.id, true) |
429 | 431 | ||
430 | await stopFfmpeg(command) | 432 | await stopFfmpeg(command) |
431 | }) | 433 | }) |
diff --git a/server/tests/api/check-params/plugins.ts b/server/tests/api/check-params/plugins.ts index 6e540bcbb..a833fe6ff 100644 --- a/server/tests/api/check-params/plugins.ts +++ b/server/tests/api/check-params/plugins.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | import { HttpStatusCode } from '@shared/core-utils' | |
5 | import { | 5 | import { |
6 | checkBadCountPagination, | 6 | checkBadCountPagination, |
7 | checkBadSortPagination, | 7 | checkBadSortPagination, |
@@ -11,14 +11,14 @@ import { | |||
11 | flushAndRunServer, | 11 | flushAndRunServer, |
12 | immutableAssign, | 12 | immutableAssign, |
13 | installPlugin, | 13 | installPlugin, |
14 | makeGetRequest, makePostBodyRequest, makePutBodyRequest, | 14 | makeGetRequest, |
15 | makePostBodyRequest, | ||
16 | makePutBodyRequest, | ||
15 | ServerInfo, | 17 | ServerInfo, |
16 | setAccessTokensToServers, | 18 | setAccessTokensToServers, |
17 | userLogin | 19 | userLogin |
18 | } from '../../../../shared/extra-utils' | 20 | } from '@shared/extra-utils' |
19 | import { PluginType } from '../../../../shared/models/plugins/plugin.type' | 21 | import { PeerTubePlugin, PluginType } from '@shared/models' |
20 | import { PeerTubePlugin } from '../../../../shared/models/plugins/peertube-plugin.model' | ||
21 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
22 | 22 | ||
23 | describe('Test server plugins API validators', function () { | 23 | describe('Test server plugins API validators', function () { |
24 | let server: ServerInfo | 24 | let server: ServerInfo |
diff --git a/server/tests/api/check-params/redundancy.ts b/server/tests/api/check-params/redundancy.ts index 71be50a6f..dac6938de 100644 --- a/server/tests/api/check-params/redundancy.ts +++ b/server/tests/api/check-params/redundancy.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | 4 | import { VideoCreateResult } from '@shared/models' | |
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { | 6 | import { |
6 | checkBadCountPagination, | 7 | checkBadCountPagination, |
7 | checkBadSortPagination, | 8 | checkBadSortPagination, |
@@ -9,20 +10,24 @@ import { | |||
9 | cleanupTests, | 10 | cleanupTests, |
10 | createUser, | 11 | createUser, |
11 | doubleFollow, | 12 | doubleFollow, |
12 | flushAndRunMultipleServers, makeDeleteRequest, | 13 | flushAndRunMultipleServers, |
13 | makeGetRequest, makePostBodyRequest, | 14 | getVideo, |
15 | makeDeleteRequest, | ||
16 | makeGetRequest, | ||
17 | makePostBodyRequest, | ||
14 | makePutBodyRequest, | 18 | makePutBodyRequest, |
15 | ServerInfo, | 19 | ServerInfo, |
16 | setAccessTokensToServers, uploadVideoAndGetId, | 20 | setAccessTokensToServers, |
17 | userLogin, waitJobs, getVideoIdFromUUID | 21 | uploadVideoAndGetId, |
22 | userLogin, | ||
23 | waitJobs | ||
18 | } from '../../../../shared/extra-utils' | 24 | } from '../../../../shared/extra-utils' |
19 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
20 | 25 | ||
21 | describe('Test server redundancy API validators', function () { | 26 | describe('Test server redundancy API validators', function () { |
22 | let servers: ServerInfo[] | 27 | let servers: ServerInfo[] |
23 | let userAccessToken = null | 28 | let userAccessToken = null |
24 | let videoIdLocal: number | 29 | let videoIdLocal: number |
25 | let videoIdRemote: number | 30 | let videoRemote: VideoCreateResult |
26 | 31 | ||
27 | // --------------------------------------------------------------- | 32 | // --------------------------------------------------------------- |
28 | 33 | ||
@@ -48,7 +53,8 @@ describe('Test server redundancy API validators', function () { | |||
48 | 53 | ||
49 | await waitJobs(servers) | 54 | await waitJobs(servers) |
50 | 55 | ||
51 | videoIdRemote = await getVideoIdFromUUID(servers[0].url, remoteUUID) | 56 | const resVideo = await getVideo(servers[0].url, remoteUUID) |
57 | videoRemote = resVideo.body | ||
52 | }) | 58 | }) |
53 | 59 | ||
54 | describe('When listing redundancies', function () { | 60 | describe('When listing redundancies', function () { |
@@ -131,7 +137,13 @@ describe('Test server redundancy API validators', function () { | |||
131 | }) | 137 | }) |
132 | 138 | ||
133 | it('Should succeed with the correct params', async function () { | 139 | it('Should succeed with the correct params', async function () { |
134 | await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdRemote }, statusCodeExpected: HttpStatusCode.NO_CONTENT_204 }) | 140 | await makePostBodyRequest({ |
141 | url, | ||
142 | path, | ||
143 | token, | ||
144 | fields: { videoId: videoRemote.shortUUID }, | ||
145 | statusCodeExpected: HttpStatusCode.NO_CONTENT_204 | ||
146 | }) | ||
135 | }) | 147 | }) |
136 | 148 | ||
137 | it('Should fail if the video is already duplicated', async function () { | 149 | it('Should fail if the video is already duplicated', async function () { |
@@ -139,7 +151,13 @@ describe('Test server redundancy API validators', function () { | |||
139 | 151 | ||
140 | await waitJobs(servers) | 152 | await waitJobs(servers) |
141 | 153 | ||
142 | await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdRemote }, statusCodeExpected: HttpStatusCode.CONFLICT_409 }) | 154 | await makePostBodyRequest({ |
155 | url, | ||
156 | path, | ||
157 | token, | ||
158 | fields: { videoId: videoRemote.uuid }, | ||
159 | statusCodeExpected: HttpStatusCode.CONFLICT_409 | ||
160 | }) | ||
143 | }) | 161 | }) |
144 | }) | 162 | }) |
145 | 163 | ||
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts index 8378c3a89..20ad46cff 100644 --- a/server/tests/api/check-params/search.ts +++ b/server/tests/api/check-params/search.ts | |||
@@ -140,6 +140,30 @@ describe('Test videos API validator', function () { | |||
140 | }) | 140 | }) |
141 | }) | 141 | }) |
142 | 142 | ||
143 | describe('When searching video playlists', function () { | ||
144 | const path = '/api/v1/search/video-playlists/' | ||
145 | |||
146 | const query = { | ||
147 | search: 'coucou' | ||
148 | } | ||
149 | |||
150 | it('Should fail with a bad start pagination', async function () { | ||
151 | await checkBadStartPagination(server.url, path, null, query) | ||
152 | }) | ||
153 | |||
154 | it('Should fail with a bad count pagination', async function () { | ||
155 | await checkBadCountPagination(server.url, path, null, query) | ||
156 | }) | ||
157 | |||
158 | it('Should fail with an incorrect sort', async function () { | ||
159 | await checkBadSortPagination(server.url, path, null, query) | ||
160 | }) | ||
161 | |||
162 | it('Should success with the correct parameters', async function () { | ||
163 | await makeGetRequest({ url: server.url, path, query, statusCodeExpected: HttpStatusCode.OK_200 }) | ||
164 | }) | ||
165 | }) | ||
166 | |||
143 | describe('When searching video channels', function () { | 167 | describe('When searching video channels', function () { |
144 | const path = '/api/v1/search/video-channels/' | 168 | const path = '/api/v1/search/video-channels/' |
145 | 169 | ||
@@ -171,6 +195,7 @@ describe('Test videos API validator', function () { | |||
171 | 195 | ||
172 | const query = { search: 'coucou' } | 196 | const query = { search: 'coucou' } |
173 | const paths = [ | 197 | const paths = [ |
198 | '/api/v1/search/video-playlists/', | ||
174 | '/api/v1/search/video-channels/', | 199 | '/api/v1/search/video-channels/', |
175 | '/api/v1/search/videos/' | 200 | '/api/v1/search/videos/' |
176 | ] | 201 | ] |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index dcff0d52b..70a872ce5 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -2,12 +2,12 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
5 | import { join } from 'path' | 5 | import { User, UserRole, VideoCreateResult } from '../../../../shared' |
6 | import { User, UserRole } from '../../../../shared' | ||
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
8 | import { | 7 | import { |
9 | addVideoChannel, | 8 | addVideoChannel, |
10 | blockUser, | 9 | blockUser, |
10 | buildAbsoluteFixturePath, | ||
11 | cleanupTests, | 11 | cleanupTests, |
12 | createUser, | 12 | createUser, |
13 | deleteMe, | 13 | deleteMe, |
@@ -45,7 +45,7 @@ describe('Test users API validators', function () { | |||
45 | let userId: number | 45 | let userId: number |
46 | let rootId: number | 46 | let rootId: number |
47 | let moderatorId: number | 47 | let moderatorId: number |
48 | let videoId: number | 48 | let video: VideoCreateResult |
49 | let server: ServerInfo | 49 | let server: ServerInfo |
50 | let serverWithRegistrationDisabled: ServerInfo | 50 | let serverWithRegistrationDisabled: ServerInfo |
51 | let userAccessToken = '' | 51 | let userAccessToken = '' |
@@ -126,7 +126,7 @@ describe('Test users API validators', function () { | |||
126 | 126 | ||
127 | { | 127 | { |
128 | const res = await uploadVideo(server.url, server.accessToken, {}) | 128 | const res = await uploadVideo(server.url, server.accessToken, {}) |
129 | videoId = res.body.video.id | 129 | video = res.body.video |
130 | } | 130 | } |
131 | 131 | ||
132 | { | 132 | { |
@@ -600,7 +600,7 @@ describe('Test users API validators', function () { | |||
600 | it('Should fail without an incorrect input file', async function () { | 600 | it('Should fail without an incorrect input file', async function () { |
601 | const fields = {} | 601 | const fields = {} |
602 | const attaches = { | 602 | const attaches = { |
603 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 603 | avatarfile: buildAbsoluteFixturePath('video_short.mp4') |
604 | } | 604 | } |
605 | await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) | 605 | await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) |
606 | }) | 606 | }) |
@@ -608,7 +608,7 @@ describe('Test users API validators', function () { | |||
608 | it('Should fail with a big file', async function () { | 608 | it('Should fail with a big file', async function () { |
609 | const fields = {} | 609 | const fields = {} |
610 | const attaches = { | 610 | const attaches = { |
611 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | 611 | avatarfile: buildAbsoluteFixturePath('avatar-big.png') |
612 | } | 612 | } |
613 | await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) | 613 | await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) |
614 | }) | 614 | }) |
@@ -616,7 +616,7 @@ describe('Test users API validators', function () { | |||
616 | it('Should fail with an unauthenticated user', async function () { | 616 | it('Should fail with an unauthenticated user', async function () { |
617 | const fields = {} | 617 | const fields = {} |
618 | const attaches = { | 618 | const attaches = { |
619 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 619 | avatarfile: buildAbsoluteFixturePath('avatar.png') |
620 | } | 620 | } |
621 | await makeUploadRequest({ | 621 | await makeUploadRequest({ |
622 | url: server.url, | 622 | url: server.url, |
@@ -630,7 +630,7 @@ describe('Test users API validators', function () { | |||
630 | it('Should succeed with the correct params', async function () { | 630 | it('Should succeed with the correct params', async function () { |
631 | const fields = {} | 631 | const fields = {} |
632 | const attaches = { | 632 | const attaches = { |
633 | avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 633 | avatarfile: buildAbsoluteFixturePath('avatar.png') |
634 | } | 634 | } |
635 | await makeUploadRequest({ | 635 | await makeUploadRequest({ |
636 | url: server.url, | 636 | url: server.url, |
@@ -829,7 +829,7 @@ describe('Test users API validators', function () { | |||
829 | 829 | ||
830 | describe('When getting my video rating', function () { | 830 | describe('When getting my video rating', function () { |
831 | it('Should fail with a non authenticated user', async function () { | 831 | it('Should fail with a non authenticated user', async function () { |
832 | await getMyUserVideoRating(server.url, 'fake_token', videoId, HttpStatusCode.UNAUTHORIZED_401) | 832 | await getMyUserVideoRating(server.url, 'fake_token', video.id, HttpStatusCode.UNAUTHORIZED_401) |
833 | }) | 833 | }) |
834 | 834 | ||
835 | it('Should fail with an incorrect video uuid', async function () { | 835 | it('Should fail with an incorrect video uuid', async function () { |
@@ -841,7 +841,9 @@ describe('Test users API validators', function () { | |||
841 | }) | 841 | }) |
842 | 842 | ||
843 | it('Should succeed with the correct parameters', async function () { | 843 | it('Should succeed with the correct parameters', async function () { |
844 | await getMyUserVideoRating(server.url, server.accessToken, videoId) | 844 | await getMyUserVideoRating(server.url, server.accessToken, video.id) |
845 | await getMyUserVideoRating(server.url, server.accessToken, video.uuid) | ||
846 | await getMyUserVideoRating(server.url, server.accessToken, video.shortUUID) | ||
845 | }) | 847 | }) |
846 | }) | 848 | }) |
847 | 849 | ||
diff --git a/server/tests/api/check-params/video-blacklist.ts b/server/tests/api/check-params/video-blacklist.ts index 3d4837d58..ce7f5fa17 100644 --- a/server/tests/api/check-params/video-blacklist.ts +++ b/server/tests/api/check-params/video-blacklist.ts | |||
@@ -191,7 +191,7 @@ describe('Test video blacklist API validators', function () { | |||
191 | }) | 191 | }) |
192 | 192 | ||
193 | it('Should succeed with the correct params', async function () { | 193 | it('Should succeed with the correct params', async function () { |
194 | const path = basePath + servers[0].video.uuid + '/blacklist' | 194 | const path = basePath + servers[0].video.shortUUID + '/blacklist' |
195 | const fields = { reason: 'hello' } | 195 | const fields = { reason: 'hello' } |
196 | 196 | ||
197 | await makePutBodyRequest({ | 197 | await makePutBodyRequest({ |
@@ -222,10 +222,14 @@ describe('Test video blacklist API validators', function () { | |||
222 | }) | 222 | }) |
223 | 223 | ||
224 | it('Should succeed with an admin', async function () { | 224 | it('Should succeed with an admin', async function () { |
225 | const res = await getVideoWithToken(servers[0].url, servers[0].accessToken, servers[0].video.uuid, HttpStatusCode.OK_200) | 225 | const video = servers[0].video |
226 | const video: VideoDetails = res.body | ||
227 | 226 | ||
228 | expect(video.blacklisted).to.be.true | 227 | for (const id of [ video.id, video.uuid, video.shortUUID ]) { |
228 | const res = await getVideoWithToken(servers[0].url, servers[0].accessToken, id, HttpStatusCode.OK_200) | ||
229 | const video: VideoDetails = res.body | ||
230 | |||
231 | expect(video.blacklisted).to.be.true | ||
232 | } | ||
229 | }) | 233 | }) |
230 | }) | 234 | }) |
231 | 235 | ||
diff --git a/server/tests/api/check-params/video-captions.ts b/server/tests/api/check-params/video-captions.ts index 2f049c03d..c0595c04d 100644 --- a/server/tests/api/check-params/video-captions.ts +++ b/server/tests/api/check-params/video-captions.ts | |||
@@ -1,7 +1,10 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { VideoCreateResult } from '@shared/models' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
4 | import { | 6 | import { |
7 | buildAbsoluteFixturePath, | ||
5 | cleanupTests, | 8 | cleanupTests, |
6 | createUser, | 9 | createUser, |
7 | flushAndRunServer, | 10 | flushAndRunServer, |
@@ -13,16 +16,14 @@ import { | |||
13 | uploadVideo, | 16 | uploadVideo, |
14 | userLogin | 17 | userLogin |
15 | } from '../../../../shared/extra-utils' | 18 | } from '../../../../shared/extra-utils' |
16 | import { join } from 'path' | ||
17 | import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions' | 19 | import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions' |
18 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
19 | 20 | ||
20 | describe('Test video captions API validator', function () { | 21 | describe('Test video captions API validator', function () { |
21 | const path = '/api/v1/videos/' | 22 | const path = '/api/v1/videos/' |
22 | 23 | ||
23 | let server: ServerInfo | 24 | let server: ServerInfo |
24 | let userAccessToken: string | 25 | let userAccessToken: string |
25 | let videoUUID: string | 26 | let video: VideoCreateResult |
26 | 27 | ||
27 | // --------------------------------------------------------------- | 28 | // --------------------------------------------------------------- |
28 | 29 | ||
@@ -35,7 +36,7 @@ describe('Test video captions API validator', function () { | |||
35 | 36 | ||
36 | { | 37 | { |
37 | const res = await uploadVideo(server.url, server.accessToken, {}) | 38 | const res = await uploadVideo(server.url, server.accessToken, {}) |
38 | videoUUID = res.body.video.uuid | 39 | video = res.body.video |
39 | } | 40 | } |
40 | 41 | ||
41 | { | 42 | { |
@@ -51,7 +52,7 @@ describe('Test video captions API validator', function () { | |||
51 | describe('When adding video caption', function () { | 52 | describe('When adding video caption', function () { |
52 | const fields = { } | 53 | const fields = { } |
53 | const attaches = { | 54 | const attaches = { |
54 | captionfile: join(__dirname, '..', '..', 'fixtures', 'subtitle-good1.vtt') | 55 | captionfile: buildAbsoluteFixturePath('subtitle-good1.vtt') |
55 | } | 56 | } |
56 | 57 | ||
57 | it('Should fail without a valid uuid', async function () { | 58 | it('Should fail without a valid uuid', async function () { |
@@ -78,7 +79,7 @@ describe('Test video captions API validator', function () { | |||
78 | }) | 79 | }) |
79 | 80 | ||
80 | it('Should fail with a missing language in path', async function () { | 81 | it('Should fail with a missing language in path', async function () { |
81 | const captionPath = path + videoUUID + '/captions' | 82 | const captionPath = path + video.uuid + '/captions' |
82 | await makeUploadRequest({ | 83 | await makeUploadRequest({ |
83 | method: 'PUT', | 84 | method: 'PUT', |
84 | url: server.url, | 85 | url: server.url, |
@@ -90,7 +91,7 @@ describe('Test video captions API validator', function () { | |||
90 | }) | 91 | }) |
91 | 92 | ||
92 | it('Should fail with an unknown language', async function () { | 93 | it('Should fail with an unknown language', async function () { |
93 | const captionPath = path + videoUUID + '/captions/15' | 94 | const captionPath = path + video.uuid + '/captions/15' |
94 | await makeUploadRequest({ | 95 | await makeUploadRequest({ |
95 | method: 'PUT', | 96 | method: 'PUT', |
96 | url: server.url, | 97 | url: server.url, |
@@ -102,7 +103,7 @@ describe('Test video captions API validator', function () { | |||
102 | }) | 103 | }) |
103 | 104 | ||
104 | it('Should fail without access token', async function () { | 105 | it('Should fail without access token', async function () { |
105 | const captionPath = path + videoUUID + '/captions/fr' | 106 | const captionPath = path + video.uuid + '/captions/fr' |
106 | await makeUploadRequest({ | 107 | await makeUploadRequest({ |
107 | method: 'PUT', | 108 | method: 'PUT', |
108 | url: server.url, | 109 | url: server.url, |
@@ -114,7 +115,7 @@ describe('Test video captions API validator', function () { | |||
114 | }) | 115 | }) |
115 | 116 | ||
116 | it('Should fail with a bad access token', async function () { | 117 | it('Should fail with a bad access token', async function () { |
117 | const captionPath = path + videoUUID + '/captions/fr' | 118 | const captionPath = path + video.uuid + '/captions/fr' |
118 | await makeUploadRequest({ | 119 | await makeUploadRequest({ |
119 | method: 'PUT', | 120 | method: 'PUT', |
120 | url: server.url, | 121 | url: server.url, |
@@ -129,10 +130,10 @@ describe('Test video captions API validator', function () { | |||
129 | // We accept any file now | 130 | // We accept any file now |
130 | // it('Should fail with an invalid captionfile extension', async function () { | 131 | // it('Should fail with an invalid captionfile extension', async function () { |
131 | // const attaches = { | 132 | // const attaches = { |
132 | // 'captionfile': join(__dirname, '..', '..', 'fixtures', 'subtitle-bad.txt') | 133 | // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.txt') |
133 | // } | 134 | // } |
134 | // | 135 | // |
135 | // const captionPath = path + videoUUID + '/captions/fr' | 136 | // const captionPath = path + video.uuid + '/captions/fr' |
136 | // await makeUploadRequest({ | 137 | // await makeUploadRequest({ |
137 | // method: 'PUT', | 138 | // method: 'PUT', |
138 | // url: server.url, | 139 | // url: server.url, |
@@ -150,7 +151,7 @@ describe('Test video captions API validator', function () { | |||
150 | // url: server.url, | 151 | // url: server.url, |
151 | // accessToken: server.accessToken, | 152 | // accessToken: server.accessToken, |
152 | // language: 'zh', | 153 | // language: 'zh', |
153 | // videoId: videoUUID, | 154 | // videoId: video.uuid, |
154 | // fixture: 'subtitle-bad.txt', | 155 | // fixture: 'subtitle-bad.txt', |
155 | // mimeType: 'application/octet-stream', | 156 | // mimeType: 'application/octet-stream', |
156 | // statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 | 157 | // statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 |
@@ -162,7 +163,7 @@ describe('Test video captions API validator', function () { | |||
162 | url: server.url, | 163 | url: server.url, |
163 | accessToken: server.accessToken, | 164 | accessToken: server.accessToken, |
164 | language: 'zh', | 165 | language: 'zh', |
165 | videoId: videoUUID, | 166 | videoId: video.uuid, |
166 | fixture: 'subtitle-good.srt', | 167 | fixture: 'subtitle-good.srt', |
167 | mimeType: 'application/octet-stream' | 168 | mimeType: 'application/octet-stream' |
168 | }) | 169 | }) |
@@ -171,10 +172,10 @@ describe('Test video captions API validator', function () { | |||
171 | // We don't check the file validity yet | 172 | // We don't check the file validity yet |
172 | // it('Should fail with an invalid captionfile srt', async function () { | 173 | // it('Should fail with an invalid captionfile srt', async function () { |
173 | // const attaches = { | 174 | // const attaches = { |
174 | // 'captionfile': join(__dirname, '..', '..', 'fixtures', 'subtitle-bad.srt') | 175 | // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.srt') |
175 | // } | 176 | // } |
176 | // | 177 | // |
177 | // const captionPath = path + videoUUID + '/captions/fr' | 178 | // const captionPath = path + video.uuid + '/captions/fr' |
178 | // await makeUploadRequest({ | 179 | // await makeUploadRequest({ |
179 | // method: 'PUT', | 180 | // method: 'PUT', |
180 | // url: server.url, | 181 | // url: server.url, |
@@ -187,7 +188,7 @@ describe('Test video captions API validator', function () { | |||
187 | // }) | 188 | // }) |
188 | 189 | ||
189 | it('Should success with the correct parameters', async function () { | 190 | it('Should success with the correct parameters', async function () { |
190 | const captionPath = path + videoUUID + '/captions/fr' | 191 | const captionPath = path + video.uuid + '/captions/fr' |
191 | await makeUploadRequest({ | 192 | await makeUploadRequest({ |
192 | method: 'PUT', | 193 | method: 'PUT', |
193 | url: server.url, | 194 | url: server.url, |
@@ -214,7 +215,7 @@ describe('Test video captions API validator', function () { | |||
214 | }) | 215 | }) |
215 | 216 | ||
216 | it('Should success with the correct parameters', async function () { | 217 | it('Should success with the correct parameters', async function () { |
217 | await makeGetRequest({ url: server.url, path: path + videoUUID + '/captions', statusCodeExpected: HttpStatusCode.OK_200 }) | 218 | await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', statusCodeExpected: HttpStatusCode.OK_200 }) |
218 | }) | 219 | }) |
219 | }) | 220 | }) |
220 | 221 | ||
@@ -245,27 +246,27 @@ describe('Test video captions API validator', function () { | |||
245 | }) | 246 | }) |
246 | 247 | ||
247 | it('Should fail with a missing language', async function () { | 248 | it('Should fail with a missing language', async function () { |
248 | const captionPath = path + videoUUID + '/captions' | 249 | const captionPath = path + video.shortUUID + '/captions' |
249 | await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) | 250 | await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) |
250 | }) | 251 | }) |
251 | 252 | ||
252 | it('Should fail with an unknown language', async function () { | 253 | it('Should fail with an unknown language', async function () { |
253 | const captionPath = path + videoUUID + '/captions/15' | 254 | const captionPath = path + video.shortUUID + '/captions/15' |
254 | await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) | 255 | await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) |
255 | }) | 256 | }) |
256 | 257 | ||
257 | it('Should fail without access token', async function () { | 258 | it('Should fail without access token', async function () { |
258 | const captionPath = path + videoUUID + '/captions/fr' | 259 | const captionPath = path + video.shortUUID + '/captions/fr' |
259 | await makeDeleteRequest({ url: server.url, path: captionPath, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 }) | 260 | await makeDeleteRequest({ url: server.url, path: captionPath, statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 }) |
260 | }) | 261 | }) |
261 | 262 | ||
262 | it('Should fail with a bad access token', async function () { | 263 | it('Should fail with a bad access token', async function () { |
263 | const captionPath = path + videoUUID + '/captions/fr' | 264 | const captionPath = path + video.shortUUID + '/captions/fr' |
264 | await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 }) | 265 | await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401 }) |
265 | }) | 266 | }) |
266 | 267 | ||
267 | it('Should fail with another user', async function () { | 268 | it('Should fail with another user', async function () { |
268 | const captionPath = path + videoUUID + '/captions/fr' | 269 | const captionPath = path + video.shortUUID + '/captions/fr' |
269 | await makeDeleteRequest({ | 270 | await makeDeleteRequest({ |
270 | url: server.url, | 271 | url: server.url, |
271 | path: captionPath, | 272 | path: captionPath, |
@@ -275,7 +276,7 @@ describe('Test video captions API validator', function () { | |||
275 | }) | 276 | }) |
276 | 277 | ||
277 | it('Should success with the correct parameters', async function () { | 278 | it('Should success with the correct parameters', async function () { |
278 | const captionPath = path + videoUUID + '/captions/fr' | 279 | const captionPath = path + video.shortUUID + '/captions/fr' |
279 | await makeDeleteRequest({ | 280 | await makeDeleteRequest({ |
280 | url: server.url, | 281 | url: server.url, |
281 | path: captionPath, | 282 | path: captionPath, |
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index bc2e6192e..5c02afd31 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts | |||
@@ -1,9 +1,11 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | ||
3 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
4 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
5 | import 'mocha' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
6 | import { | 7 | import { |
8 | buildAbsoluteFixturePath, | ||
7 | cleanupTests, | 9 | cleanupTests, |
8 | createUser, | 10 | createUser, |
9 | deleteVideoChannel, | 11 | deleteVideoChannel, |
@@ -23,9 +25,7 @@ import { | |||
23 | checkBadSortPagination, | 25 | checkBadSortPagination, |
24 | checkBadStartPagination | 26 | checkBadStartPagination |
25 | } from '../../../../shared/extra-utils/requests/check-api-params' | 27 | } from '../../../../shared/extra-utils/requests/check-api-params' |
26 | import { join } from 'path' | ||
27 | import { VideoChannelUpdate } from '../../../../shared/models/videos' | 28 | import { VideoChannelUpdate } from '../../../../shared/models/videos' |
28 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
29 | 29 | ||
30 | const expect = chai.expect | 30 | const expect = chai.expect |
31 | 31 | ||
@@ -246,7 +246,7 @@ describe('Test video channels API validator', function () { | |||
246 | for (const type of types) { | 246 | for (const type of types) { |
247 | const fields = {} | 247 | const fields = {} |
248 | const attaches = { | 248 | const attaches = { |
249 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 249 | [type + 'file']: buildAbsoluteFixturePath('video_short.mp4') |
250 | } | 250 | } |
251 | 251 | ||
252 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | 252 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) |
@@ -257,7 +257,7 @@ describe('Test video channels API validator', function () { | |||
257 | for (const type of types) { | 257 | for (const type of types) { |
258 | const fields = {} | 258 | const fields = {} |
259 | const attaches = { | 259 | const attaches = { |
260 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | 260 | [type + 'file']: buildAbsoluteFixturePath('avatar-big.png') |
261 | } | 261 | } |
262 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) | 262 | await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) |
263 | } | 263 | } |
@@ -267,7 +267,7 @@ describe('Test video channels API validator', function () { | |||
267 | for (const type of types) { | 267 | for (const type of types) { |
268 | const fields = {} | 268 | const fields = {} |
269 | const attaches = { | 269 | const attaches = { |
270 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 270 | [type + 'file']: buildAbsoluteFixturePath('avatar.png') |
271 | } | 271 | } |
272 | await makeUploadRequest({ | 272 | await makeUploadRequest({ |
273 | url: server.url, | 273 | url: server.url, |
@@ -283,7 +283,7 @@ describe('Test video channels API validator', function () { | |||
283 | for (const type of types) { | 283 | for (const type of types) { |
284 | const fields = {} | 284 | const fields = {} |
285 | const attaches = { | 285 | const attaches = { |
286 | [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png') | 286 | [type + 'file']: buildAbsoluteFixturePath('avatar.png') |
287 | } | 287 | } |
288 | await makeUploadRequest({ | 288 | await makeUploadRequest({ |
289 | url: server.url, | 289 | url: server.url, |
diff --git a/server/tests/api/check-params/video-comments.ts b/server/tests/api/check-params/video-comments.ts index 659a10c41..a38420851 100644 --- a/server/tests/api/check-params/video-comments.ts +++ b/server/tests/api/check-params/video-comments.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { VideoCreateResult } from '@shared/models' | ||
6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { | 7 | import { |
6 | cleanupTests, | 8 | cleanupTests, |
7 | createUser, | 9 | createUser, |
@@ -20,7 +22,6 @@ import { | |||
20 | checkBadStartPagination | 22 | checkBadStartPagination |
21 | } from '../../../../shared/extra-utils/requests/check-api-params' | 23 | } from '../../../../shared/extra-utils/requests/check-api-params' |
22 | import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' | 24 | import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' |
23 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
24 | 25 | ||
25 | const expect = chai.expect | 26 | const expect = chai.expect |
26 | 27 | ||
@@ -28,7 +29,7 @@ describe('Test video comments API validator', function () { | |||
28 | let pathThread: string | 29 | let pathThread: string |
29 | let pathComment: string | 30 | let pathComment: string |
30 | let server: ServerInfo | 31 | let server: ServerInfo |
31 | let videoUUID: string | 32 | let video: VideoCreateResult |
32 | let userAccessToken: string | 33 | let userAccessToken: string |
33 | let userAccessToken2: string | 34 | let userAccessToken2: string |
34 | let commentId: number | 35 | let commentId: number |
@@ -44,14 +45,14 @@ describe('Test video comments API validator', function () { | |||
44 | 45 | ||
45 | { | 46 | { |
46 | const res = await uploadVideo(server.url, server.accessToken, {}) | 47 | const res = await uploadVideo(server.url, server.accessToken, {}) |
47 | videoUUID = res.body.video.uuid | 48 | video = res.body.video |
48 | pathThread = '/api/v1/videos/' + videoUUID + '/comment-threads' | 49 | pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads' |
49 | } | 50 | } |
50 | 51 | ||
51 | { | 52 | { |
52 | const res = await addVideoCommentThread(server.url, server.accessToken, videoUUID, 'coucou') | 53 | const res = await addVideoCommentThread(server.url, server.accessToken, video.uuid, 'coucou') |
53 | commentId = res.body.comment.id | 54 | commentId = res.body.comment.id |
54 | pathComment = '/api/v1/videos/' + videoUUID + '/comments/' + commentId | 55 | pathComment = '/api/v1/videos/' + video.uuid + '/comments/' + commentId |
55 | } | 56 | } |
56 | 57 | ||
57 | { | 58 | { |
@@ -101,7 +102,7 @@ describe('Test video comments API validator', function () { | |||
101 | it('Should fail with an incorrect thread id', async function () { | 102 | it('Should fail with an incorrect thread id', async function () { |
102 | await makeGetRequest({ | 103 | await makeGetRequest({ |
103 | url: server.url, | 104 | url: server.url, |
104 | path: '/api/v1/videos/' + videoUUID + '/comment-threads/156', | 105 | path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/156', |
105 | statusCodeExpected: HttpStatusCode.NOT_FOUND_404 | 106 | statusCodeExpected: HttpStatusCode.NOT_FOUND_404 |
106 | }) | 107 | }) |
107 | }) | 108 | }) |
@@ -109,7 +110,7 @@ describe('Test video comments API validator', function () { | |||
109 | it('Should success with the correct params', async function () { | 110 | it('Should success with the correct params', async function () { |
110 | await makeGetRequest({ | 111 | await makeGetRequest({ |
111 | url: server.url, | 112 | url: server.url, |
112 | path: '/api/v1/videos/' + videoUUID + '/comment-threads/' + commentId, | 113 | path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/' + commentId, |
113 | statusCodeExpected: HttpStatusCode.OK_200 | 114 | statusCodeExpected: HttpStatusCode.OK_200 |
114 | }) | 115 | }) |
115 | }) | 116 | }) |
@@ -225,7 +226,7 @@ describe('Test video comments API validator', function () { | |||
225 | }) | 226 | }) |
226 | 227 | ||
227 | it('Should fail with an incorrect comment', async function () { | 228 | it('Should fail with an incorrect comment', async function () { |
228 | const path = '/api/v1/videos/' + videoUUID + '/comments/124' | 229 | const path = '/api/v1/videos/' + video.uuid + '/comments/124' |
229 | const fields = { | 230 | const fields = { |
230 | text: 'super comment' | 231 | text: 'super comment' |
231 | } | 232 | } |
@@ -272,7 +273,7 @@ describe('Test video comments API validator', function () { | |||
272 | }) | 273 | }) |
273 | 274 | ||
274 | it('Should fail with an incorrect comment', async function () { | 275 | it('Should fail with an incorrect comment', async function () { |
275 | const path = '/api/v1/videos/' + videoUUID + '/comments/124' | 276 | const path = '/api/v1/videos/' + video.uuid + '/comments/124' |
276 | await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 }) | 277 | await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: HttpStatusCode.NOT_FOUND_404 }) |
277 | }) | 278 | }) |
278 | 279 | ||
@@ -280,11 +281,11 @@ describe('Test video comments API validator', function () { | |||
280 | let commentToDelete: number | 281 | let commentToDelete: number |
281 | 282 | ||
282 | { | 283 | { |
283 | const res = await addVideoCommentThread(server.url, userAccessToken, videoUUID, 'hello') | 284 | const res = await addVideoCommentThread(server.url, userAccessToken, video.uuid, 'hello') |
284 | commentToDelete = res.body.comment.id | 285 | commentToDelete = res.body.comment.id |
285 | } | 286 | } |
286 | 287 | ||
287 | const path = '/api/v1/videos/' + videoUUID + '/comments/' + commentToDelete | 288 | const path = '/api/v1/videos/' + video.uuid + '/comments/' + commentToDelete |
288 | 289 | ||
289 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 }) | 290 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, statusCodeExpected: HttpStatusCode.FORBIDDEN_403 }) |
290 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.NO_CONTENT_204 }) | 291 | await makeDeleteRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: HttpStatusCode.NO_CONTENT_204 }) |
@@ -323,8 +324,8 @@ describe('Test video comments API validator', function () { | |||
323 | describe('When a video has comments disabled', function () { | 324 | describe('When a video has comments disabled', function () { |
324 | before(async function () { | 325 | before(async function () { |
325 | const res = await uploadVideo(server.url, server.accessToken, { commentsEnabled: false }) | 326 | const res = await uploadVideo(server.url, server.accessToken, { commentsEnabled: false }) |
326 | videoUUID = res.body.video.uuid | 327 | video = res.body.video |
327 | pathThread = '/api/v1/videos/' + videoUUID + '/comment-threads' | 328 | pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads' |
328 | }) | 329 | }) |
329 | 330 | ||
330 | it('Should return an empty thread list', async function () { | 331 | it('Should return an empty thread list', async function () { |
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts index 6de6b40c8..a27b624d0 100644 --- a/server/tests/api/check-params/video-imports.ts +++ b/server/tests/api/check-params/video-imports.ts | |||
@@ -2,8 +2,9 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { omit } from 'lodash' | 4 | import { omit } from 'lodash' |
5 | import { join } from 'path' | 5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
6 | import { | 6 | import { |
7 | buildAbsoluteFixturePath, | ||
7 | cleanupTests, | 8 | cleanupTests, |
8 | createUser, | 9 | createUser, |
9 | flushAndRunServer, | 10 | flushAndRunServer, |
@@ -22,9 +23,8 @@ import { | |||
22 | checkBadSortPagination, | 23 | checkBadSortPagination, |
23 | checkBadStartPagination | 24 | checkBadStartPagination |
24 | } from '../../../../shared/extra-utils/requests/check-api-params' | 25 | } from '../../../../shared/extra-utils/requests/check-api-params' |
25 | import { getMagnetURI, getGoodVideoUrl } from '../../../../shared/extra-utils/videos/video-imports' | 26 | import { getGoodVideoUrl, getMagnetURI } from '../../../../shared/extra-utils/videos/video-imports' |
26 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' | 27 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' |
27 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
28 | 28 | ||
29 | describe('Test video imports API validator', function () { | 29 | describe('Test video imports API validator', function () { |
30 | const path = '/api/v1/videos/imports' | 30 | const path = '/api/v1/videos/imports' |
@@ -201,7 +201,7 @@ describe('Test video imports API validator', function () { | |||
201 | it('Should fail with an incorrect thumbnail file', async function () { | 201 | it('Should fail with an incorrect thumbnail file', async function () { |
202 | const fields = baseCorrectParams | 202 | const fields = baseCorrectParams |
203 | const attaches = { | 203 | const attaches = { |
204 | thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 204 | thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') |
205 | } | 205 | } |
206 | 206 | ||
207 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 207 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -210,7 +210,7 @@ describe('Test video imports API validator', function () { | |||
210 | it('Should fail with a big thumbnail file', async function () { | 210 | it('Should fail with a big thumbnail file', async function () { |
211 | const fields = baseCorrectParams | 211 | const fields = baseCorrectParams |
212 | const attaches = { | 212 | const attaches = { |
213 | thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'preview-big.png') | 213 | thumbnailfile: buildAbsoluteFixturePath('preview-big.png') |
214 | } | 214 | } |
215 | 215 | ||
216 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 216 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -219,7 +219,7 @@ describe('Test video imports API validator', function () { | |||
219 | it('Should fail with an incorrect preview file', async function () { | 219 | it('Should fail with an incorrect preview file', async function () { |
220 | const fields = baseCorrectParams | 220 | const fields = baseCorrectParams |
221 | const attaches = { | 221 | const attaches = { |
222 | previewfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') | 222 | previewfile: buildAbsoluteFixturePath('video_short.mp4') |
223 | } | 223 | } |
224 | 224 | ||
225 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 225 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -228,7 +228,7 @@ describe('Test video imports API validator', function () { | |||
228 | it('Should fail with a big preview file', async function () { | 228 | it('Should fail with a big preview file', async function () { |
229 | const fields = baseCorrectParams | 229 | const fields = baseCorrectParams |
230 | const attaches = { | 230 | const attaches = { |
231 | previewfile: join(__dirname, '..', '..', 'fixtures', 'preview-big.png') | 231 | previewfile: buildAbsoluteFixturePath('preview-big.png') |
232 | } | 232 | } |
233 | 233 | ||
234 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 234 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -237,7 +237,7 @@ describe('Test video imports API validator', function () { | |||
237 | it('Should fail with an invalid torrent file', async function () { | 237 | it('Should fail with an invalid torrent file', async function () { |
238 | const fields = omit(baseCorrectParams, 'targetUrl') | 238 | const fields = omit(baseCorrectParams, 'targetUrl') |
239 | const attaches = { | 239 | const attaches = { |
240 | torrentfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') | 240 | torrentfile: buildAbsoluteFixturePath('avatar-big.png') |
241 | } | 241 | } |
242 | 242 | ||
243 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) | 243 | await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) |
@@ -312,7 +312,7 @@ describe('Test video imports API validator', function () { | |||
312 | 312 | ||
313 | fields = omit(fields, 'magnetUri') | 313 | fields = omit(fields, 'magnetUri') |
314 | const attaches = { | 314 | const attaches = { |
315 | torrentfile: join(__dirname, '..', '..', 'fixtures', 'video-720p.torrent') | 315 | torrentfile: buildAbsoluteFixturePath('video-720p.torrent') |
316 | } | 316 | } |
317 | 317 | ||
318 | await makeUploadRequest({ | 318 | await makeUploadRequest({ |
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts index bbea88354..18253d11a 100644 --- a/server/tests/api/check-params/video-playlists.ts +++ b/server/tests/api/check-params/video-playlists.ts | |||
@@ -1,8 +1,13 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { VideoPlaylistCreateResult, VideoPlaylistPrivacy, VideoPlaylistType } from '@shared/models' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
4 | import { | 6 | import { |
5 | addVideoInPlaylist, | 7 | addVideoInPlaylist, |
8 | checkBadCountPagination, | ||
9 | checkBadSortPagination, | ||
10 | checkBadStartPagination, | ||
6 | cleanupTests, | 11 | cleanupTests, |
7 | createVideoPlaylist, | 12 | createVideoPlaylist, |
8 | deleteVideoPlaylist, | 13 | deleteVideoPlaylist, |
@@ -21,20 +26,14 @@ import { | |||
21 | updateVideoPlaylistElement, | 26 | updateVideoPlaylistElement, |
22 | uploadVideoAndGetId | 27 | uploadVideoAndGetId |
23 | } from '../../../../shared/extra-utils' | 28 | } from '../../../../shared/extra-utils' |
24 | import { | ||
25 | checkBadCountPagination, | ||
26 | checkBadSortPagination, | ||
27 | checkBadStartPagination | ||
28 | } from '../../../../shared/extra-utils/requests/check-api-params' | ||
29 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
30 | import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model' | ||
31 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
32 | 29 | ||
33 | describe('Test video playlists API validator', function () { | 30 | describe('Test video playlists API validator', function () { |
34 | let server: ServerInfo | 31 | let server: ServerInfo |
35 | let userAccessToken: string | 32 | let userAccessToken: string |
36 | let playlistUUID: string | 33 | |
34 | let playlist: VideoPlaylistCreateResult | ||
37 | let privatePlaylistUUID: string | 35 | let privatePlaylistUUID: string |
36 | |||
38 | let watchLaterPlaylistId: number | 37 | let watchLaterPlaylistId: number |
39 | let videoId: number | 38 | let videoId: number |
40 | let playlistElementId: number | 39 | let playlistElementId: number |
@@ -67,7 +66,7 @@ describe('Test video playlists API validator', function () { | |||
67 | videoChannelId: server.videoChannel.id | 66 | videoChannelId: server.videoChannel.id |
68 | } | 67 | } |
69 | }) | 68 | }) |
70 | playlistUUID = res.body.videoPlaylist.uuid | 69 | playlist = res.body.videoPlaylist |
71 | } | 70 | } |
72 | 71 | ||
73 | { | 72 | { |
@@ -150,15 +149,15 @@ describe('Test video playlists API validator', function () { | |||
150 | const path = '/api/v1/video-playlists/' | 149 | const path = '/api/v1/video-playlists/' |
151 | 150 | ||
152 | it('Should fail with a bad start pagination', async function () { | 151 | it('Should fail with a bad start pagination', async function () { |
153 | await checkBadStartPagination(server.url, path + playlistUUID + '/videos', server.accessToken) | 152 | await checkBadStartPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken) |
154 | }) | 153 | }) |
155 | 154 | ||
156 | it('Should fail with a bad count pagination', async function () { | 155 | it('Should fail with a bad count pagination', async function () { |
157 | await checkBadCountPagination(server.url, path + playlistUUID + '/videos', server.accessToken) | 156 | await checkBadCountPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken) |
158 | }) | 157 | }) |
159 | 158 | ||
160 | it('Should success with the correct parameters', async function () { | 159 | it('Should success with the correct parameters', async function () { |
161 | await makeGetRequest({ url: server.url, path: path + playlistUUID + '/videos', statusCodeExpected: HttpStatusCode.OK_200 }) | 160 | await makeGetRequest({ url: server.url, path: path + playlist.shortUUID + '/videos', statusCodeExpected: HttpStatusCode.OK_200 }) |
162 | }) | 161 | }) |
163 | }) | 162 | }) |
164 | 163 | ||
@@ -177,6 +176,7 @@ describe('Test video playlists API validator', function () { | |||
177 | token: server.accessToken, | 176 | token: server.accessToken, |
178 | playlistAttrs: { | 177 | playlistAttrs: { |
179 | displayName: 'super playlist', | 178 | displayName: 'super playlist', |
179 | videoChannelId: server.videoChannel.id, | ||
180 | privacy: VideoPlaylistPrivacy.UNLISTED | 180 | privacy: VideoPlaylistPrivacy.UNLISTED |
181 | } | 181 | } |
182 | }) | 182 | }) |
@@ -187,7 +187,7 @@ describe('Test video playlists API validator', function () { | |||
187 | }) | 187 | }) |
188 | 188 | ||
189 | it('Should succeed with the correct params', async function () { | 189 | it('Should succeed with the correct params', async function () { |
190 | await getVideoPlaylist(server.url, playlistUUID, HttpStatusCode.OK_200) | 190 | await getVideoPlaylist(server.url, playlist.uuid, HttpStatusCode.OK_200) |
191 | }) | 191 | }) |
192 | }) | 192 | }) |
193 | 193 | ||
@@ -213,7 +213,7 @@ describe('Test video playlists API validator', function () { | |||
213 | const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | 213 | const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) |
214 | 214 | ||
215 | await createVideoPlaylist(params) | 215 | await createVideoPlaylist(params) |
216 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 216 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
217 | }) | 217 | }) |
218 | 218 | ||
219 | it('Should fail without displayName', async function () { | 219 | it('Should fail without displayName', async function () { |
@@ -226,42 +226,42 @@ describe('Test video playlists API validator', function () { | |||
226 | const params = getBase({ displayName: 's'.repeat(300) }) | 226 | const params = getBase({ displayName: 's'.repeat(300) }) |
227 | 227 | ||
228 | await createVideoPlaylist(params) | 228 | await createVideoPlaylist(params) |
229 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 229 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
230 | }) | 230 | }) |
231 | 231 | ||
232 | it('Should fail with an incorrect description', async function () { | 232 | it('Should fail with an incorrect description', async function () { |
233 | const params = getBase({ description: 't' }) | 233 | const params = getBase({ description: 't' }) |
234 | 234 | ||
235 | await createVideoPlaylist(params) | 235 | await createVideoPlaylist(params) |
236 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 236 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
237 | }) | 237 | }) |
238 | 238 | ||
239 | it('Should fail with an incorrect privacy', async function () { | 239 | it('Should fail with an incorrect privacy', async function () { |
240 | const params = getBase({ privacy: 45 }) | 240 | const params = getBase({ privacy: 45 }) |
241 | 241 | ||
242 | await createVideoPlaylist(params) | 242 | await createVideoPlaylist(params) |
243 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 243 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
244 | }) | 244 | }) |
245 | 245 | ||
246 | it('Should fail with an unknown video channel id', async function () { | 246 | it('Should fail with an unknown video channel id', async function () { |
247 | const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }) | 247 | const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }) |
248 | 248 | ||
249 | await createVideoPlaylist(params) | 249 | await createVideoPlaylist(params) |
250 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 250 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
251 | }) | 251 | }) |
252 | 252 | ||
253 | it('Should fail with an incorrect thumbnail file', async function () { | 253 | it('Should fail with an incorrect thumbnail file', async function () { |
254 | const params = getBase({ thumbnailfile: 'video_short.mp4' }) | 254 | const params = getBase({ thumbnailfile: 'video_short.mp4' }) |
255 | 255 | ||
256 | await createVideoPlaylist(params) | 256 | await createVideoPlaylist(params) |
257 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 257 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
258 | }) | 258 | }) |
259 | 259 | ||
260 | it('Should fail with a thumbnail file too big', async function () { | 260 | it('Should fail with a thumbnail file too big', async function () { |
261 | const params = getBase({ thumbnailfile: 'preview-big.png' }) | 261 | const params = getBase({ thumbnailfile: 'preview-big.png' }) |
262 | 262 | ||
263 | await createVideoPlaylist(params) | 263 | await createVideoPlaylist(params) |
264 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 264 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
265 | }) | 265 | }) |
266 | 266 | ||
267 | it('Should fail to set "public" a playlist not assigned to a channel', async function () { | 267 | it('Should fail to set "public" a playlist not assigned to a channel', async function () { |
@@ -272,8 +272,8 @@ describe('Test video playlists API validator', function () { | |||
272 | await createVideoPlaylist(params) | 272 | await createVideoPlaylist(params) |
273 | await createVideoPlaylist(params2) | 273 | await createVideoPlaylist(params2) |
274 | await updateVideoPlaylist(getUpdate(params, privatePlaylistUUID)) | 274 | await updateVideoPlaylist(getUpdate(params, privatePlaylistUUID)) |
275 | await updateVideoPlaylist(getUpdate(params2, playlistUUID)) | 275 | await updateVideoPlaylist(getUpdate(params2, playlist.shortUUID)) |
276 | await updateVideoPlaylist(getUpdate(params3, playlistUUID)) | 276 | await updateVideoPlaylist(getUpdate(params3, playlist.shortUUID)) |
277 | }) | 277 | }) |
278 | 278 | ||
279 | it('Should fail with an unknown playlist to update', async function () { | 279 | it('Should fail with an unknown playlist to update', async function () { |
@@ -286,7 +286,7 @@ describe('Test video playlists API validator', function () { | |||
286 | it('Should fail to update a playlist of another user', async function () { | 286 | it('Should fail to update a playlist of another user', async function () { |
287 | await updateVideoPlaylist(getUpdate( | 287 | await updateVideoPlaylist(getUpdate( |
288 | getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }), | 288 | getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }), |
289 | playlistUUID | 289 | playlist.shortUUID |
290 | )) | 290 | )) |
291 | }) | 291 | }) |
292 | 292 | ||
@@ -305,7 +305,7 @@ describe('Test video playlists API validator', function () { | |||
305 | 305 | ||
306 | { | 306 | { |
307 | const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) | 307 | const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) |
308 | await updateVideoPlaylist(getUpdate(params, playlistUUID)) | 308 | await updateVideoPlaylist(getUpdate(params, playlist.shortUUID)) |
309 | } | 309 | } |
310 | }) | 310 | }) |
311 | }) | 311 | }) |
@@ -316,7 +316,7 @@ describe('Test video playlists API validator', function () { | |||
316 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, | 316 | expectedStatus: HttpStatusCode.BAD_REQUEST_400, |
317 | url: server.url, | 317 | url: server.url, |
318 | token: server.accessToken, | 318 | token: server.accessToken, |
319 | playlistId: playlistUUID, | 319 | playlistId: playlist.id, |
320 | elementAttrs: Object.assign({ | 320 | elementAttrs: Object.assign({ |
321 | videoId, | 321 | videoId, |
322 | startTimestamp: 2, | 322 | startTimestamp: 2, |
@@ -381,7 +381,7 @@ describe('Test video playlists API validator', function () { | |||
381 | stopTimestamp: 2 | 381 | stopTimestamp: 2 |
382 | }, elementAttrs), | 382 | }, elementAttrs), |
383 | playlistElementId, | 383 | playlistElementId, |
384 | playlistId: playlistUUID, | 384 | playlistId: playlist.id, |
385 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | 385 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 |
386 | }, wrapper) | 386 | }, wrapper) |
387 | } | 387 | } |
@@ -451,7 +451,7 @@ describe('Test video playlists API validator', function () { | |||
451 | return Object.assign({ | 451 | return Object.assign({ |
452 | url: server.url, | 452 | url: server.url, |
453 | token: server.accessToken, | 453 | token: server.accessToken, |
454 | playlistId: playlistUUID, | 454 | playlistId: playlist.shortUUID, |
455 | elementAttrs: Object.assign({ | 455 | elementAttrs: Object.assign({ |
456 | startPosition: 1, | 456 | startPosition: 1, |
457 | insertAfterPosition: 2, | 457 | insertAfterPosition: 2, |
@@ -469,7 +469,7 @@ describe('Test video playlists API validator', function () { | |||
469 | await addVideoInPlaylist({ | 469 | await addVideoInPlaylist({ |
470 | url: server.url, | 470 | url: server.url, |
471 | token: server.accessToken, | 471 | token: server.accessToken, |
472 | playlistId: playlistUUID, | 472 | playlistId: playlist.shortUUID, |
473 | elementAttrs: { videoId: id } | 473 | elementAttrs: { videoId: id } |
474 | }) | 474 | }) |
475 | } | 475 | } |
@@ -606,7 +606,7 @@ describe('Test video playlists API validator', function () { | |||
606 | url: server.url, | 606 | url: server.url, |
607 | token: server.accessToken, | 607 | token: server.accessToken, |
608 | playlistElementId, | 608 | playlistElementId, |
609 | playlistId: playlistUUID, | 609 | playlistId: playlist.uuid, |
610 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | 610 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 |
611 | }, wrapper) | 611 | }, wrapper) |
612 | } | 612 | } |
@@ -662,7 +662,7 @@ describe('Test video playlists API validator', function () { | |||
662 | }) | 662 | }) |
663 | 663 | ||
664 | it('Should fail with a playlist of another user', async function () { | 664 | it('Should fail with a playlist of another user', async function () { |
665 | await deleteVideoPlaylist(server.url, userAccessToken, playlistUUID, HttpStatusCode.FORBIDDEN_403) | 665 | await deleteVideoPlaylist(server.url, userAccessToken, playlist.uuid, HttpStatusCode.FORBIDDEN_403) |
666 | }) | 666 | }) |
667 | 667 | ||
668 | it('Should fail with the watch later playlist', async function () { | 668 | it('Should fail with the watch later playlist', async function () { |
@@ -670,7 +670,7 @@ describe('Test video playlists API validator', function () { | |||
670 | }) | 670 | }) |
671 | 671 | ||
672 | it('Should succeed with the correct params', async function () { | 672 | it('Should succeed with the correct params', async function () { |
673 | await deleteVideoPlaylist(server.url, server.accessToken, playlistUUID) | 673 | await deleteVideoPlaylist(server.url, server.accessToken, playlist.uuid) |
674 | }) | 674 | }) |
675 | }) | 675 | }) |
676 | 676 | ||
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts index 2391584a7..4d54a4fd0 100644 --- a/server/tests/api/check-params/videos-filter.ts +++ b/server/tests/api/check-params/videos-filter.ts | |||
@@ -35,7 +35,7 @@ async function testEndpoints (server: ServerInfo, token: string, filter: string, | |||
35 | } | 35 | } |
36 | } | 36 | } |
37 | 37 | ||
38 | describe('Test videos filters', function () { | 38 | describe('Test video filters validators', function () { |
39 | let server: ServerInfo | 39 | let server: ServerInfo |
40 | let userAccessToken: string | 40 | let userAccessToken: string |
41 | let moderatorAccessToken: string | 41 | let moderatorAccessToken: string |
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index c970c4a15..4d7a9a23b 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -4,6 +4,8 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import { join } from 'path' | 6 | import { join } from 'path' |
7 | import { randomInt } from '@shared/core-utils' | ||
8 | import { PeerTubeProblemDocument, VideoCreateResult } from '@shared/models' | ||
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 9 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
8 | import { | 10 | import { |
9 | checkUploadVideoParam, | 11 | checkUploadVideoParam, |
@@ -30,7 +32,6 @@ import { | |||
30 | checkBadStartPagination | 32 | checkBadStartPagination |
31 | } from '../../../../shared/extra-utils/requests/check-api-params' | 33 | } from '../../../../shared/extra-utils/requests/check-api-params' |
32 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' | 34 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' |
33 | import { randomInt } from '@shared/core-utils' | ||
34 | 35 | ||
35 | const expect = chai.expect | 36 | const expect = chai.expect |
36 | 37 | ||
@@ -41,7 +42,7 @@ describe('Test videos API validator', function () { | |||
41 | let accountName: string | 42 | let accountName: string |
42 | let channelId: number | 43 | let channelId: number |
43 | let channelName: string | 44 | let channelName: string |
44 | let videoId | 45 | let video: VideoCreateResult |
45 | 46 | ||
46 | // --------------------------------------------------------------- | 47 | // --------------------------------------------------------------- |
47 | 48 | ||
@@ -411,6 +412,31 @@ describe('Test videos API validator', function () { | |||
411 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) | 412 | await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode) |
412 | }) | 413 | }) |
413 | 414 | ||
415 | it('Should report the appropriate error', async function () { | ||
416 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) | ||
417 | const attaches = baseCorrectAttaches | ||
418 | |||
419 | const attributes = { ...fields, ...attaches } | ||
420 | const res = await checkUploadVideoParam(server.url, server.accessToken, attributes, HttpStatusCode.BAD_REQUEST_400, mode) | ||
421 | |||
422 | const error = res.body as PeerTubeProblemDocument | ||
423 | |||
424 | if (mode === 'legacy') { | ||
425 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy') | ||
426 | } else { | ||
427 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit') | ||
428 | } | ||
429 | |||
430 | expect(error.type).to.equal('about:blank') | ||
431 | expect(error.title).to.equal('Bad Request') | ||
432 | |||
433 | expect(error.detail).to.equal('Incorrect request parameters: language') | ||
434 | expect(error.error).to.equal('Incorrect request parameters: language') | ||
435 | |||
436 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
437 | expect(error['invalid-params'].language).to.exist | ||
438 | }) | ||
439 | |||
414 | it('Should succeed with the correct parameters', async function () { | 440 | it('Should succeed with the correct parameters', async function () { |
415 | this.timeout(10000) | 441 | this.timeout(10000) |
416 | 442 | ||
@@ -464,7 +490,7 @@ describe('Test videos API validator', function () { | |||
464 | 490 | ||
465 | before(async function () { | 491 | before(async function () { |
466 | const res = await getVideosList(server.url) | 492 | const res = await getVideosList(server.url) |
467 | videoId = res.body.data[0].uuid | 493 | video = res.body.data[0] |
468 | }) | 494 | }) |
469 | 495 | ||
470 | it('Should fail with nothing', async function () { | 496 | it('Should fail with nothing', async function () { |
@@ -492,79 +518,79 @@ describe('Test videos API validator', function () { | |||
492 | it('Should fail with a long name', async function () { | 518 | it('Should fail with a long name', async function () { |
493 | const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) | 519 | const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(65) }) |
494 | 520 | ||
495 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 521 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
496 | }) | 522 | }) |
497 | 523 | ||
498 | it('Should fail with a bad category', async function () { | 524 | it('Should fail with a bad category', async function () { |
499 | const fields = immutableAssign(baseCorrectParams, { category: 125 }) | 525 | const fields = immutableAssign(baseCorrectParams, { category: 125 }) |
500 | 526 | ||
501 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 527 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
502 | }) | 528 | }) |
503 | 529 | ||
504 | it('Should fail with a bad licence', async function () { | 530 | it('Should fail with a bad licence', async function () { |
505 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) | 531 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) |
506 | 532 | ||
507 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 533 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
508 | }) | 534 | }) |
509 | 535 | ||
510 | it('Should fail with a bad language', async function () { | 536 | it('Should fail with a bad language', async function () { |
511 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) | 537 | const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) }) |
512 | 538 | ||
513 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 539 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
514 | }) | 540 | }) |
515 | 541 | ||
516 | it('Should fail with a long description', async function () { | 542 | it('Should fail with a long description', async function () { |
517 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) | 543 | const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) |
518 | 544 | ||
519 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 545 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
520 | }) | 546 | }) |
521 | 547 | ||
522 | it('Should fail with a long support text', async function () { | 548 | it('Should fail with a long support text', async function () { |
523 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) | 549 | const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(201) }) |
524 | 550 | ||
525 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 551 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
526 | }) | 552 | }) |
527 | 553 | ||
528 | it('Should fail with a bad channel', async function () { | 554 | it('Should fail with a bad channel', async function () { |
529 | const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) | 555 | const fields = immutableAssign(baseCorrectParams, { channelId: 545454 }) |
530 | 556 | ||
531 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 557 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
532 | }) | 558 | }) |
533 | 559 | ||
534 | it('Should fail with too many tags', async function () { | 560 | it('Should fail with too many tags', async function () { |
535 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) | 561 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }) |
536 | 562 | ||
537 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 563 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
538 | }) | 564 | }) |
539 | 565 | ||
540 | it('Should fail with a tag length too low', async function () { | 566 | it('Should fail with a tag length too low', async function () { |
541 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) | 567 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 't' ] }) |
542 | 568 | ||
543 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 569 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
544 | }) | 570 | }) |
545 | 571 | ||
546 | it('Should fail with a tag length too big', async function () { | 572 | it('Should fail with a tag length too big', async function () { |
547 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) | 573 | const fields = immutableAssign(baseCorrectParams, { tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }) |
548 | 574 | ||
549 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 575 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
550 | }) | 576 | }) |
551 | 577 | ||
552 | it('Should fail with a bad schedule update (miss updateAt)', async function () { | 578 | it('Should fail with a bad schedule update (miss updateAt)', async function () { |
553 | const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }) | 579 | const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }) |
554 | 580 | ||
555 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 581 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
556 | }) | 582 | }) |
557 | 583 | ||
558 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { | 584 | it('Should fail with a bad schedule update (wrong updateAt)', async function () { |
559 | const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } }) | 585 | const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } }) |
560 | 586 | ||
561 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 587 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
562 | }) | 588 | }) |
563 | 589 | ||
564 | it('Should fail with a bad originally published at param', async function () { | 590 | it('Should fail with a bad originally published at param', async function () { |
565 | const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' }) | 591 | const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' }) |
566 | 592 | ||
567 | await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) | 593 | await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) |
568 | }) | 594 | }) |
569 | 595 | ||
570 | it('Should fail with an incorrect thumbnail file', async function () { | 596 | it('Should fail with an incorrect thumbnail file', async function () { |
@@ -576,7 +602,7 @@ describe('Test videos API validator', function () { | |||
576 | await makeUploadRequest({ | 602 | await makeUploadRequest({ |
577 | url: server.url, | 603 | url: server.url, |
578 | method: 'PUT', | 604 | method: 'PUT', |
579 | path: path + videoId, | 605 | path: path + video.shortUUID, |
580 | token: server.accessToken, | 606 | token: server.accessToken, |
581 | fields, | 607 | fields, |
582 | attaches | 608 | attaches |
@@ -592,7 +618,7 @@ describe('Test videos API validator', function () { | |||
592 | await makeUploadRequest({ | 618 | await makeUploadRequest({ |
593 | url: server.url, | 619 | url: server.url, |
594 | method: 'PUT', | 620 | method: 'PUT', |
595 | path: path + videoId, | 621 | path: path + video.shortUUID, |
596 | token: server.accessToken, | 622 | token: server.accessToken, |
597 | fields, | 623 | fields, |
598 | attaches | 624 | attaches |
@@ -608,7 +634,7 @@ describe('Test videos API validator', function () { | |||
608 | await makeUploadRequest({ | 634 | await makeUploadRequest({ |
609 | url: server.url, | 635 | url: server.url, |
610 | method: 'PUT', | 636 | method: 'PUT', |
611 | path: path + videoId, | 637 | path: path + video.shortUUID, |
612 | token: server.accessToken, | 638 | token: server.accessToken, |
613 | fields, | 639 | fields, |
614 | attaches | 640 | attaches |
@@ -624,7 +650,7 @@ describe('Test videos API validator', function () { | |||
624 | await makeUploadRequest({ | 650 | await makeUploadRequest({ |
625 | url: server.url, | 651 | url: server.url, |
626 | method: 'PUT', | 652 | method: 'PUT', |
627 | path: path + videoId, | 653 | path: path + video.shortUUID, |
628 | token: server.accessToken, | 654 | token: server.accessToken, |
629 | fields, | 655 | fields, |
630 | attaches | 656 | attaches |
@@ -636,7 +662,7 @@ describe('Test videos API validator', function () { | |||
636 | 662 | ||
637 | await makePutBodyRequest({ | 663 | await makePutBodyRequest({ |
638 | url: server.url, | 664 | url: server.url, |
639 | path: path + videoId, | 665 | path: path + video.shortUUID, |
640 | token: userAccessToken, | 666 | token: userAccessToken, |
641 | fields, | 667 | fields, |
642 | statusCodeExpected: HttpStatusCode.FORBIDDEN_403 | 668 | statusCodeExpected: HttpStatusCode.FORBIDDEN_403 |
@@ -645,12 +671,30 @@ describe('Test videos API validator', function () { | |||
645 | 671 | ||
646 | it('Should fail with a video of another server') | 672 | it('Should fail with a video of another server') |
647 | 673 | ||
674 | it('Shoud report the appropriate error', async function () { | ||
675 | const fields = immutableAssign(baseCorrectParams, { licence: 125 }) | ||
676 | |||
677 | const res = await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) | ||
678 | const error = res.body as PeerTubeProblemDocument | ||
679 | |||
680 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo') | ||
681 | |||
682 | expect(error.type).to.equal('about:blank') | ||
683 | expect(error.title).to.equal('Bad Request') | ||
684 | |||
685 | expect(error.detail).to.equal('Incorrect request parameters: licence') | ||
686 | expect(error.error).to.equal('Incorrect request parameters: licence') | ||
687 | |||
688 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
689 | expect(error['invalid-params'].licence).to.exist | ||
690 | }) | ||
691 | |||
648 | it('Should succeed with the correct parameters', async function () { | 692 | it('Should succeed with the correct parameters', async function () { |
649 | const fields = baseCorrectParams | 693 | const fields = baseCorrectParams |
650 | 694 | ||
651 | await makePutBodyRequest({ | 695 | await makePutBodyRequest({ |
652 | url: server.url, | 696 | url: server.url, |
653 | path: path + videoId, | 697 | path: path + video.shortUUID, |
654 | token: server.accessToken, | 698 | token: server.accessToken, |
655 | fields, | 699 | fields, |
656 | statusCodeExpected: HttpStatusCode.NO_CONTENT_204 | 700 | statusCodeExpected: HttpStatusCode.NO_CONTENT_204 |
@@ -678,8 +722,24 @@ describe('Test videos API validator', function () { | |||
678 | await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404) | 722 | await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404) |
679 | }) | 723 | }) |
680 | 724 | ||
725 | it('Shoud report the appropriate error', async function () { | ||
726 | const res = await getVideo(server.url, 'hi', HttpStatusCode.BAD_REQUEST_400) | ||
727 | const error = res.body as PeerTubeProblemDocument | ||
728 | |||
729 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo') | ||
730 | |||
731 | expect(error.type).to.equal('about:blank') | ||
732 | expect(error.title).to.equal('Bad Request') | ||
733 | |||
734 | expect(error.detail).to.equal('Incorrect request parameters: id') | ||
735 | expect(error.error).to.equal('Incorrect request parameters: id') | ||
736 | |||
737 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
738 | expect(error['invalid-params'].id).to.exist | ||
739 | }) | ||
740 | |||
681 | it('Should succeed with the correct parameters', async function () { | 741 | it('Should succeed with the correct parameters', async function () { |
682 | await getVideo(server.url, videoId) | 742 | await getVideo(server.url, video.shortUUID) |
683 | }) | 743 | }) |
684 | }) | 744 | }) |
685 | 745 | ||
@@ -750,13 +810,29 @@ describe('Test videos API validator', function () { | |||
750 | }) | 810 | }) |
751 | 811 | ||
752 | it('Should fail with a video of another user without the appropriate right', async function () { | 812 | it('Should fail with a video of another user without the appropriate right', async function () { |
753 | await removeVideo(server.url, userAccessToken, videoId, HttpStatusCode.FORBIDDEN_403) | 813 | await removeVideo(server.url, userAccessToken, video.uuid, HttpStatusCode.FORBIDDEN_403) |
754 | }) | 814 | }) |
755 | 815 | ||
756 | it('Should fail with a video of another server') | 816 | it('Should fail with a video of another server') |
757 | 817 | ||
818 | it('Shoud report the appropriate error', async function () { | ||
819 | const res = await removeVideo(server.url, server.accessToken, 'hello', HttpStatusCode.BAD_REQUEST_400) | ||
820 | const error = res.body as PeerTubeProblemDocument | ||
821 | |||
822 | expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo') | ||
823 | |||
824 | expect(error.type).to.equal('about:blank') | ||
825 | expect(error.title).to.equal('Bad Request') | ||
826 | |||
827 | expect(error.detail).to.equal('Incorrect request parameters: id') | ||
828 | expect(error.error).to.equal('Incorrect request parameters: id') | ||
829 | |||
830 | expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) | ||
831 | expect(error['invalid-params'].id).to.exist | ||
832 | }) | ||
833 | |||
758 | it('Should succeed with the correct parameters', async function () { | 834 | it('Should succeed with the correct parameters', async function () { |
759 | await removeVideo(server.url, server.accessToken, videoId) | 835 | await removeVideo(server.url, server.accessToken, video.uuid) |
760 | }) | 836 | }) |
761 | }) | 837 | }) |
762 | 838 | ||
diff --git a/server/tests/api/live/index.ts b/server/tests/api/live/index.ts index c733f564e..e6bcef49f 100644 --- a/server/tests/api/live/index.ts +++ b/server/tests/api/live/index.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import './live-constraints' | 1 | import './live-constraints' |
2 | import './live-socket-messages' | ||
2 | import './live-permanent' | 3 | import './live-permanent' |
3 | import './live-save-replay' | 4 | import './live-save-replay' |
5 | import './live-views' | ||
4 | import './live' | 6 | import './live' |
diff --git a/server/tests/api/live/live-permanent.ts b/server/tests/api/live/live-permanent.ts index d52e8c7e4..71b7d28a8 100644 --- a/server/tests/api/live/live-permanent.ts +++ b/server/tests/api/live/live-permanent.ts | |||
@@ -27,7 +27,7 @@ import { | |||
27 | 27 | ||
28 | const expect = chai.expect | 28 | const expect = chai.expect |
29 | 29 | ||
30 | describe('Permenant live', function () { | 30 | describe('Permanent live', function () { |
31 | let servers: ServerInfo[] = [] | 31 | let servers: ServerInfo[] = [] |
32 | let videoUUID: string | 32 | let videoUUID: string |
33 | 33 | ||
@@ -106,7 +106,7 @@ describe('Permenant live', function () { | |||
106 | }) | 106 | }) |
107 | 107 | ||
108 | it('Should stream into this permanent live', async function () { | 108 | it('Should stream into this permanent live', async function () { |
109 | this.timeout(60000) | 109 | this.timeout(120000) |
110 | 110 | ||
111 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, videoUUID) | 111 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, videoUUID) |
112 | 112 | ||
diff --git a/server/tests/api/live/live-socket-messages.ts b/server/tests/api/live/live-socket-messages.ts new file mode 100644 index 000000000..e00909ade --- /dev/null +++ b/server/tests/api/live/live-socket-messages.ts | |||
@@ -0,0 +1,196 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io' | ||
6 | import { VideoPrivacy, VideoState } from '@shared/models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createLive, | ||
10 | doubleFollow, | ||
11 | flushAndRunMultipleServers, | ||
12 | getVideoIdFromUUID, | ||
13 | sendRTMPStreamInVideo, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultVideoChannel, | ||
17 | stopFfmpeg, | ||
18 | updateCustomSubConfig, | ||
19 | viewVideo, | ||
20 | wait, | ||
21 | waitJobs, | ||
22 | waitUntilLiveEnded, | ||
23 | waitUntilLivePublishedOnAllServers | ||
24 | } from '../../../../shared/extra-utils' | ||
25 | |||
26 | const expect = chai.expect | ||
27 | |||
28 | describe('Test live', function () { | ||
29 | let servers: ServerInfo[] = [] | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(120000) | ||
33 | |||
34 | servers = await flushAndRunMultipleServers(2) | ||
35 | |||
36 | // Get the access tokens | ||
37 | await setAccessTokensToServers(servers) | ||
38 | await setDefaultVideoChannel(servers) | ||
39 | |||
40 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
41 | live: { | ||
42 | enabled: true, | ||
43 | allowReplay: true, | ||
44 | transcoding: { | ||
45 | enabled: false | ||
46 | } | ||
47 | } | ||
48 | }) | ||
49 | |||
50 | // Server 1 and server 2 follow each other | ||
51 | await doubleFollow(servers[0], servers[1]) | ||
52 | }) | ||
53 | |||
54 | describe('Live socket messages', function () { | ||
55 | |||
56 | async function createLiveWrapper () { | ||
57 | const liveAttributes = { | ||
58 | name: 'live video', | ||
59 | channelId: servers[0].videoChannel.id, | ||
60 | privacy: VideoPrivacy.PUBLIC | ||
61 | } | ||
62 | |||
63 | const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes) | ||
64 | return res.body.video.uuid | ||
65 | } | ||
66 | |||
67 | it('Should correctly send a message when the live starts and ends', async function () { | ||
68 | this.timeout(60000) | ||
69 | |||
70 | const localStateChanges: VideoState[] = [] | ||
71 | const remoteStateChanges: VideoState[] = [] | ||
72 | |||
73 | const liveVideoUUID = await createLiveWrapper() | ||
74 | await waitJobs(servers) | ||
75 | |||
76 | { | ||
77 | const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID) | ||
78 | |||
79 | const localSocket = getLiveNotificationSocket(servers[0].url) | ||
80 | localSocket.on('state-change', data => localStateChanges.push(data.state)) | ||
81 | localSocket.emit('subscribe', { videoId }) | ||
82 | } | ||
83 | |||
84 | { | ||
85 | const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID) | ||
86 | |||
87 | const remoteSocket = getLiveNotificationSocket(servers[1].url) | ||
88 | remoteSocket.on('state-change', data => remoteStateChanges.push(data.state)) | ||
89 | remoteSocket.emit('subscribe', { videoId }) | ||
90 | } | ||
91 | |||
92 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
93 | |||
94 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
95 | await waitJobs(servers) | ||
96 | |||
97 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
98 | expect(stateChanges).to.have.length.at.least(1) | ||
99 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.PUBLISHED) | ||
100 | } | ||
101 | |||
102 | await stopFfmpeg(command) | ||
103 | |||
104 | for (const server of servers) { | ||
105 | await waitUntilLiveEnded(server.url, server.accessToken, liveVideoUUID) | ||
106 | } | ||
107 | await waitJobs(servers) | ||
108 | |||
109 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
110 | expect(stateChanges).to.have.length.at.least(2) | ||
111 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.LIVE_ENDED) | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | it('Should correctly send views change notification', async function () { | ||
116 | this.timeout(60000) | ||
117 | |||
118 | let localLastVideoViews = 0 | ||
119 | let remoteLastVideoViews = 0 | ||
120 | |||
121 | const liveVideoUUID = await createLiveWrapper() | ||
122 | await waitJobs(servers) | ||
123 | |||
124 | { | ||
125 | const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID) | ||
126 | |||
127 | const localSocket = getLiveNotificationSocket(servers[0].url) | ||
128 | localSocket.on('views-change', data => { localLastVideoViews = data.views }) | ||
129 | localSocket.emit('subscribe', { videoId }) | ||
130 | } | ||
131 | |||
132 | { | ||
133 | const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID) | ||
134 | |||
135 | const remoteSocket = getLiveNotificationSocket(servers[1].url) | ||
136 | remoteSocket.on('views-change', data => { remoteLastVideoViews = data.views }) | ||
137 | remoteSocket.emit('subscribe', { videoId }) | ||
138 | } | ||
139 | |||
140 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
141 | |||
142 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
143 | await waitJobs(servers) | ||
144 | |||
145 | expect(localLastVideoViews).to.equal(0) | ||
146 | expect(remoteLastVideoViews).to.equal(0) | ||
147 | |||
148 | await viewVideo(servers[0].url, liveVideoUUID) | ||
149 | await viewVideo(servers[1].url, liveVideoUUID) | ||
150 | |||
151 | await waitJobs(servers) | ||
152 | await wait(5000) | ||
153 | await waitJobs(servers) | ||
154 | |||
155 | expect(localLastVideoViews).to.equal(2) | ||
156 | expect(remoteLastVideoViews).to.equal(2) | ||
157 | |||
158 | await stopFfmpeg(command) | ||
159 | }) | ||
160 | |||
161 | it('Should not receive a notification after unsubscribe', async function () { | ||
162 | this.timeout(120000) | ||
163 | |||
164 | const stateChanges: VideoState[] = [] | ||
165 | |||
166 | const liveVideoUUID = await createLiveWrapper() | ||
167 | await waitJobs(servers) | ||
168 | |||
169 | const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID) | ||
170 | |||
171 | const socket = getLiveNotificationSocket(servers[0].url) | ||
172 | socket.on('state-change', data => stateChanges.push(data.state)) | ||
173 | socket.emit('subscribe', { videoId }) | ||
174 | |||
175 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
176 | |||
177 | await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID) | ||
178 | await waitJobs(servers) | ||
179 | |||
180 | // Notifier waits before sending a notification | ||
181 | await wait(10000) | ||
182 | |||
183 | expect(stateChanges).to.have.lengthOf(1) | ||
184 | socket.emit('unsubscribe', { videoId }) | ||
185 | |||
186 | await stopFfmpeg(command) | ||
187 | await waitJobs(servers) | ||
188 | |||
189 | expect(stateChanges).to.have.lengthOf(1) | ||
190 | }) | ||
191 | }) | ||
192 | |||
193 | after(async function () { | ||
194 | await cleanupTests(servers) | ||
195 | }) | ||
196 | }) | ||
diff --git a/server/tests/api/live/live-views.ts b/server/tests/api/live/live-views.ts new file mode 100644 index 000000000..a44d21ffa --- /dev/null +++ b/server/tests/api/live/live-views.ts | |||
@@ -0,0 +1,130 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
6 | import { VideoDetails, VideoPrivacy } from '@shared/models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createLive, | ||
10 | doubleFollow, | ||
11 | flushAndRunMultipleServers, | ||
12 | getVideo, | ||
13 | sendRTMPStreamInVideo, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultVideoChannel, | ||
17 | stopFfmpeg, | ||
18 | updateCustomSubConfig, | ||
19 | viewVideo, | ||
20 | wait, | ||
21 | waitJobs, | ||
22 | waitUntilLivePublishedOnAllServers | ||
23 | } from '../../../../shared/extra-utils' | ||
24 | |||
25 | const expect = chai.expect | ||
26 | |||
27 | describe('Test live', function () { | ||
28 | let servers: ServerInfo[] = [] | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | servers = await flushAndRunMultipleServers(2) | ||
34 | |||
35 | // Get the access tokens | ||
36 | await setAccessTokensToServers(servers) | ||
37 | await setDefaultVideoChannel(servers) | ||
38 | |||
39 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
40 | live: { | ||
41 | enabled: true, | ||
42 | allowReplay: true, | ||
43 | transcoding: { | ||
44 | enabled: false | ||
45 | } | ||
46 | } | ||
47 | }) | ||
48 | |||
49 | // Server 1 and server 2 follow each other | ||
50 | await doubleFollow(servers[0], servers[1]) | ||
51 | }) | ||
52 | |||
53 | describe('Live views', function () { | ||
54 | let liveVideoId: string | ||
55 | let command: FfmpegCommand | ||
56 | |||
57 | async function countViews (expected: number) { | ||
58 | for (const server of servers) { | ||
59 | const res = await getVideo(server.url, liveVideoId) | ||
60 | const video: VideoDetails = res.body | ||
61 | |||
62 | expect(video.views).to.equal(expected) | ||
63 | } | ||
64 | } | ||
65 | |||
66 | before(async function () { | ||
67 | this.timeout(30000) | ||
68 | |||
69 | const liveAttributes = { | ||
70 | name: 'live video', | ||
71 | channelId: servers[0].videoChannel.id, | ||
72 | privacy: VideoPrivacy.PUBLIC | ||
73 | } | ||
74 | |||
75 | const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes) | ||
76 | liveVideoId = res.body.video.uuid | ||
77 | |||
78 | command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) | ||
79 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) | ||
80 | await waitJobs(servers) | ||
81 | }) | ||
82 | |||
83 | it('Should display no views for a live', async function () { | ||
84 | await countViews(0) | ||
85 | }) | ||
86 | |||
87 | it('Should view a live twice and display 1 view', async function () { | ||
88 | this.timeout(30000) | ||
89 | |||
90 | await viewVideo(servers[0].url, liveVideoId) | ||
91 | await viewVideo(servers[0].url, liveVideoId) | ||
92 | |||
93 | await wait(7000) | ||
94 | |||
95 | await waitJobs(servers) | ||
96 | |||
97 | await countViews(1) | ||
98 | }) | ||
99 | |||
100 | it('Should wait and display 0 views', async function () { | ||
101 | this.timeout(30000) | ||
102 | |||
103 | await wait(12000) | ||
104 | await waitJobs(servers) | ||
105 | |||
106 | await countViews(0) | ||
107 | }) | ||
108 | |||
109 | it('Should view a live on a remote and on local and display 2 views', async function () { | ||
110 | this.timeout(30000) | ||
111 | |||
112 | await viewVideo(servers[0].url, liveVideoId) | ||
113 | await viewVideo(servers[1].url, liveVideoId) | ||
114 | await viewVideo(servers[1].url, liveVideoId) | ||
115 | |||
116 | await wait(7000) | ||
117 | await waitJobs(servers) | ||
118 | |||
119 | await countViews(2) | ||
120 | }) | ||
121 | |||
122 | after(async function () { | ||
123 | await stopFfmpeg(command) | ||
124 | }) | ||
125 | }) | ||
126 | |||
127 | after(async function () { | ||
128 | await cleanupTests(servers) | ||
129 | }) | ||
130 | }) | ||
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 57fb58150..50397924e 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -2,10 +2,8 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
6 | import { join } from 'path' | 5 | import { join } from 'path' |
7 | import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' | 6 | import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' |
8 | import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io' | ||
9 | import { LiveVideo, LiveVideoCreate, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' | 7 | import { LiveVideo, LiveVideoCreate, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' |
10 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 8 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
11 | import { | 9 | import { |
@@ -22,7 +20,6 @@ import { | |||
22 | getMyVideosWithFilter, | 20 | getMyVideosWithFilter, |
23 | getPlaylist, | 21 | getPlaylist, |
24 | getVideo, | 22 | getVideo, |
25 | getVideoIdFromUUID, | ||
26 | getVideosList, | 23 | getVideosList, |
27 | getVideosWithFilters, | 24 | getVideosWithFilters, |
28 | killallServers, | 25 | killallServers, |
@@ -40,11 +37,11 @@ import { | |||
40 | updateCustomSubConfig, | 37 | updateCustomSubConfig, |
41 | updateLive, | 38 | updateLive, |
42 | uploadVideoAndGetId, | 39 | uploadVideoAndGetId, |
43 | viewVideo, | ||
44 | wait, | 40 | wait, |
45 | waitJobs, | 41 | waitJobs, |
46 | waitUntilLiveEnded, | 42 | waitUntilLiveEnded, |
47 | waitUntilLivePublished, | 43 | waitUntilLivePublished, |
44 | waitUntilLivePublishedOnAllServers, | ||
48 | waitUntilLiveSegmentGeneration | 45 | waitUntilLiveSegmentGeneration |
49 | } from '../../../../shared/extra-utils' | 46 | } from '../../../../shared/extra-utils' |
50 | 47 | ||
@@ -53,12 +50,6 @@ const expect = chai.expect | |||
53 | describe('Test live', function () { | 50 | describe('Test live', function () { |
54 | let servers: ServerInfo[] = [] | 51 | let servers: ServerInfo[] = [] |
55 | 52 | ||
56 | async function waitUntilLivePublishedOnAllServers (videoId: string) { | ||
57 | for (const server of servers) { | ||
58 | await waitUntilLivePublished(server.url, server.accessToken, videoId) | ||
59 | } | ||
60 | } | ||
61 | |||
62 | before(async function () { | 53 | before(async function () { |
63 | this.timeout(120000) | 54 | this.timeout(120000) |
64 | 55 | ||
@@ -247,7 +238,7 @@ describe('Test live', function () { | |||
247 | liveVideoId = resLive.body.video.uuid | 238 | liveVideoId = resLive.body.video.uuid |
248 | 239 | ||
249 | command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) | 240 | command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) |
250 | await waitUntilLivePublishedOnAllServers(liveVideoId) | 241 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) |
251 | await waitJobs(servers) | 242 | await waitJobs(servers) |
252 | }) | 243 | }) |
253 | 244 | ||
@@ -461,7 +452,7 @@ describe('Test live', function () { | |||
461 | liveVideoId = await createLiveWrapper(false) | 452 | liveVideoId = await createLiveWrapper(false) |
462 | 453 | ||
463 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) | 454 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) |
464 | await waitUntilLivePublishedOnAllServers(liveVideoId) | 455 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) |
465 | await waitJobs(servers) | 456 | await waitJobs(servers) |
466 | 457 | ||
467 | await testVideoResolutions(liveVideoId, [ 720 ]) | 458 | await testVideoResolutions(liveVideoId, [ 720 ]) |
@@ -477,7 +468,7 @@ describe('Test live', function () { | |||
477 | liveVideoId = await createLiveWrapper(false) | 468 | liveVideoId = await createLiveWrapper(false) |
478 | 469 | ||
479 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) | 470 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) |
480 | await waitUntilLivePublishedOnAllServers(liveVideoId) | 471 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) |
481 | await waitJobs(servers) | 472 | await waitJobs(servers) |
482 | 473 | ||
483 | await testVideoResolutions(liveVideoId, resolutions) | 474 | await testVideoResolutions(liveVideoId, resolutions) |
@@ -494,7 +485,7 @@ describe('Test live', function () { | |||
494 | liveVideoId = await createLiveWrapper(true) | 485 | liveVideoId = await createLiveWrapper(true) |
495 | 486 | ||
496 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId, 'video_short2.webm') | 487 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId, 'video_short2.webm') |
497 | await waitUntilLivePublishedOnAllServers(liveVideoId) | 488 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) |
498 | await waitJobs(servers) | 489 | await waitJobs(servers) |
499 | 490 | ||
500 | await testVideoResolutions(liveVideoId, resolutions) | 491 | await testVideoResolutions(liveVideoId, resolutions) |
@@ -504,7 +495,7 @@ describe('Test live', function () { | |||
504 | 495 | ||
505 | await waitJobs(servers) | 496 | await waitJobs(servers) |
506 | 497 | ||
507 | await waitUntilLivePublishedOnAllServers(liveVideoId) | 498 | await waitUntilLivePublishedOnAllServers(servers, liveVideoId) |
508 | 499 | ||
509 | const bitrateLimits = { | 500 | const bitrateLimits = { |
510 | 720: 5000 * 1000, // 60FPS | 501 | 720: 5000 * 1000, // 60FPS |
@@ -559,216 +550,6 @@ describe('Test live', function () { | |||
559 | }) | 550 | }) |
560 | }) | 551 | }) |
561 | 552 | ||
562 | describe('Live views', function () { | ||
563 | let liveVideoId: string | ||
564 | let command: FfmpegCommand | ||
565 | |||
566 | async function countViews (expected: number) { | ||
567 | for (const server of servers) { | ||
568 | const res = await getVideo(server.url, liveVideoId) | ||
569 | const video: VideoDetails = res.body | ||
570 | |||
571 | expect(video.views).to.equal(expected) | ||
572 | } | ||
573 | } | ||
574 | |||
575 | before(async function () { | ||
576 | this.timeout(30000) | ||
577 | |||
578 | const liveAttributes = { | ||
579 | name: 'live video', | ||
580 | channelId: servers[0].videoChannel.id, | ||
581 | privacy: VideoPrivacy.PUBLIC | ||
582 | } | ||
583 | |||
584 | const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes) | ||
585 | liveVideoId = res.body.video.uuid | ||
586 | |||
587 | command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId) | ||
588 | await waitUntilLivePublishedOnAllServers(liveVideoId) | ||
589 | await waitJobs(servers) | ||
590 | }) | ||
591 | |||
592 | it('Should display no views for a live', async function () { | ||
593 | await countViews(0) | ||
594 | }) | ||
595 | |||
596 | it('Should view a live twice and display 1 view', async function () { | ||
597 | this.timeout(30000) | ||
598 | |||
599 | await viewVideo(servers[0].url, liveVideoId) | ||
600 | await viewVideo(servers[0].url, liveVideoId) | ||
601 | |||
602 | await wait(7000) | ||
603 | |||
604 | await waitJobs(servers) | ||
605 | |||
606 | await countViews(1) | ||
607 | }) | ||
608 | |||
609 | it('Should wait and display 0 views', async function () { | ||
610 | this.timeout(30000) | ||
611 | |||
612 | await wait(7000) | ||
613 | await waitJobs(servers) | ||
614 | |||
615 | await countViews(0) | ||
616 | }) | ||
617 | |||
618 | it('Should view a live on a remote and on local and display 2 views', async function () { | ||
619 | this.timeout(30000) | ||
620 | |||
621 | await viewVideo(servers[0].url, liveVideoId) | ||
622 | await viewVideo(servers[1].url, liveVideoId) | ||
623 | await viewVideo(servers[1].url, liveVideoId) | ||
624 | |||
625 | await wait(7000) | ||
626 | await waitJobs(servers) | ||
627 | |||
628 | await countViews(2) | ||
629 | }) | ||
630 | |||
631 | after(async function () { | ||
632 | await stopFfmpeg(command) | ||
633 | }) | ||
634 | }) | ||
635 | |||
636 | describe('Live socket messages', function () { | ||
637 | |||
638 | async function createLiveWrapper () { | ||
639 | const liveAttributes = { | ||
640 | name: 'live video', | ||
641 | channelId: servers[0].videoChannel.id, | ||
642 | privacy: VideoPrivacy.PUBLIC | ||
643 | } | ||
644 | |||
645 | const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes) | ||
646 | return res.body.video.uuid | ||
647 | } | ||
648 | |||
649 | it('Should correctly send a message when the live starts and ends', async function () { | ||
650 | this.timeout(60000) | ||
651 | |||
652 | const localStateChanges: VideoState[] = [] | ||
653 | const remoteStateChanges: VideoState[] = [] | ||
654 | |||
655 | const liveVideoUUID = await createLiveWrapper() | ||
656 | await waitJobs(servers) | ||
657 | |||
658 | { | ||
659 | const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID) | ||
660 | |||
661 | const localSocket = getLiveNotificationSocket(servers[0].url) | ||
662 | localSocket.on('state-change', data => localStateChanges.push(data.state)) | ||
663 | localSocket.emit('subscribe', { videoId }) | ||
664 | } | ||
665 | |||
666 | { | ||
667 | const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID) | ||
668 | |||
669 | const remoteSocket = getLiveNotificationSocket(servers[1].url) | ||
670 | remoteSocket.on('state-change', data => remoteStateChanges.push(data.state)) | ||
671 | remoteSocket.emit('subscribe', { videoId }) | ||
672 | } | ||
673 | |||
674 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
675 | |||
676 | await waitUntilLivePublishedOnAllServers(liveVideoUUID) | ||
677 | await waitJobs(servers) | ||
678 | |||
679 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
680 | expect(stateChanges).to.have.length.at.least(1) | ||
681 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.PUBLISHED) | ||
682 | } | ||
683 | |||
684 | await stopFfmpeg(command) | ||
685 | |||
686 | for (const server of servers) { | ||
687 | await waitUntilLiveEnded(server.url, server.accessToken, liveVideoUUID) | ||
688 | } | ||
689 | await waitJobs(servers) | ||
690 | |||
691 | for (const stateChanges of [ localStateChanges, remoteStateChanges ]) { | ||
692 | expect(stateChanges).to.have.length.at.least(2) | ||
693 | expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.LIVE_ENDED) | ||
694 | } | ||
695 | }) | ||
696 | |||
697 | it('Should correctly send views change notification', async function () { | ||
698 | this.timeout(60000) | ||
699 | |||
700 | let localLastVideoViews = 0 | ||
701 | let remoteLastVideoViews = 0 | ||
702 | |||
703 | const liveVideoUUID = await createLiveWrapper() | ||
704 | await waitJobs(servers) | ||
705 | |||
706 | { | ||
707 | const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID) | ||
708 | |||
709 | const localSocket = getLiveNotificationSocket(servers[0].url) | ||
710 | localSocket.on('views-change', data => { localLastVideoViews = data.views }) | ||
711 | localSocket.emit('subscribe', { videoId }) | ||
712 | } | ||
713 | |||
714 | { | ||
715 | const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID) | ||
716 | |||
717 | const remoteSocket = getLiveNotificationSocket(servers[1].url) | ||
718 | remoteSocket.on('views-change', data => { remoteLastVideoViews = data.views }) | ||
719 | remoteSocket.emit('subscribe', { videoId }) | ||
720 | } | ||
721 | |||
722 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
723 | |||
724 | await waitUntilLivePublishedOnAllServers(liveVideoUUID) | ||
725 | await waitJobs(servers) | ||
726 | |||
727 | expect(localLastVideoViews).to.equal(0) | ||
728 | expect(remoteLastVideoViews).to.equal(0) | ||
729 | |||
730 | await viewVideo(servers[0].url, liveVideoUUID) | ||
731 | await viewVideo(servers[1].url, liveVideoUUID) | ||
732 | |||
733 | await waitJobs(servers) | ||
734 | await wait(5000) | ||
735 | await waitJobs(servers) | ||
736 | |||
737 | expect(localLastVideoViews).to.equal(2) | ||
738 | expect(remoteLastVideoViews).to.equal(2) | ||
739 | |||
740 | await stopFfmpeg(command) | ||
741 | }) | ||
742 | |||
743 | it('Should not receive a notification after unsubscribe', async function () { | ||
744 | this.timeout(60000) | ||
745 | |||
746 | const stateChanges: VideoState[] = [] | ||
747 | |||
748 | const liveVideoUUID = await createLiveWrapper() | ||
749 | await waitJobs(servers) | ||
750 | |||
751 | const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID) | ||
752 | |||
753 | const socket = getLiveNotificationSocket(servers[0].url) | ||
754 | socket.on('state-change', data => stateChanges.push(data.state)) | ||
755 | socket.emit('subscribe', { videoId }) | ||
756 | |||
757 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
758 | |||
759 | await waitUntilLivePublishedOnAllServers(liveVideoUUID) | ||
760 | await waitJobs(servers) | ||
761 | |||
762 | expect(stateChanges).to.have.lengthOf(1) | ||
763 | socket.emit('unsubscribe', { videoId }) | ||
764 | |||
765 | await stopFfmpeg(command) | ||
766 | await waitJobs(servers) | ||
767 | |||
768 | expect(stateChanges).to.have.lengthOf(1) | ||
769 | }) | ||
770 | }) | ||
771 | |||
772 | describe('After a server restart', function () { | 553 | describe('After a server restart', function () { |
773 | let liveVideoId: string | 554 | let liveVideoId: string |
774 | let liveVideoReplayId: string | 555 | let liveVideoReplayId: string |
diff --git a/server/tests/api/moderation/blocklist.ts b/server/tests/api/moderation/blocklist.ts index e8202aff1..793abbcb4 100644 --- a/server/tests/api/moderation/blocklist.ts +++ b/server/tests/api/moderation/blocklist.ts | |||
@@ -1,46 +1,50 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
5 | import { AccountBlock, ServerBlock, Video, UserNotification, UserNotificationType } from '../../../../shared/index' | 4 | import * as chai from 'chai' |
6 | import { | 5 | import { |
6 | addAccountToAccountBlocklist, | ||
7 | addAccountToServerBlocklist, | ||
8 | addServerToAccountBlocklist, | ||
9 | addServerToServerBlocklist, | ||
10 | addVideoCommentReply, | ||
11 | addVideoCommentThread, | ||
7 | cleanupTests, | 12 | cleanupTests, |
8 | createUser, | 13 | createUser, |
9 | deleteVideoComment, | 14 | deleteVideoComment, |
10 | doubleFollow, | 15 | doubleFollow, |
16 | findCommentId, | ||
11 | flushAndRunMultipleServers, | 17 | flushAndRunMultipleServers, |
12 | ServerInfo, | ||
13 | uploadVideo, | ||
14 | userLogin, | ||
15 | follow, | 18 | follow, |
16 | unfollow | ||
17 | } from '../../../../shared/extra-utils/index' | ||
18 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' | ||
19 | import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos' | ||
20 | import { | ||
21 | addVideoCommentReply, | ||
22 | addVideoCommentThread, | ||
23 | getVideoCommentThreads, | ||
24 | getVideoThreadComments, | ||
25 | findCommentId | ||
26 | } from '../../../../shared/extra-utils/videos/video-comments' | ||
27 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
28 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | ||
29 | import { | ||
30 | addAccountToAccountBlocklist, | ||
31 | addAccountToServerBlocklist, | ||
32 | addServerToAccountBlocklist, | ||
33 | addServerToServerBlocklist, | ||
34 | getAccountBlocklistByAccount, | 19 | getAccountBlocklistByAccount, |
35 | getAccountBlocklistByServer, | 20 | getAccountBlocklistByServer, |
36 | getServerBlocklistByAccount, | 21 | getServerBlocklistByAccount, |
37 | getServerBlocklistByServer, | 22 | getServerBlocklistByServer, |
23 | getUserNotifications, | ||
24 | getVideoCommentThreads, | ||
25 | getVideosList, | ||
26 | getVideosListWithToken, | ||
27 | getVideoThreadComments, | ||
38 | removeAccountFromAccountBlocklist, | 28 | removeAccountFromAccountBlocklist, |
39 | removeAccountFromServerBlocklist, | 29 | removeAccountFromServerBlocklist, |
40 | removeServerFromAccountBlocklist, | 30 | removeServerFromAccountBlocklist, |
41 | removeServerFromServerBlocklist | 31 | removeServerFromServerBlocklist, |
42 | } from '../../../../shared/extra-utils/users/blocklist' | 32 | ServerInfo, |
43 | import { getUserNotifications } from '../../../../shared/extra-utils/users/user-notifications' | 33 | setAccessTokensToServers, |
34 | unfollow, | ||
35 | uploadVideo, | ||
36 | userLogin, | ||
37 | waitJobs | ||
38 | } from '@shared/extra-utils' | ||
39 | import { | ||
40 | AccountBlock, | ||
41 | ServerBlock, | ||
42 | UserNotification, | ||
43 | UserNotificationType, | ||
44 | Video, | ||
45 | VideoComment, | ||
46 | VideoCommentThreadTree | ||
47 | } from '@shared/models' | ||
44 | 48 | ||
45 | const expect = chai.expect | 49 | const expect = chai.expect |
46 | 50 | ||
@@ -211,7 +215,7 @@ describe('Test blocklist', function () { | |||
211 | 215 | ||
212 | const threads: VideoComment[] = resThreads.body.data | 216 | const threads: VideoComment[] = resThreads.body.data |
213 | expect(threads).to.have.lengthOf(1) | 217 | expect(threads).to.have.lengthOf(1) |
214 | expect(threads[0].totalReplies).to.equal(0) | 218 | expect(threads[0].totalReplies).to.equal(1) |
215 | 219 | ||
216 | const t = threads.find(t => t.text === 'comment user 1') | 220 | const t = threads.find(t => t.text === 'comment user 1') |
217 | expect(t).to.be.undefined | 221 | expect(t).to.be.undefined |
@@ -561,7 +565,7 @@ describe('Test blocklist', function () { | |||
561 | threads = threads.filter(t => t.isDeleted === false) | 565 | threads = threads.filter(t => t.isDeleted === false) |
562 | 566 | ||
563 | expect(threads).to.have.lengthOf(1) | 567 | expect(threads).to.have.lengthOf(1) |
564 | expect(threads[0].totalReplies).to.equal(0) | 568 | expect(threads[0].totalReplies).to.equal(1) |
565 | 569 | ||
566 | const t = threads.find(t => t.text === 'comment user 1') | 570 | const t = threads.find(t => t.text === 'comment user 1') |
567 | expect(t).to.be.undefined | 571 | expect(t).to.be.undefined |
diff --git a/server/tests/api/notifications/comments-notifications.ts b/server/tests/api/notifications/comments-notifications.ts index 5e4ab0d6c..d2badf237 100644 --- a/server/tests/api/notifications/comments-notifications.ts +++ b/server/tests/api/notifications/comments-notifications.ts | |||
@@ -2,20 +2,25 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { cleanupTests, getVideoCommentThreads, getVideoThreadComments, updateMyUser } from '../../../../shared/extra-utils' | ||
6 | import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' | ||
7 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | ||
8 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
9 | import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/extra-utils/users/blocklist' | ||
10 | import { | 5 | import { |
6 | addAccountToAccountBlocklist, | ||
7 | addVideoCommentReply, | ||
8 | addVideoCommentThread, | ||
11 | checkCommentMention, | 9 | checkCommentMention, |
12 | CheckerBaseParams, | 10 | CheckerBaseParams, |
13 | checkNewCommentOnMyVideo, | 11 | checkNewCommentOnMyVideo, |
14 | prepareNotificationsTest | 12 | cleanupTests, |
15 | } from '../../../../shared/extra-utils/users/user-notifications' | 13 | getVideoCommentThreads, |
16 | import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' | 14 | getVideoThreadComments, |
17 | import { UserNotification } from '../../../../shared/models/users' | 15 | MockSmtpServer, |
18 | import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 16 | prepareNotificationsTest, |
17 | removeAccountFromAccountBlocklist, | ||
18 | ServerInfo, | ||
19 | updateMyUser, | ||
20 | uploadVideo, | ||
21 | waitJobs | ||
22 | } from '@shared/extra-utils' | ||
23 | import { UserNotification, VideoCommentThreadTree } from '@shared/models' | ||
19 | 24 | ||
20 | const expect = chai.expect | 25 | const expect = chai.expect |
21 | 26 | ||
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts index 4ce6675b6..3425480ae 100644 --- a/server/tests/api/notifications/moderation-notifications.ts +++ b/server/tests/api/notifications/moderation-notifications.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { v4 as uuidv4 } from 'uuid' | 4 | import { buildUUID } from '@server/helpers/uuid' |
5 | import { AbuseState } from '@shared/models' | 5 | import { AbuseState } from '@shared/models' |
6 | import { | 6 | import { |
7 | addAbuseMessage, | 7 | addAbuseMessage, |
@@ -85,7 +85,7 @@ describe('Test moderation notifications', function () { | |||
85 | it('Should send a notification to moderators on local video abuse', async function () { | 85 | it('Should send a notification to moderators on local video abuse', async function () { |
86 | this.timeout(20000) | 86 | this.timeout(20000) |
87 | 87 | ||
88 | const name = 'video for abuse ' + uuidv4() | 88 | const name = 'video for abuse ' + buildUUID() |
89 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 89 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
90 | const video = resVideo.body.video | 90 | const video = resVideo.body.video |
91 | 91 | ||
@@ -98,7 +98,7 @@ describe('Test moderation notifications', function () { | |||
98 | it('Should send a notification to moderators on remote video abuse', async function () { | 98 | it('Should send a notification to moderators on remote video abuse', async function () { |
99 | this.timeout(20000) | 99 | this.timeout(20000) |
100 | 100 | ||
101 | const name = 'video for abuse ' + uuidv4() | 101 | const name = 'video for abuse ' + buildUUID() |
102 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 102 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
103 | const video = resVideo.body.video | 103 | const video = resVideo.body.video |
104 | 104 | ||
@@ -114,10 +114,10 @@ describe('Test moderation notifications', function () { | |||
114 | it('Should send a notification to moderators on local comment abuse', async function () { | 114 | it('Should send a notification to moderators on local comment abuse', async function () { |
115 | this.timeout(20000) | 115 | this.timeout(20000) |
116 | 116 | ||
117 | const name = 'video for abuse ' + uuidv4() | 117 | const name = 'video for abuse ' + buildUUID() |
118 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 118 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
119 | const video = resVideo.body.video | 119 | const video = resVideo.body.video |
120 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4()) | 120 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + buildUUID()) |
121 | const comment = resComment.body.comment | 121 | const comment = resComment.body.comment |
122 | 122 | ||
123 | await waitJobs(servers) | 123 | await waitJobs(servers) |
@@ -131,10 +131,10 @@ describe('Test moderation notifications', function () { | |||
131 | it('Should send a notification to moderators on remote comment abuse', async function () { | 131 | it('Should send a notification to moderators on remote comment abuse', async function () { |
132 | this.timeout(20000) | 132 | this.timeout(20000) |
133 | 133 | ||
134 | const name = 'video for abuse ' + uuidv4() | 134 | const name = 'video for abuse ' + buildUUID() |
135 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 135 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
136 | const video = resVideo.body.video | 136 | const video = resVideo.body.video |
137 | await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + uuidv4()) | 137 | await addVideoCommentThread(servers[0].url, userAccessToken, video.id, 'comment abuse ' + buildUUID()) |
138 | 138 | ||
139 | await waitJobs(servers) | 139 | await waitJobs(servers) |
140 | 140 | ||
@@ -188,7 +188,7 @@ describe('Test moderation notifications', function () { | |||
188 | token: userAccessToken | 188 | token: userAccessToken |
189 | } | 189 | } |
190 | 190 | ||
191 | const name = 'abuse ' + uuidv4() | 191 | const name = 'abuse ' + buildUUID() |
192 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 192 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
193 | const video = resVideo.body.video | 193 | const video = resVideo.body.video |
194 | 194 | ||
@@ -236,7 +236,7 @@ describe('Test moderation notifications', function () { | |||
236 | token: servers[0].accessToken | 236 | token: servers[0].accessToken |
237 | } | 237 | } |
238 | 238 | ||
239 | const name = 'abuse ' + uuidv4() | 239 | const name = 'abuse ' + buildUUID() |
240 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 240 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
241 | const video = resVideo.body.video | 241 | const video = resVideo.body.video |
242 | 242 | ||
@@ -307,7 +307,7 @@ describe('Test moderation notifications', function () { | |||
307 | it('Should send a notification to video owner on blacklist', async function () { | 307 | it('Should send a notification to video owner on blacklist', async function () { |
308 | this.timeout(10000) | 308 | this.timeout(10000) |
309 | 309 | ||
310 | const name = 'video for abuse ' + uuidv4() | 310 | const name = 'video for abuse ' + buildUUID() |
311 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 311 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
312 | const uuid = resVideo.body.video.uuid | 312 | const uuid = resVideo.body.video.uuid |
313 | 313 | ||
@@ -320,7 +320,7 @@ describe('Test moderation notifications', function () { | |||
320 | it('Should send a notification to video owner on unblacklist', async function () { | 320 | it('Should send a notification to video owner on unblacklist', async function () { |
321 | this.timeout(10000) | 321 | this.timeout(10000) |
322 | 322 | ||
323 | const name = 'video for abuse ' + uuidv4() | 323 | const name = 'video for abuse ' + buildUUID() |
324 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) | 324 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name }) |
325 | const uuid = resVideo.body.video.uuid | 325 | const uuid = resVideo.body.video.uuid |
326 | 326 | ||
@@ -507,7 +507,7 @@ describe('Test moderation notifications', function () { | |||
507 | it('Should send notification to moderators on new video with auto-blacklist', async function () { | 507 | it('Should send notification to moderators on new video with auto-blacklist', async function () { |
508 | this.timeout(40000) | 508 | this.timeout(40000) |
509 | 509 | ||
510 | videoName = 'video with auto-blacklist ' + uuidv4() | 510 | videoName = 'video with auto-blacklist ' + buildUUID() |
511 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) | 511 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) |
512 | videoUUID = resVideo.body.video.uuid | 512 | videoUUID = resVideo.body.video.uuid |
513 | 513 | ||
@@ -553,7 +553,7 @@ describe('Test moderation notifications', function () { | |||
553 | 553 | ||
554 | const updateAt = new Date(new Date().getTime() + 1000000) | 554 | const updateAt = new Date(new Date().getTime() + 1000000) |
555 | 555 | ||
556 | const name = 'video with auto-blacklist and future schedule ' + uuidv4() | 556 | const name = 'video with auto-blacklist and future schedule ' + buildUUID() |
557 | 557 | ||
558 | const data = { | 558 | const data = { |
559 | name, | 559 | name, |
@@ -586,7 +586,7 @@ describe('Test moderation notifications', function () { | |||
586 | // In 2 seconds | 586 | // In 2 seconds |
587 | const updateAt = new Date(new Date().getTime() + 2000) | 587 | const updateAt = new Date(new Date().getTime() + 2000) |
588 | 588 | ||
589 | const name = 'video with schedule done and still auto-blacklisted ' + uuidv4() | 589 | const name = 'video with schedule done and still auto-blacklisted ' + buildUUID() |
590 | 590 | ||
591 | const data = { | 591 | const data = { |
592 | name, | 592 | name, |
@@ -609,7 +609,7 @@ describe('Test moderation notifications', function () { | |||
609 | it('Should not send a notification to moderators on new video without auto-blacklist', async function () { | 609 | it('Should not send a notification to moderators on new video without auto-blacklist', async function () { |
610 | this.timeout(60000) | 610 | this.timeout(60000) |
611 | 611 | ||
612 | const name = 'video without auto-blacklist ' + uuidv4() | 612 | const name = 'video without auto-blacklist ' + buildUUID() |
613 | 613 | ||
614 | // admin with blacklist right will not be auto-blacklisted | 614 | // admin with blacklist right will not be auto-blacklisted |
615 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name }) | 615 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name }) |
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts index 7e88d979b..e981c1718 100644 --- a/server/tests/api/notifications/user-notifications.ts +++ b/server/tests/api/notifications/user-notifications.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { v4 as uuidv4 } from 'uuid' | 5 | import { buildUUID } from '@server/helpers/uuid' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | updateMyUser, | 8 | updateMyUser, |
@@ -207,7 +207,7 @@ describe('Test user notifications', function () { | |||
207 | it('Should send a new video notification after a video import', async function () { | 207 | it('Should send a new video notification after a video import', async function () { |
208 | this.timeout(100000) | 208 | this.timeout(100000) |
209 | 209 | ||
210 | const name = 'video import ' + uuidv4() | 210 | const name = 'video import ' + buildUUID() |
211 | 211 | ||
212 | const attributes = { | 212 | const attributes = { |
213 | name, | 213 | name, |
@@ -278,7 +278,7 @@ describe('Test user notifications', function () { | |||
278 | it('Should send a notification when an imported video is transcoded', async function () { | 278 | it('Should send a notification when an imported video is transcoded', async function () { |
279 | this.timeout(50000) | 279 | this.timeout(50000) |
280 | 280 | ||
281 | const name = 'video import ' + uuidv4() | 281 | const name = 'video import ' + buildUUID() |
282 | 282 | ||
283 | const attributes = { | 283 | const attributes = { |
284 | name, | 284 | name, |
@@ -347,7 +347,7 @@ describe('Test user notifications', function () { | |||
347 | it('Should send a notification when the video import failed', async function () { | 347 | it('Should send a notification when the video import failed', async function () { |
348 | this.timeout(70000) | 348 | this.timeout(70000) |
349 | 349 | ||
350 | const name = 'video import ' + uuidv4() | 350 | const name = 'video import ' + buildUUID() |
351 | 351 | ||
352 | const attributes = { | 352 | const attributes = { |
353 | name, | 353 | name, |
@@ -365,7 +365,7 @@ describe('Test user notifications', function () { | |||
365 | it('Should send a notification when the video import succeeded', async function () { | 365 | it('Should send a notification when the video import succeeded', async function () { |
366 | this.timeout(70000) | 366 | this.timeout(70000) |
367 | 367 | ||
368 | const name = 'video import ' + uuidv4() | 368 | const name = 'video import ' + buildUUID() |
369 | 369 | ||
370 | const attributes = { | 370 | const attributes = { |
371 | name, | 371 | name, |
diff --git a/server/tests/api/search/index.ts b/server/tests/api/search/index.ts index 232c1f2a4..a976d210d 100644 --- a/server/tests/api/search/index.ts +++ b/server/tests/api/search/index.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import './search-activitypub-video-playlists' | ||
1 | import './search-activitypub-video-channels' | 2 | import './search-activitypub-video-channels' |
2 | import './search-activitypub-videos' | 3 | import './search-activitypub-videos' |
4 | import './search-channels' | ||
3 | import './search-index' | 5 | import './search-index' |
6 | import './search-playlists' | ||
4 | import './search-videos' | 7 | import './search-videos' |
5 | import './search-channels' | ||
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts index d7e3ed5be..e83eb7171 100644 --- a/server/tests/api/search/search-activitypub-video-channels.ts +++ b/server/tests/api/search/search-activitypub-video-channels.ts | |||
@@ -106,9 +106,25 @@ describe('Test ActivityPub video channels search', function () { | |||
106 | } | 106 | } |
107 | }) | 107 | }) |
108 | 108 | ||
109 | it('Should search a local video channel with an alternative URL', async function () { | ||
110 | const search = 'http://localhost:' + servers[0].port + '/c/channel1_server1' | ||
111 | |||
112 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
113 | const res = await searchVideoChannel(servers[0].url, search, token) | ||
114 | |||
115 | expect(res.body.total).to.equal(1) | ||
116 | expect(res.body.data).to.be.an('array') | ||
117 | expect(res.body.data).to.have.lengthOf(1) | ||
118 | expect(res.body.data[0].name).to.equal('channel1_server1') | ||
119 | expect(res.body.data[0].displayName).to.equal('Channel 1 server 1') | ||
120 | } | ||
121 | }) | ||
122 | |||
109 | it('Should search a remote video channel with URL or handle', async function () { | 123 | it('Should search a remote video channel with URL or handle', async function () { |
110 | const searches = [ | 124 | const searches = [ |
111 | 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2', | 125 | 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2', |
126 | 'http://localhost:' + servers[1].port + '/c/channel1_server2', | ||
127 | 'http://localhost:' + servers[1].port + '/c/channel1_server2/videos', | ||
112 | 'channel1_server2@localhost:' + servers[1].port | 128 | 'channel1_server2@localhost:' + servers[1].port |
113 | ] | 129 | ] |
114 | 130 | ||
diff --git a/server/tests/api/search/search-activitypub-video-playlists.ts b/server/tests/api/search/search-activitypub-video-playlists.ts new file mode 100644 index 000000000..4c08e9548 --- /dev/null +++ b/server/tests/api/search/search-activitypub-video-playlists.ts | |||
@@ -0,0 +1,212 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { | ||
6 | addVideoInPlaylist, | ||
7 | cleanupTests, | ||
8 | createVideoPlaylist, | ||
9 | deleteVideoPlaylist, | ||
10 | flushAndRunMultipleServers, | ||
11 | getVideoPlaylistsList, | ||
12 | searchVideoPlaylists, | ||
13 | ServerInfo, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | uploadVideoAndGetId, | ||
17 | wait | ||
18 | } from '../../../../shared/extra-utils' | ||
19 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
20 | import { VideoPlaylist, VideoPlaylistPrivacy } from '../../../../shared/models/videos' | ||
21 | |||
22 | const expect = chai.expect | ||
23 | |||
24 | describe('Test ActivityPub playlists search', function () { | ||
25 | let servers: ServerInfo[] | ||
26 | let playlistServer1UUID: string | ||
27 | let playlistServer2UUID: string | ||
28 | let video2Server2: string | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | servers = await flushAndRunMultipleServers(2) | ||
34 | |||
35 | await setAccessTokensToServers(servers) | ||
36 | await setDefaultVideoChannel(servers) | ||
37 | |||
38 | { | ||
39 | const video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid | ||
40 | const video2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid | ||
41 | |||
42 | const attributes = { | ||
43 | displayName: 'playlist 1 on server 1', | ||
44 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
45 | videoChannelId: servers[0].videoChannel.id | ||
46 | } | ||
47 | const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs: attributes }) | ||
48 | playlistServer1UUID = res.body.videoPlaylist.uuid | ||
49 | |||
50 | for (const videoId of [ video1, video2 ]) { | ||
51 | await addVideoInPlaylist({ | ||
52 | url: servers[0].url, | ||
53 | token: servers[0].accessToken, | ||
54 | playlistId: playlistServer1UUID, | ||
55 | elementAttrs: { videoId } | ||
56 | }) | ||
57 | } | ||
58 | } | ||
59 | |||
60 | { | ||
61 | const videoId = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 1' })).uuid | ||
62 | video2Server2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 2' })).uuid | ||
63 | |||
64 | const attributes = { | ||
65 | displayName: 'playlist 1 on server 2', | ||
66 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
67 | videoChannelId: servers[1].videoChannel.id | ||
68 | } | ||
69 | const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs: attributes }) | ||
70 | playlistServer2UUID = res.body.videoPlaylist.uuid | ||
71 | |||
72 | await addVideoInPlaylist({ | ||
73 | url: servers[1].url, | ||
74 | token: servers[1].accessToken, | ||
75 | playlistId: playlistServer2UUID, | ||
76 | elementAttrs: { videoId } | ||
77 | }) | ||
78 | } | ||
79 | |||
80 | await waitJobs(servers) | ||
81 | }) | ||
82 | |||
83 | it('Should not find a remote playlist', async function () { | ||
84 | { | ||
85 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/43' | ||
86 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
87 | |||
88 | expect(res.body.total).to.equal(0) | ||
89 | expect(res.body.data).to.be.an('array') | ||
90 | expect(res.body.data).to.have.lengthOf(0) | ||
91 | } | ||
92 | |||
93 | { | ||
94 | // Without token | ||
95 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID | ||
96 | const res = await searchVideoPlaylists(servers[0].url, search) | ||
97 | |||
98 | expect(res.body.total).to.equal(0) | ||
99 | expect(res.body.data).to.be.an('array') | ||
100 | expect(res.body.data).to.have.lengthOf(0) | ||
101 | } | ||
102 | }) | ||
103 | |||
104 | it('Should search a local playlist', async function () { | ||
105 | const search = 'http://localhost:' + servers[0].port + '/video-playlists/' + playlistServer1UUID | ||
106 | const res = await searchVideoPlaylists(servers[0].url, search) | ||
107 | |||
108 | expect(res.body.total).to.equal(1) | ||
109 | expect(res.body.data).to.be.an('array') | ||
110 | expect(res.body.data).to.have.lengthOf(1) | ||
111 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
112 | expect(res.body.data[0].videosLength).to.equal(2) | ||
113 | }) | ||
114 | |||
115 | it('Should search a local playlist with an alternative URL', async function () { | ||
116 | const searches = [ | ||
117 | 'http://localhost:' + servers[0].port + '/videos/watch/playlist/' + playlistServer1UUID, | ||
118 | 'http://localhost:' + servers[0].port + '/w/p/' + playlistServer1UUID | ||
119 | ] | ||
120 | |||
121 | for (const search of searches) { | ||
122 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
123 | const res = await searchVideoPlaylists(servers[0].url, search, token) | ||
124 | |||
125 | expect(res.body.total).to.equal(1) | ||
126 | expect(res.body.data).to.be.an('array') | ||
127 | expect(res.body.data).to.have.lengthOf(1) | ||
128 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
129 | expect(res.body.data[0].videosLength).to.equal(2) | ||
130 | } | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | it('Should search a remote playlist', async function () { | ||
135 | const searches = [ | ||
136 | 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID, | ||
137 | 'http://localhost:' + servers[1].port + '/videos/watch/playlist/' + playlistServer2UUID, | ||
138 | 'http://localhost:' + servers[1].port + '/w/p/' + playlistServer2UUID | ||
139 | ] | ||
140 | |||
141 | for (const search of searches) { | ||
142 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
143 | |||
144 | expect(res.body.total).to.equal(1) | ||
145 | expect(res.body.data).to.be.an('array') | ||
146 | expect(res.body.data).to.have.lengthOf(1) | ||
147 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 2') | ||
148 | expect(res.body.data[0].videosLength).to.equal(1) | ||
149 | } | ||
150 | }) | ||
151 | |||
152 | it('Should not list this remote playlist', async function () { | ||
153 | const res = await getVideoPlaylistsList(servers[0].url, 0, 10) | ||
154 | expect(res.body.total).to.equal(1) | ||
155 | expect(res.body.data).to.have.lengthOf(1) | ||
156 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
157 | }) | ||
158 | |||
159 | it('Should update the playlist of server 2, and refresh it on server 1', async function () { | ||
160 | this.timeout(60000) | ||
161 | |||
162 | await addVideoInPlaylist({ | ||
163 | url: servers[1].url, | ||
164 | token: servers[1].accessToken, | ||
165 | playlistId: playlistServer2UUID, | ||
166 | elementAttrs: { videoId: video2Server2 } | ||
167 | }) | ||
168 | |||
169 | await waitJobs(servers) | ||
170 | // Expire playlist | ||
171 | await wait(10000) | ||
172 | |||
173 | // Will run refresh async | ||
174 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID | ||
175 | await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
176 | |||
177 | // Wait refresh | ||
178 | await wait(5000) | ||
179 | |||
180 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
181 | expect(res.body.total).to.equal(1) | ||
182 | expect(res.body.data).to.have.lengthOf(1) | ||
183 | |||
184 | const playlist: VideoPlaylist = res.body.data[0] | ||
185 | expect(playlist.videosLength).to.equal(2) | ||
186 | }) | ||
187 | |||
188 | it('Should delete playlist of server 2, and delete it on server 1', async function () { | ||
189 | this.timeout(60000) | ||
190 | |||
191 | await deleteVideoPlaylist(servers[1].url, servers[1].accessToken, playlistServer2UUID) | ||
192 | |||
193 | await waitJobs(servers) | ||
194 | // Expiration | ||
195 | await wait(10000) | ||
196 | |||
197 | // Will run refresh async | ||
198 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID | ||
199 | await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
200 | |||
201 | // Wait refresh | ||
202 | await wait(5000) | ||
203 | |||
204 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
205 | expect(res.body.total).to.equal(0) | ||
206 | expect(res.body.data).to.have.lengthOf(0) | ||
207 | }) | ||
208 | |||
209 | after(async function () { | ||
210 | await cleanupTests(servers) | ||
211 | }) | ||
212 | }) | ||
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts index c62dfca0d..e9b4978da 100644 --- a/server/tests/api/search/search-activitypub-videos.ts +++ b/server/tests/api/search/search-activitypub-videos.ts | |||
@@ -77,14 +77,33 @@ describe('Test ActivityPub videos search', function () { | |||
77 | expect(res.body.data[0].name).to.equal('video 1 on server 1') | 77 | expect(res.body.data[0].name).to.equal('video 1 on server 1') |
78 | }) | 78 | }) |
79 | 79 | ||
80 | it('Should search a local video with an alternative URL', async function () { | ||
81 | const search = 'http://localhost:' + servers[0].port + '/w/' + videoServer1UUID | ||
82 | const res1 = await searchVideo(servers[0].url, search) | ||
83 | const res2 = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken) | ||
84 | |||
85 | for (const res of [ res1, res2 ]) { | ||
86 | expect(res.body.total).to.equal(1) | ||
87 | expect(res.body.data).to.be.an('array') | ||
88 | expect(res.body.data).to.have.lengthOf(1) | ||
89 | expect(res.body.data[0].name).to.equal('video 1 on server 1') | ||
90 | } | ||
91 | }) | ||
92 | |||
80 | it('Should search a remote video', async function () { | 93 | it('Should search a remote video', async function () { |
81 | const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID | 94 | const searches = [ |
82 | const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken) | 95 | 'http://localhost:' + servers[1].port + '/w/' + videoServer2UUID, |
96 | 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID | ||
97 | ] | ||
83 | 98 | ||
84 | expect(res.body.total).to.equal(1) | 99 | for (const search of searches) { |
85 | expect(res.body.data).to.be.an('array') | 100 | const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken) |
86 | expect(res.body.data).to.have.lengthOf(1) | 101 | |
87 | expect(res.body.data[0].name).to.equal('video 1 on server 2') | 102 | expect(res.body.total).to.equal(1) |
103 | expect(res.body.data).to.be.an('array') | ||
104 | expect(res.body.data).to.have.lengthOf(1) | ||
105 | expect(res.body.data[0].name).to.equal('video 1 on server 2') | ||
106 | } | ||
88 | }) | 107 | }) |
89 | 108 | ||
90 | it('Should not list this remote video', async function () { | 109 | it('Should not list this remote video', async function () { |
@@ -95,7 +114,7 @@ describe('Test ActivityPub videos search', function () { | |||
95 | }) | 114 | }) |
96 | 115 | ||
97 | it('Should update video of server 2, and refresh it on server 1', async function () { | 116 | it('Should update video of server 2, and refresh it on server 1', async function () { |
98 | this.timeout(60000) | 117 | this.timeout(120000) |
99 | 118 | ||
100 | const channelAttributes = { | 119 | const channelAttributes = { |
101 | name: 'super_channel', | 120 | name: 'super_channel', |
@@ -134,7 +153,7 @@ describe('Test ActivityPub videos search', function () { | |||
134 | }) | 153 | }) |
135 | 154 | ||
136 | it('Should delete video of server 2, and delete it on server 1', async function () { | 155 | it('Should delete video of server 2, and delete it on server 1', async function () { |
137 | this.timeout(60000) | 156 | this.timeout(120000) |
138 | 157 | ||
139 | await removeVideo(servers[1].url, servers[1].accessToken, videoServer2UUID) | 158 | await removeVideo(servers[1].url, servers[1].accessToken, videoServer2UUID) |
140 | 159 | ||
diff --git a/server/tests/api/search/search-index.ts b/server/tests/api/search/search-index.ts index 849a8a893..00f79232a 100644 --- a/server/tests/api/search/search-index.ts +++ b/server/tests/api/search/search-index.ts | |||
@@ -2,19 +2,21 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { advancedVideoChannelSearch, searchVideoChannel } from '@shared/extra-utils/search/video-channels' | ||
6 | import { Video, VideoChannel, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType, VideosSearchQuery } from '@shared/models' | ||
5 | import { | 7 | import { |
8 | advancedVideoPlaylistSearch, | ||
9 | advancedVideosSearch, | ||
6 | cleanupTests, | 10 | cleanupTests, |
7 | flushAndRunServer, | 11 | flushAndRunServer, |
12 | immutableAssign, | ||
8 | searchVideo, | 13 | searchVideo, |
14 | searchVideoPlaylists, | ||
9 | ServerInfo, | 15 | ServerInfo, |
10 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
11 | updateCustomSubConfig, | 17 | updateCustomSubConfig, |
12 | uploadVideo, | 18 | uploadVideo |
13 | advancedVideosSearch, | ||
14 | immutableAssign | ||
15 | } from '../../../../shared/extra-utils' | 19 | } from '../../../../shared/extra-utils' |
16 | import { searchVideoChannel, advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels' | ||
17 | import { VideosSearchQuery, Video, VideoChannel } from '@shared/models' | ||
18 | 20 | ||
19 | const expect = chai.expect | 21 | const expect = chai.expect |
20 | 22 | ||
@@ -277,6 +279,56 @@ describe('Test videos search', function () { | |||
277 | }) | 279 | }) |
278 | }) | 280 | }) |
279 | 281 | ||
282 | describe('Playlists search', async function () { | ||
283 | |||
284 | it('Should make a simple search and not have results', async function () { | ||
285 | const res = await searchVideoPlaylists(server.url, 'a'.repeat(500)) | ||
286 | |||
287 | expect(res.body.total).to.equal(0) | ||
288 | expect(res.body.data).to.have.lengthOf(0) | ||
289 | }) | ||
290 | |||
291 | it('Should make a search and have results', async function () { | ||
292 | const res = await advancedVideoPlaylistSearch(server.url, { search: 'E2E playlist', sort: '-match' }) | ||
293 | |||
294 | expect(res.body.total).to.be.greaterThan(0) | ||
295 | expect(res.body.data).to.have.length.greaterThan(0) | ||
296 | |||
297 | const videoPlaylist: VideoPlaylist = res.body.data[0] | ||
298 | |||
299 | expect(videoPlaylist.url).to.equal('https://peertube2.cpy.re/videos/watch/playlist/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
300 | expect(videoPlaylist.thumbnailUrl).to.exist | ||
301 | expect(videoPlaylist.embedUrl).to.equal('https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
302 | |||
303 | expect(videoPlaylist.type.id).to.equal(VideoPlaylistType.REGULAR) | ||
304 | expect(videoPlaylist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) | ||
305 | expect(videoPlaylist.videosLength).to.exist | ||
306 | |||
307 | expect(videoPlaylist.createdAt).to.exist | ||
308 | expect(videoPlaylist.updatedAt).to.exist | ||
309 | |||
310 | expect(videoPlaylist.uuid).to.equal('73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
311 | expect(videoPlaylist.displayName).to.exist | ||
312 | |||
313 | expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz') | ||
314 | expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz') | ||
315 | expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re') | ||
316 | expect(videoPlaylist.ownerAccount.avatar).to.exist | ||
317 | |||
318 | expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel') | ||
319 | expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel') | ||
320 | expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re') | ||
321 | expect(videoPlaylist.videoChannel.avatar).to.exist | ||
322 | }) | ||
323 | |||
324 | it('Should have a correct pagination', async function () { | ||
325 | const res = await advancedVideoChannelSearch(server.url, { search: 'root', start: 0, count: 2 }) | ||
326 | |||
327 | expect(res.body.total).to.be.greaterThan(2) | ||
328 | expect(res.body.data).to.have.lengthOf(2) | ||
329 | }) | ||
330 | }) | ||
331 | |||
280 | after(async function () { | 332 | after(async function () { |
281 | await cleanupTests([ server ]) | 333 | await cleanupTests([ server ]) |
282 | }) | 334 | }) |
diff --git a/server/tests/api/search/search-playlists.ts b/server/tests/api/search/search-playlists.ts new file mode 100644 index 000000000..ab17d55e9 --- /dev/null +++ b/server/tests/api/search/search-playlists.ts | |||
@@ -0,0 +1,128 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { VideoPlaylist, VideoPlaylistPrivacy } from '@shared/models' | ||
6 | import { | ||
7 | addVideoInPlaylist, | ||
8 | advancedVideoPlaylistSearch, | ||
9 | cleanupTests, | ||
10 | createVideoPlaylist, | ||
11 | flushAndRunServer, | ||
12 | searchVideoPlaylists, | ||
13 | ServerInfo, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | uploadVideoAndGetId | ||
17 | } from '../../../../shared/extra-utils' | ||
18 | |||
19 | const expect = chai.expect | ||
20 | |||
21 | describe('Test playlists search', function () { | ||
22 | let server: ServerInfo = null | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await flushAndRunServer(1) | ||
28 | |||
29 | await setAccessTokensToServers([ server ]) | ||
30 | await setDefaultVideoChannel([ server ]) | ||
31 | |||
32 | const videoId = (await uploadVideoAndGetId({ server: server, videoName: 'video' })).uuid | ||
33 | |||
34 | { | ||
35 | const attributes = { | ||
36 | displayName: 'Dr. Kenzo Tenma hospital videos', | ||
37 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
38 | videoChannelId: server.videoChannel.id | ||
39 | } | ||
40 | const res = await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes }) | ||
41 | |||
42 | await addVideoInPlaylist({ | ||
43 | url: server.url, | ||
44 | token: server.accessToken, | ||
45 | playlistId: res.body.videoPlaylist.id, | ||
46 | elementAttrs: { videoId } | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | { | ||
51 | const attributes = { | ||
52 | displayName: 'Johan & Anna Libert musics', | ||
53 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
54 | videoChannelId: server.videoChannel.id | ||
55 | } | ||
56 | const res = await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes }) | ||
57 | |||
58 | await addVideoInPlaylist({ | ||
59 | url: server.url, | ||
60 | token: server.accessToken, | ||
61 | playlistId: res.body.videoPlaylist.id, | ||
62 | elementAttrs: { videoId } | ||
63 | }) | ||
64 | } | ||
65 | |||
66 | { | ||
67 | const attributes = { | ||
68 | displayName: 'Inspector Lunge playlist', | ||
69 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
70 | videoChannelId: server.videoChannel.id | ||
71 | } | ||
72 | await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes }) | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | it('Should make a simple search and not have results', async function () { | ||
77 | const res = await searchVideoPlaylists(server.url, 'abc') | ||
78 | |||
79 | expect(res.body.total).to.equal(0) | ||
80 | expect(res.body.data).to.have.lengthOf(0) | ||
81 | }) | ||
82 | |||
83 | it('Should make a search and have results', async function () { | ||
84 | { | ||
85 | const search = { | ||
86 | search: 'tenma', | ||
87 | start: 0, | ||
88 | count: 1 | ||
89 | } | ||
90 | const res = await advancedVideoPlaylistSearch(server.url, search) | ||
91 | expect(res.body.total).to.equal(1) | ||
92 | expect(res.body.data).to.have.lengthOf(1) | ||
93 | |||
94 | const playlist: VideoPlaylist = res.body.data[0] | ||
95 | expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos') | ||
96 | expect(playlist.url).to.equal(server.url + '/video-playlists/' + playlist.uuid) | ||
97 | } | ||
98 | |||
99 | { | ||
100 | const search = { | ||
101 | search: 'Anna Livert', | ||
102 | start: 0, | ||
103 | count: 1 | ||
104 | } | ||
105 | const res = await advancedVideoPlaylistSearch(server.url, search) | ||
106 | expect(res.body.total).to.equal(1) | ||
107 | expect(res.body.data).to.have.lengthOf(1) | ||
108 | |||
109 | const playlist: VideoPlaylist = res.body.data[0] | ||
110 | expect(playlist.displayName).to.equal('Johan & Anna Libert musics') | ||
111 | } | ||
112 | }) | ||
113 | |||
114 | it('Should not display playlists without videos', async function () { | ||
115 | const search = { | ||
116 | search: 'Lunge', | ||
117 | start: 0, | ||
118 | count: 1 | ||
119 | } | ||
120 | const res = await advancedVideoPlaylistSearch(server.url, search) | ||
121 | expect(res.body.total).to.equal(0) | ||
122 | expect(res.body.data).to.have.lengthOf(0) | ||
123 | }) | ||
124 | |||
125 | after(async function () { | ||
126 | await cleanupTests([ server ]) | ||
127 | }) | ||
128 | }) | ||
diff --git a/server/tests/api/server/bulk.ts b/server/tests/api/server/bulk.ts index 51ba0e7af..80fa7fce6 100644 --- a/server/tests/api/server/bulk.ts +++ b/server/tests/api/server/bulk.ts | |||
@@ -2,12 +2,14 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { VideoComment } from '@shared/models/videos/video-comment.model' | 5 | import { Video, VideoComment } from '@shared/models' |
6 | import { | 6 | import { |
7 | addVideoCommentReply, | ||
7 | addVideoCommentThread, | 8 | addVideoCommentThread, |
8 | bulkRemoveCommentsOf, | 9 | bulkRemoveCommentsOf, |
9 | cleanupTests, | 10 | cleanupTests, |
10 | createUser, | 11 | createUser, |
12 | doubleFollow, | ||
11 | flushAndRunMultipleServers, | 13 | flushAndRunMultipleServers, |
12 | getVideoCommentThreads, | 14 | getVideoCommentThreads, |
13 | getVideosList, | 15 | getVideosList, |
@@ -15,11 +17,8 @@ import { | |||
15 | setAccessTokensToServers, | 17 | setAccessTokensToServers, |
16 | uploadVideo, | 18 | uploadVideo, |
17 | userLogin, | 19 | userLogin, |
18 | waitJobs, | 20 | waitJobs |
19 | addVideoCommentReply | ||
20 | } from '../../../../shared/extra-utils/index' | 21 | } from '../../../../shared/extra-utils/index' |
21 | import { doubleFollow } from '../../../../shared/extra-utils/server/follows' | ||
22 | import { Video } from '@shared/models' | ||
23 | 22 | ||
24 | const expect = chai.expect | 23 | const expect = chai.expect |
25 | 24 | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 1d9ea31df..19bf9582c 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -60,6 +60,7 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) { | |||
60 | 60 | ||
61 | expect(data.signup.enabled).to.be.true | 61 | expect(data.signup.enabled).to.be.true |
62 | expect(data.signup.limit).to.equal(4) | 62 | expect(data.signup.limit).to.equal(4) |
63 | expect(data.signup.minimumAge).to.equal(16) | ||
63 | expect(data.signup.requiresEmailVerification).to.be.false | 64 | expect(data.signup.requiresEmailVerification).to.be.false |
64 | 65 | ||
65 | expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com') | 66 | expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com') |
@@ -151,6 +152,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
151 | expect(data.signup.enabled).to.be.false | 152 | expect(data.signup.enabled).to.be.false |
152 | expect(data.signup.limit).to.equal(5) | 153 | expect(data.signup.limit).to.equal(5) |
153 | expect(data.signup.requiresEmailVerification).to.be.false | 154 | expect(data.signup.requiresEmailVerification).to.be.false |
155 | expect(data.signup.minimumAge).to.equal(10) | ||
154 | 156 | ||
155 | // We override admin email in parallel tests, so skip this exception | 157 | // We override admin email in parallel tests, so skip this exception |
156 | if (parallelTests() === false) { | 158 | if (parallelTests() === false) { |
@@ -316,7 +318,8 @@ describe('Test config', function () { | |||
316 | signup: { | 318 | signup: { |
317 | enabled: false, | 319 | enabled: false, |
318 | limit: 5, | 320 | limit: 5, |
319 | requiresEmailVerification: false | 321 | requiresEmailVerification: false, |
322 | minimumAge: 10 | ||
320 | }, | 323 | }, |
321 | admin: { | 324 | admin: { |
322 | email: 'superadmin1@example.com' | 325 | email: 'superadmin1@example.com' |
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts index 9b4af1915..8851ad55e 100644 --- a/server/tests/api/server/contact-form.ts +++ b/server/tests/api/server/contact-form.ts | |||
@@ -54,6 +54,10 @@ describe('Test contact form', function () { | |||
54 | }) | 54 | }) |
55 | 55 | ||
56 | it('Should not be able to send another contact form because of the anti spam checker', async function () { | 56 | it('Should not be able to send another contact form because of the anti spam checker', async function () { |
57 | this.timeout(10000) | ||
58 | |||
59 | await wait(1000) | ||
60 | |||
57 | await sendContactForm({ | 61 | await sendContactForm({ |
58 | url: server.url, | 62 | url: server.url, |
59 | fromEmail: 'toto@example.com', | 63 | fromEmail: 'toto@example.com', |
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts index 8a91fbba3..3f2f71f46 100644 --- a/server/tests/api/server/follow-constraints.ts +++ b/server/tests/api/server/follow-constraints.ts | |||
@@ -18,6 +18,7 @@ import { unfollow } from '../../../../shared/extra-utils/server/follows' | |||
18 | import { userLogin } from '../../../../shared/extra-utils/users/login' | 18 | import { userLogin } from '../../../../shared/extra-utils/users/login' |
19 | import { createUser } from '../../../../shared/extra-utils/users/users' | 19 | import { createUser } from '../../../../shared/extra-utils/users/users' |
20 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 20 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
21 | import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models' | ||
21 | 22 | ||
22 | const expect = chai.expect | 23 | const expect = chai.expect |
23 | 24 | ||
@@ -153,7 +154,20 @@ describe('Test follow constraints', function () { | |||
153 | }) | 154 | }) |
154 | 155 | ||
155 | it('Should not get the remote video', async function () { | 156 | it('Should not get the remote video', async function () { |
156 | await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403) | 157 | const res = await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403) |
158 | |||
159 | const error = res.body as PeerTubeProblemDocument | ||
160 | |||
161 | const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints' | ||
162 | expect(error.type).to.equal(doc) | ||
163 | expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS) | ||
164 | |||
165 | expect(error.detail).to.equal('Cannot get this video regarding follow constraints') | ||
166 | expect(error.error).to.equal(error.detail) | ||
167 | |||
168 | expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
169 | |||
170 | expect(error.originUrl).to.contains(servers[1].url) | ||
157 | }) | 171 | }) |
158 | 172 | ||
159 | it('Should list local account videos', async function () { | 173 | it('Should list local account videos', async function () { |
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index eb9ab10eb..9e5aa00c7 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts | |||
@@ -1,37 +1,35 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
5 | import { Video, VideoPrivacy } from '../../../../shared/models/videos' | 4 | import * as chai from 'chai' |
6 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | ||
7 | import { cleanupTests, completeVideoCheck, deleteVideoComment } from '../../../../shared/extra-utils' | ||
8 | import { | 5 | import { |
6 | addVideoCommentReply, | ||
7 | addVideoCommentThread, | ||
8 | cleanupTests, | ||
9 | completeVideoCheck, | ||
10 | createUser, | ||
11 | createVideoCaption, | ||
12 | dateIsValid, | ||
13 | deleteVideoComment, | ||
14 | expectAccountFollows, | ||
9 | flushAndRunMultipleServers, | 15 | flushAndRunMultipleServers, |
10 | getVideosList, | ||
11 | ServerInfo, | ||
12 | setAccessTokensToServers, | ||
13 | uploadVideo | ||
14 | } from '../../../../shared/extra-utils/index' | ||
15 | import { dateIsValid } from '../../../../shared/extra-utils/miscs/miscs' | ||
16 | import { | ||
17 | follow, | 16 | follow, |
18 | getFollowersListPaginationAndSort, | 17 | getFollowersListPaginationAndSort, |
19 | getFollowingListPaginationAndSort, | 18 | getFollowingListPaginationAndSort, |
20 | unfollow | ||
21 | } from '../../../../shared/extra-utils/server/follows' | ||
22 | import { expectAccountFollows } from '../../../../shared/extra-utils/users/accounts' | ||
23 | import { userLogin } from '../../../../shared/extra-utils/users/login' | ||
24 | import { createUser } from '../../../../shared/extra-utils/users/users' | ||
25 | import { | ||
26 | addVideoCommentReply, | ||
27 | addVideoCommentThread, | ||
28 | getVideoCommentThreads, | 19 | getVideoCommentThreads, |
29 | getVideoThreadComments | 20 | getVideosList, |
30 | } from '../../../../shared/extra-utils/videos/video-comments' | 21 | getVideoThreadComments, |
31 | import { rateVideo } from '../../../../shared/extra-utils/videos/videos' | 22 | listVideoCaptions, |
32 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 23 | rateVideo, |
33 | import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/extra-utils/videos/video-captions' | 24 | ServerInfo, |
34 | import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' | 25 | setAccessTokensToServers, |
26 | testCaptionFile, | ||
27 | unfollow, | ||
28 | uploadVideo, | ||
29 | userLogin, | ||
30 | waitJobs | ||
31 | } from '@shared/extra-utils' | ||
32 | import { Video, VideoCaption, VideoComment, VideoCommentThreadTree, VideoPrivacy } from '@shared/models' | ||
35 | 33 | ||
36 | const expect = chai.expect | 34 | const expect = chai.expect |
37 | 35 | ||
@@ -521,7 +519,7 @@ describe('Test follows', function () { | |||
521 | expect(deletedComment.text).to.equal('') | 519 | expect(deletedComment.text).to.equal('') |
522 | expect(deletedComment.inReplyToCommentId).to.be.null | 520 | expect(deletedComment.inReplyToCommentId).to.be.null |
523 | expect(deletedComment.account).to.be.null | 521 | expect(deletedComment.account).to.be.null |
524 | expect(deletedComment.totalReplies).to.equal(3) | 522 | expect(deletedComment.totalReplies).to.equal(2) |
525 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | 523 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true |
526 | 524 | ||
527 | const res2 = await getVideoThreadComments(servers[0].url, video4.id, deletedComment.threadId) | 525 | const res2 = await getVideoThreadComments(servers[0].url, video4.id, deletedComment.threadId) |
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts index 817c79f6e..d57d72f5e 100644 --- a/server/tests/api/server/handle-down.ts +++ b/server/tests/api/server/handle-down.ts | |||
@@ -4,7 +4,7 @@ import * as chai from 'chai' | |||
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { JobState, Video } from '../../../../shared/models' | 5 | import { JobState, Video } from '../../../../shared/models' |
6 | import { VideoPrivacy } from '../../../../shared/models/videos' | 6 | import { VideoPrivacy } from '../../../../shared/models/videos' |
7 | import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | 7 | import { VideoCommentThreadTree } from '../../../../shared/models/videos/comment/video-comment.model' |
8 | 8 | ||
9 | import { | 9 | import { |
10 | cleanupTests, | 10 | cleanupTests, |
@@ -47,7 +47,7 @@ describe('Test handle downs', function () { | |||
47 | let missedVideo2: Video | 47 | let missedVideo2: Video |
48 | let unlistedVideo: Video | 48 | let unlistedVideo: Video |
49 | 49 | ||
50 | const videoIdsServer1: number[] = [] | 50 | const videoIdsServer1: string[] = [] |
51 | 51 | ||
52 | const videoAttributes = { | 52 | const videoAttributes = { |
53 | name: 'my super name for server 1', | 53 | name: 'my super name for server 1', |
@@ -346,10 +346,12 @@ describe('Test handle downs', function () { | |||
346 | // Wait video expiration | 346 | // Wait video expiration |
347 | await wait(11000) | 347 | await wait(11000) |
348 | 348 | ||
349 | for (let i = 0; i < 3; i++) { | 349 | for (let i = 0; i < 5; i++) { |
350 | await getVideo(servers[1].url, videoIdsServer1[i]) | 350 | try { |
351 | await waitJobs([ servers[1] ]) | 351 | await getVideo(servers[1].url, videoIdsServer1[i]) |
352 | await wait(1500) | 352 | await waitJobs([ servers[1] ]) |
353 | await wait(1500) | ||
354 | } catch {} | ||
353 | } | 355 | } |
354 | 356 | ||
355 | for (const id of videoIdsServer1) { | 357 | for (const id of videoIdsServer1) { |
diff --git a/server/tests/api/server/homepage.ts b/server/tests/api/server/homepage.ts new file mode 100644 index 000000000..e8ba89ca6 --- /dev/null +++ b/server/tests/api/server/homepage.ts | |||
@@ -0,0 +1,85 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { HttpStatusCode } from '@shared/core-utils' | ||
6 | import { CustomPage, ServerConfig } from '@shared/models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | flushAndRunServer, | ||
10 | getConfig, | ||
11 | getInstanceHomepage, | ||
12 | killallServers, | ||
13 | reRunServer, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | updateInstanceHomepage | ||
17 | } from '../../../../shared/extra-utils/index' | ||
18 | |||
19 | const expect = chai.expect | ||
20 | |||
21 | async function getHomepageState (server: ServerInfo) { | ||
22 | const res = await getConfig(server.url) | ||
23 | |||
24 | const config = res.body as ServerConfig | ||
25 | return config.homepage.enabled | ||
26 | } | ||
27 | |||
28 | describe('Test instance homepage actions', function () { | ||
29 | let server: ServerInfo | ||
30 | |||
31 | before(async function () { | ||
32 | this.timeout(30000) | ||
33 | |||
34 | server = await flushAndRunServer(1) | ||
35 | await setAccessTokensToServers([ server ]) | ||
36 | }) | ||
37 | |||
38 | it('Should not have a homepage', async function () { | ||
39 | const state = await getHomepageState(server) | ||
40 | expect(state).to.be.false | ||
41 | |||
42 | await getInstanceHomepage(server.url, HttpStatusCode.NOT_FOUND_404) | ||
43 | }) | ||
44 | |||
45 | it('Should set a homepage', async function () { | ||
46 | await updateInstanceHomepage(server.url, server.accessToken, '<picsou-magazine></picsou-magazine>') | ||
47 | |||
48 | const res = await getInstanceHomepage(server.url) | ||
49 | const page: CustomPage = res.body | ||
50 | expect(page.content).to.equal('<picsou-magazine></picsou-magazine>') | ||
51 | |||
52 | const state = await getHomepageState(server) | ||
53 | expect(state).to.be.true | ||
54 | }) | ||
55 | |||
56 | it('Should have the same homepage after a restart', async function () { | ||
57 | this.timeout(30000) | ||
58 | |||
59 | killallServers([ server ]) | ||
60 | |||
61 | await reRunServer(server) | ||
62 | |||
63 | const res = await getInstanceHomepage(server.url) | ||
64 | const page: CustomPage = res.body | ||
65 | expect(page.content).to.equal('<picsou-magazine></picsou-magazine>') | ||
66 | |||
67 | const state = await getHomepageState(server) | ||
68 | expect(state).to.be.true | ||
69 | }) | ||
70 | |||
71 | it('Should empty the homepage', async function () { | ||
72 | await updateInstanceHomepage(server.url, server.accessToken, '') | ||
73 | |||
74 | const res = await getInstanceHomepage(server.url) | ||
75 | const page: CustomPage = res.body | ||
76 | expect(page.content).to.be.empty | ||
77 | |||
78 | const state = await getHomepageState(server) | ||
79 | expect(state).to.be.false | ||
80 | }) | ||
81 | |||
82 | after(async function () { | ||
83 | await cleanupTests([ server ]) | ||
84 | }) | ||
85 | }) | ||
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index be743973a..56e6eb5da 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts | |||
@@ -5,6 +5,7 @@ import './email' | |||
5 | import './follow-constraints' | 5 | import './follow-constraints' |
6 | import './follows' | 6 | import './follows' |
7 | import './follows-moderation' | 7 | import './follows-moderation' |
8 | import './homepage' | ||
8 | import './handle-down' | 9 | import './handle-down' |
9 | import './jobs' | 10 | import './jobs' |
10 | import './logs' | 11 | import './logs' |
diff --git a/server/tests/api/server/plugins.ts b/server/tests/api/server/plugins.ts index f4190c352..6b61c7c33 100644 --- a/server/tests/api/server/plugins.ts +++ b/server/tests/api/server/plugins.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { HttpStatusCode } from '@shared/core-utils' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | closeAllSequelize, | 8 | closeAllSequelize, |
@@ -10,6 +11,7 @@ import { | |||
10 | getMyUserInformation, | 11 | getMyUserInformation, |
11 | getPlugin, | 12 | getPlugin, |
12 | getPluginPackageJSON, | 13 | getPluginPackageJSON, |
14 | getPluginTestPath, | ||
13 | getPublicSettings, | 15 | getPublicSettings, |
14 | installPlugin, | 16 | installPlugin, |
15 | killallServers, | 17 | killallServers, |
@@ -28,14 +30,8 @@ import { | |||
28 | updatePluginSettings, | 30 | updatePluginSettings, |
29 | wait, | 31 | wait, |
30 | waitUntilLog | 32 | waitUntilLog |
31 | } from '../../../../shared/extra-utils' | 33 | } from '@shared/extra-utils' |
32 | import { PeerTubePluginIndex } from '../../../../shared/models/plugins/peertube-plugin-index.model' | 34 | import { PeerTubePlugin, PeerTubePluginIndex, PluginPackageJson, PluginType, PublicServerSetting, ServerConfig, User } from '@shared/models' |
33 | import { PeerTubePlugin } from '../../../../shared/models/plugins/peertube-plugin.model' | ||
34 | import { PluginPackageJson } from '../../../../shared/models/plugins/plugin-package-json.model' | ||
35 | import { PluginType } from '../../../../shared/models/plugins/plugin.type' | ||
36 | import { PublicServerSetting } from '../../../../shared/models/plugins/public-server.setting' | ||
37 | import { ServerConfig } from '../../../../shared/models/server' | ||
38 | import { User } from '../../../../shared/models/users' | ||
39 | 35 | ||
40 | const expect = chai.expect | 36 | const expect = chai.expect |
41 | 37 | ||
@@ -406,6 +402,36 @@ describe('Test plugins', function () { | |||
406 | expect((res.body as User).theme).to.equal('instance-default') | 402 | expect((res.body as User).theme).to.equal('instance-default') |
407 | }) | 403 | }) |
408 | 404 | ||
405 | it('Should not install a broken plugin', async function () { | ||
406 | this.timeout(60000) | ||
407 | |||
408 | async function check () { | ||
409 | const res = await listPlugins({ | ||
410 | url: server.url, | ||
411 | accessToken: server.accessToken, | ||
412 | pluginType: PluginType.PLUGIN | ||
413 | }) | ||
414 | |||
415 | const plugins: PeerTubePlugin[] = res.body.data | ||
416 | |||
417 | expect(plugins.find(p => p.name === 'test-broken')).to.not.exist | ||
418 | } | ||
419 | |||
420 | await installPlugin({ | ||
421 | url: server.url, | ||
422 | accessToken: server.accessToken, | ||
423 | path: getPluginTestPath('-broken'), | ||
424 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
425 | }) | ||
426 | |||
427 | await check() | ||
428 | |||
429 | killallServers([ server ]) | ||
430 | await reRunServer(server) | ||
431 | |||
432 | await check() | ||
433 | }) | ||
434 | |||
409 | after(async function () { | 435 | after(async function () { |
410 | await closeAllSequelize([ server ]) | 436 | await closeAllSequelize([ server ]) |
411 | await cleanupTests([ server ]) | 437 | await cleanupTests([ server ]) |
diff --git a/server/tests/api/server/services.ts b/server/tests/api/server/services.ts index f0fa91674..ea64e4040 100644 --- a/server/tests/api/server/services.ts +++ b/server/tests/api/server/services.ts | |||
@@ -67,61 +67,67 @@ describe('Test services', function () { | |||
67 | }) | 67 | }) |
68 | 68 | ||
69 | it('Should have a valid oEmbed video response', async function () { | 69 | it('Should have a valid oEmbed video response', async function () { |
70 | const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + video.uuid | 70 | for (const basePath of [ '/videos/watch/', '/w/' ]) { |
71 | 71 | const oembedUrl = 'http://localhost:' + server.port + basePath + video.uuid | |
72 | const res = await getOEmbed(server.url, oembedUrl) | 72 | |
73 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + | 73 | const res = await getOEmbed(server.url, oembedUrl) |
74 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 74 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + |
75 | 'frameborder="0" allowfullscreen></iframe>' | 75 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + |
76 | const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath | 76 | 'frameborder="0" allowfullscreen></iframe>' |
77 | 77 | const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath | |
78 | expect(res.body.html).to.equal(expectedHtml) | 78 | |
79 | expect(res.body.title).to.equal(video.name) | 79 | expect(res.body.html).to.equal(expectedHtml) |
80 | expect(res.body.author_name).to.equal(server.videoChannel.displayName) | 80 | expect(res.body.title).to.equal(video.name) |
81 | expect(res.body.width).to.equal(560) | 81 | expect(res.body.author_name).to.equal(server.videoChannel.displayName) |
82 | expect(res.body.height).to.equal(315) | 82 | expect(res.body.width).to.equal(560) |
83 | expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) | 83 | expect(res.body.height).to.equal(315) |
84 | expect(res.body.thumbnail_width).to.equal(850) | 84 | expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl) |
85 | expect(res.body.thumbnail_height).to.equal(480) | 85 | expect(res.body.thumbnail_width).to.equal(850) |
86 | expect(res.body.thumbnail_height).to.equal(480) | ||
87 | } | ||
86 | }) | 88 | }) |
87 | 89 | ||
88 | it('Should have a valid playlist oEmbed response', async function () { | 90 | it('Should have a valid playlist oEmbed response', async function () { |
89 | const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/playlist/' + playlistUUID | 91 | for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) { |
90 | 92 | const oembedUrl = 'http://localhost:' + server.port + basePath + playlistUUID | |
91 | const res = await getOEmbed(server.url, oembedUrl) | 93 | |
92 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + | 94 | const res = await getOEmbed(server.url, oembedUrl) |
93 | `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + | 95 | const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + |
94 | 'frameborder="0" allowfullscreen></iframe>' | 96 | `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + |
95 | 97 | 'frameborder="0" allowfullscreen></iframe>' | |
96 | expect(res.body.html).to.equal(expectedHtml) | 98 | |
97 | expect(res.body.title).to.equal('The Life and Times of Scrooge McDuck') | 99 | expect(res.body.html).to.equal(expectedHtml) |
98 | expect(res.body.author_name).to.equal(server.videoChannel.displayName) | 100 | expect(res.body.title).to.equal('The Life and Times of Scrooge McDuck') |
99 | expect(res.body.width).to.equal(560) | 101 | expect(res.body.author_name).to.equal(server.videoChannel.displayName) |
100 | expect(res.body.height).to.equal(315) | 102 | expect(res.body.width).to.equal(560) |
101 | expect(res.body.thumbnail_url).exist | 103 | expect(res.body.height).to.equal(315) |
102 | expect(res.body.thumbnail_width).to.equal(280) | 104 | expect(res.body.thumbnail_url).exist |
103 | expect(res.body.thumbnail_height).to.equal(157) | 105 | expect(res.body.thumbnail_width).to.equal(280) |
106 | expect(res.body.thumbnail_height).to.equal(157) | ||
107 | } | ||
104 | }) | 108 | }) |
105 | 109 | ||
106 | it('Should have a valid oEmbed response with small max height query', async function () { | 110 | it('Should have a valid oEmbed response with small max height query', async function () { |
107 | const oembedUrl = 'http://localhost:' + server.port + '/videos/watch/' + video.uuid | 111 | for (const basePath of [ '/videos/watch/', '/w/' ]) { |
108 | const format = 'json' | 112 | const oembedUrl = 'http://localhost:' + server.port + basePath + video.uuid |
109 | const maxHeight = 50 | 113 | const format = 'json' |
110 | const maxWidth = 50 | 114 | const maxHeight = 50 |
111 | 115 | const maxWidth = 50 | |
112 | const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) | 116 | |
113 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + | 117 | const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) |
114 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + | 118 | const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + |
115 | 'frameborder="0" allowfullscreen></iframe>' | 119 | `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + |
116 | 120 | 'frameborder="0" allowfullscreen></iframe>' | |
117 | expect(res.body.html).to.equal(expectedHtml) | 121 | |
118 | expect(res.body.title).to.equal(video.name) | 122 | expect(res.body.html).to.equal(expectedHtml) |
119 | expect(res.body.author_name).to.equal(server.videoChannel.displayName) | 123 | expect(res.body.title).to.equal(video.name) |
120 | expect(res.body.height).to.equal(50) | 124 | expect(res.body.author_name).to.equal(server.videoChannel.displayName) |
121 | expect(res.body.width).to.equal(50) | 125 | expect(res.body.height).to.equal(50) |
122 | expect(res.body).to.not.have.property('thumbnail_url') | 126 | expect(res.body.width).to.equal(50) |
123 | expect(res.body).to.not.have.property('thumbnail_width') | 127 | expect(res.body).to.not.have.property('thumbnail_url') |
124 | expect(res.body).to.not.have.property('thumbnail_height') | 128 | expect(res.body).to.not.have.property('thumbnail_width') |
129 | expect(res.body).to.not.have.property('thumbnail_height') | ||
130 | } | ||
125 | }) | 131 | }) |
126 | 132 | ||
127 | after(async function () { | 133 | after(async function () { |
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts index 1a9a519a0..e0f2f2112 100644 --- a/server/tests/api/users/users-verification.ts +++ b/server/tests/api/users/users-verification.ts | |||
@@ -19,6 +19,7 @@ import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/l | |||
19 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' | 19 | import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' |
20 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 20 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
21 | import { User } from '../../../../shared/models/users' | 21 | import { User } from '../../../../shared/models/users' |
22 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
22 | 23 | ||
23 | const expect = chai.expect | 24 | const expect = chai.expect |
24 | 25 | ||
@@ -89,8 +90,8 @@ describe('Test users account verification', function () { | |||
89 | }) | 90 | }) |
90 | 91 | ||
91 | it('Should not allow login for user with unverified email', async function () { | 92 | it('Should not allow login for user with unverified email', async function () { |
92 | const resLogin = await login(server.url, server.client, user1, 400) | 93 | const resLogin = await login(server.url, server.client, user1, HttpStatusCode.BAD_REQUEST_400) |
93 | expect(resLogin.body.error).to.contain('User email is not verified.') | 94 | expect(resLogin.body.detail).to.contain('User email is not verified.') |
94 | }) | 95 | }) |
95 | 96 | ||
96 | it('Should verify the user via email and allow login', async function () { | 97 | it('Should verify the user via email and allow login', async function () { |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index cea98aac7..87ba775f6 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' | 5 | import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' |
6 | import { CustomConfig } from '@shared/models/server' | 6 | import { CustomConfig, OAuth2ErrorCode } from '@shared/models/server' |
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
8 | import { | 8 | import { |
9 | addVideoCommentThread, | 9 | addVideoCommentThread, |
@@ -93,16 +93,20 @@ describe('Test users', function () { | |||
93 | const client = { id: 'client', secret: server.client.secret } | 93 | const client = { id: 'client', secret: server.client.secret } |
94 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) | 94 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) |
95 | 95 | ||
96 | expect(res.body.code).to.equal('invalid_client') | 96 | expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) |
97 | expect(res.body.error).to.contain('client is invalid') | 97 | expect(res.body.error).to.contain('client is invalid') |
98 | expect(res.body.type.startsWith('https://')).to.be.true | ||
99 | expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
98 | }) | 100 | }) |
99 | 101 | ||
100 | it('Should not login with an invalid client secret', async function () { | 102 | it('Should not login with an invalid client secret', async function () { |
101 | const client = { id: server.client.id, secret: 'coucou' } | 103 | const client = { id: server.client.id, secret: 'coucou' } |
102 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) | 104 | const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) |
103 | 105 | ||
104 | expect(res.body.code).to.equal('invalid_client') | 106 | expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) |
105 | expect(res.body.error).to.contain('client is invalid') | 107 | expect(res.body.error).to.contain('client is invalid') |
108 | expect(res.body.type.startsWith('https://')).to.be.true | ||
109 | expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
106 | }) | 110 | }) |
107 | }) | 111 | }) |
108 | 112 | ||
@@ -112,16 +116,20 @@ describe('Test users', function () { | |||
112 | const user = { username: 'captain crochet', password: server.user.password } | 116 | const user = { username: 'captain crochet', password: server.user.password } |
113 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) | 117 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) |
114 | 118 | ||
115 | expect(res.body.code).to.equal('invalid_grant') | 119 | expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) |
116 | expect(res.body.error).to.contain('credentials are invalid') | 120 | expect(res.body.error).to.contain('credentials are invalid') |
121 | expect(res.body.type.startsWith('https://')).to.be.true | ||
122 | expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
117 | }) | 123 | }) |
118 | 124 | ||
119 | it('Should not login with an invalid password', async function () { | 125 | it('Should not login with an invalid password', async function () { |
120 | const user = { username: server.user.username, password: 'mew_three' } | 126 | const user = { username: server.user.username, password: 'mew_three' } |
121 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) | 127 | const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) |
122 | 128 | ||
123 | expect(res.body.code).to.equal('invalid_grant') | 129 | expect(res.body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) |
124 | expect(res.body.error).to.contain('credentials are invalid') | 130 | expect(res.body.error).to.contain('credentials are invalid') |
131 | expect(res.body.type.startsWith('https://')).to.be.true | ||
132 | expect(res.body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
125 | }) | 133 | }) |
126 | 134 | ||
127 | it('Should not be able to upload a video', async function () { | 135 | it('Should not be able to upload a video', async function () { |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 41cd814e0..a8c8a889b 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -1,13 +1,12 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
5 | import { join } from 'path' | 4 | import * as chai from 'chai' |
6 | import * as request from 'supertest' | 5 | import * as request from 'supertest' |
7 | import { VideoPrivacy } from '../../../../shared/models/videos' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
8 | import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | ||
9 | import { | 7 | import { |
10 | addVideoChannel, | 8 | addVideoChannel, |
9 | buildAbsoluteFixturePath, | ||
11 | checkTmpIsEmpty, | 10 | checkTmpIsEmpty, |
12 | checkVideoFilesWereRemoved, | 11 | checkVideoFilesWereRemoved, |
13 | cleanupTests, | 12 | cleanupTests, |
@@ -32,16 +31,16 @@ import { | |||
32 | wait, | 31 | wait, |
33 | webtorrentAdd | 32 | webtorrentAdd |
34 | } from '../../../../shared/extra-utils' | 33 | } from '../../../../shared/extra-utils' |
34 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
35 | import { | 35 | import { |
36 | addVideoCommentReply, | 36 | addVideoCommentReply, |
37 | addVideoCommentThread, | 37 | addVideoCommentThread, |
38 | deleteVideoComment, | 38 | deleteVideoComment, |
39 | findCommentId, | ||
39 | getVideoCommentThreads, | 40 | getVideoCommentThreads, |
40 | getVideoThreadComments, | 41 | getVideoThreadComments |
41 | findCommentId | ||
42 | } from '../../../../shared/extra-utils/videos/video-comments' | 42 | } from '../../../../shared/extra-utils/videos/video-comments' |
43 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 43 | import { VideoComment, VideoCommentThreadTree, VideoPrivacy } from '../../../../shared/models/videos' |
44 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
45 | 44 | ||
46 | const expect = chai.expect | 45 | const expect = chai.expect |
47 | 46 | ||
@@ -935,7 +934,7 @@ describe('Test multiple servers', function () { | |||
935 | expect(deletedComment.text).to.equal('') | 934 | expect(deletedComment.text).to.equal('') |
936 | expect(deletedComment.inReplyToCommentId).to.be.null | 935 | expect(deletedComment.inReplyToCommentId).to.be.null |
937 | expect(deletedComment.account).to.be.null | 936 | expect(deletedComment.account).to.be.null |
938 | expect(deletedComment.totalReplies).to.equal(3) | 937 | expect(deletedComment.totalReplies).to.equal(2) |
939 | expect(dateIsValid(deletedComment.createdAt as string)).to.be.true | 938 | expect(dateIsValid(deletedComment.createdAt as string)).to.be.true |
940 | expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true | 939 | expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true |
941 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true | 940 | expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true |
@@ -977,7 +976,7 @@ describe('Test multiple servers', function () { | |||
977 | expect(comment.createdAt).to.not.be.null | 976 | expect(comment.createdAt).to.not.be.null |
978 | expect(comment.deletedAt).to.not.be.null | 977 | expect(comment.deletedAt).to.not.be.null |
979 | expect(comment.account).to.be.null | 978 | expect(comment.account).to.be.null |
980 | expect(comment.totalReplies).to.equal(3) | 979 | expect(comment.totalReplies).to.equal(2) |
981 | } | 980 | } |
982 | } | 981 | } |
983 | }) | 982 | }) |
@@ -1019,9 +1018,7 @@ describe('Test multiple servers', function () { | |||
1019 | .field('privacy', '1') | 1018 | .field('privacy', '1') |
1020 | .field('channelId', '1') | 1019 | .field('channelId', '1') |
1021 | 1020 | ||
1022 | const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm') | 1021 | await req.attach('videofile', buildAbsoluteFixturePath('video_short.webm')) |
1023 | |||
1024 | await req.attach('videofile', filePath) | ||
1025 | .expect(HttpStatusCode.OK_200) | 1022 | .expect(HttpStatusCode.OK_200) |
1026 | 1023 | ||
1027 | await waitJobs(servers) | 1024 | await waitJobs(servers) |
diff --git a/server/tests/api/videos/resumable-upload.ts b/server/tests/api/videos/resumable-upload.ts index af9221c43..4fc3317df 100644 --- a/server/tests/api/videos/resumable-upload.ts +++ b/server/tests/api/videos/resumable-upload.ts | |||
@@ -8,6 +8,7 @@ import { HttpStatusCode } from '@shared/core-utils' | |||
8 | import { | 8 | import { |
9 | buildAbsoluteFixturePath, | 9 | buildAbsoluteFixturePath, |
10 | buildServerDirectory, | 10 | buildServerDirectory, |
11 | cleanupTests, | ||
11 | flushAndRunServer, | 12 | flushAndRunServer, |
12 | getMyUserInformation, | 13 | getMyUserInformation, |
13 | prepareResumableUpload, | 14 | prepareResumableUpload, |
@@ -184,4 +185,7 @@ describe('Test resumable upload', function () { | |||
184 | }) | 185 | }) |
185 | }) | 186 | }) |
186 | 187 | ||
188 | after(async function () { | ||
189 | await cleanupTests([ server ]) | ||
190 | }) | ||
187 | }) | 191 | }) |
diff --git a/server/tests/api/videos/video-change-ownership.ts b/server/tests/api/videos/video-change-ownership.ts index fad4c8b1f..a3384851b 100644 --- a/server/tests/api/videos/video-change-ownership.ts +++ b/server/tests/api/videos/video-change-ownership.ts | |||
@@ -1,11 +1,13 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { | 6 | import { |
6 | acceptChangeOwnership, | 7 | acceptChangeOwnership, |
7 | changeVideoOwnership, | 8 | changeVideoOwnership, |
8 | cleanupTests, | 9 | cleanupTests, |
10 | createLive, | ||
9 | createUser, | 11 | createUser, |
10 | doubleFollow, | 12 | doubleFollow, |
11 | flushAndRunMultipleServers, | 13 | flushAndRunMultipleServers, |
@@ -17,13 +19,14 @@ import { | |||
17 | refuseChangeOwnership, | 19 | refuseChangeOwnership, |
18 | ServerInfo, | 20 | ServerInfo, |
19 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
22 | setDefaultVideoChannel, | ||
23 | updateCustomSubConfig, | ||
20 | uploadVideo, | 24 | uploadVideo, |
21 | userLogin | 25 | userLogin |
22 | } from '../../../../shared/extra-utils' | 26 | } from '../../../../shared/extra-utils' |
23 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 27 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' |
24 | import { User } from '../../../../shared/models/users' | 28 | import { User } from '../../../../shared/models/users' |
25 | import { VideoDetails } from '../../../../shared/models/videos' | 29 | import { VideoDetails, VideoPrivacy } from '../../../../shared/models/videos' |
26 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
27 | 30 | ||
28 | const expect = chai.expect | 31 | const expect = chai.expect |
29 | 32 | ||
@@ -37,15 +40,32 @@ describe('Test video change ownership - nominal', function () { | |||
37 | username: 'second', | 40 | username: 'second', |
38 | password: 'My other password' | 41 | password: 'My other password' |
39 | } | 42 | } |
43 | |||
40 | let firstUserAccessToken = '' | 44 | let firstUserAccessToken = '' |
45 | let firstUserChannelId: number | ||
46 | |||
41 | let secondUserAccessToken = '' | 47 | let secondUserAccessToken = '' |
48 | let secondUserChannelId: number | ||
49 | |||
42 | let lastRequestChangeOwnershipId = '' | 50 | let lastRequestChangeOwnershipId = '' |
43 | 51 | ||
52 | let liveId: number | ||
53 | |||
44 | before(async function () { | 54 | before(async function () { |
45 | this.timeout(50000) | 55 | this.timeout(50000) |
46 | 56 | ||
47 | servers = await flushAndRunMultipleServers(2) | 57 | servers = await flushAndRunMultipleServers(2) |
48 | await setAccessTokensToServers(servers) | 58 | await setAccessTokensToServers(servers) |
59 | await setDefaultVideoChannel(servers) | ||
60 | |||
61 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
62 | transcoding: { | ||
63 | enabled: false | ||
64 | }, | ||
65 | live: { | ||
66 | enabled: true | ||
67 | } | ||
68 | }) | ||
49 | 69 | ||
50 | const videoQuota = 42000000 | 70 | const videoQuota = 42000000 |
51 | await createUser({ | 71 | await createUser({ |
@@ -66,22 +86,35 @@ describe('Test video change ownership - nominal', function () { | |||
66 | firstUserAccessToken = await userLogin(servers[0], firstUser) | 86 | firstUserAccessToken = await userLogin(servers[0], firstUser) |
67 | secondUserAccessToken = await userLogin(servers[0], secondUser) | 87 | secondUserAccessToken = await userLogin(servers[0], secondUser) |
68 | 88 | ||
69 | const videoAttributes = { | 89 | { |
70 | name: 'my super name', | 90 | const res = await getMyUserInformation(servers[0].url, firstUserAccessToken) |
71 | description: 'my super description' | 91 | const firstUserInformation: User = res.body |
92 | firstUserChannelId = firstUserInformation.videoChannels[0].id | ||
72 | } | 93 | } |
73 | await uploadVideo(servers[0].url, firstUserAccessToken, videoAttributes) | ||
74 | 94 | ||
75 | await waitJobs(servers) | 95 | { |
96 | const res = await getMyUserInformation(servers[0].url, secondUserAccessToken) | ||
97 | const secondUserInformation: User = res.body | ||
98 | secondUserChannelId = secondUserInformation.videoChannels[0].id | ||
99 | } | ||
76 | 100 | ||
77 | const res = await getVideosList(servers[0].url) | 101 | { |
78 | const videos = res.body.data | 102 | const videoAttributes = { |
103 | name: 'my super name', | ||
104 | description: 'my super description' | ||
105 | } | ||
106 | const res = await uploadVideo(servers[0].url, firstUserAccessToken, videoAttributes) | ||
79 | 107 | ||
80 | expect(videos.length).to.equal(1) | 108 | const resVideo = await getVideo(servers[0].url, res.body.video.id) |
109 | servers[0].video = resVideo.body | ||
110 | } | ||
81 | 111 | ||
82 | const video = videos.find(video => video.name === 'my super name') | 112 | { |
83 | expect(video.channel.name).to.equal('first_channel') | 113 | const attributes = { name: 'live', channelId: firstUserChannelId, privacy: VideoPrivacy.PUBLIC } |
84 | servers[0].video = video | 114 | const res = await createLive(servers[0].url, firstUserAccessToken, attributes) |
115 | |||
116 | liveId = res.body.video.id | ||
117 | } | ||
85 | 118 | ||
86 | await doubleFollow(servers[0], servers[1]) | 119 | await doubleFollow(servers[0], servers[1]) |
87 | }) | 120 | }) |
@@ -175,19 +208,19 @@ describe('Test video change ownership - nominal', function () { | |||
175 | it('Should not be possible to accept the change of ownership from first user', async function () { | 208 | it('Should not be possible to accept the change of ownership from first user', async function () { |
176 | this.timeout(10000) | 209 | this.timeout(10000) |
177 | 210 | ||
178 | const secondUserInformationResponse = await getMyUserInformation(servers[0].url, secondUserAccessToken) | 211 | await acceptChangeOwnership( |
179 | const secondUserInformation: User = secondUserInformationResponse.body | 212 | servers[0].url, |
180 | const channelId = secondUserInformation.videoChannels[0].id | 213 | firstUserAccessToken, |
181 | await acceptChangeOwnership(servers[0].url, firstUserAccessToken, lastRequestChangeOwnershipId, channelId, HttpStatusCode.FORBIDDEN_403) | 214 | lastRequestChangeOwnershipId, |
215 | secondUserChannelId, | ||
216 | HttpStatusCode.FORBIDDEN_403 | ||
217 | ) | ||
182 | }) | 218 | }) |
183 | 219 | ||
184 | it('Should be possible to accept the change of ownership from second user', async function () { | 220 | it('Should be possible to accept the change of ownership from second user', async function () { |
185 | this.timeout(10000) | 221 | this.timeout(10000) |
186 | 222 | ||
187 | const secondUserInformationResponse = await getMyUserInformation(servers[0].url, secondUserAccessToken) | 223 | await acceptChangeOwnership(servers[0].url, secondUserAccessToken, lastRequestChangeOwnershipId, secondUserChannelId) |
188 | const secondUserInformation: User = secondUserInformationResponse.body | ||
189 | const channelId = secondUserInformation.videoChannels[0].id | ||
190 | await acceptChangeOwnership(servers[0].url, secondUserAccessToken, lastRequestChangeOwnershipId, channelId) | ||
191 | 224 | ||
192 | await waitJobs(servers) | 225 | await waitJobs(servers) |
193 | }) | 226 | }) |
@@ -204,6 +237,37 @@ describe('Test video change ownership - nominal', function () { | |||
204 | } | 237 | } |
205 | }) | 238 | }) |
206 | 239 | ||
240 | it('Should send a request to change ownership of a live', async function () { | ||
241 | this.timeout(15000) | ||
242 | |||
243 | await changeVideoOwnership(servers[0].url, firstUserAccessToken, liveId, secondUser.username) | ||
244 | |||
245 | const resSecondUser = await getVideoChangeOwnershipList(servers[0].url, secondUserAccessToken) | ||
246 | |||
247 | expect(resSecondUser.body.total).to.equal(3) | ||
248 | expect(resSecondUser.body.data.length).to.equal(3) | ||
249 | |||
250 | lastRequestChangeOwnershipId = resSecondUser.body.data[0].id | ||
251 | }) | ||
252 | |||
253 | it('Should accept a live ownership change', async function () { | ||
254 | this.timeout(20000) | ||
255 | |||
256 | await acceptChangeOwnership(servers[0].url, secondUserAccessToken, lastRequestChangeOwnershipId, secondUserChannelId) | ||
257 | |||
258 | await waitJobs(servers) | ||
259 | |||
260 | for (const server of servers) { | ||
261 | const res = await getVideo(server.url, servers[0].video.uuid) | ||
262 | |||
263 | const video: VideoDetails = res.body | ||
264 | |||
265 | expect(video.name).to.equal('my super name') | ||
266 | expect(video.channel.displayName).to.equal('Main second channel') | ||
267 | expect(video.channel.name).to.equal('second_channel') | ||
268 | } | ||
269 | }) | ||
270 | |||
207 | after(async function () { | 271 | after(async function () { |
208 | await cleanupTests(servers) | 272 | await cleanupTests(servers) |
209 | }) | 273 | }) |
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index 7e7ad028c..865098777 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -15,6 +15,7 @@ import { | |||
15 | getVideoChannel, | 15 | getVideoChannel, |
16 | getVideoChannelVideos, | 16 | getVideoChannelVideos, |
17 | setDefaultVideoChannel, | 17 | setDefaultVideoChannel, |
18 | testFileExistsOrNot, | ||
18 | testImage, | 19 | testImage, |
19 | updateVideo, | 20 | updateVideo, |
20 | updateVideoChannelImage, | 21 | updateVideoChannelImage, |
@@ -53,6 +54,9 @@ describe('Test video channels', function () { | |||
53 | let videoUUID: string | 54 | let videoUUID: string |
54 | let accountName: string | 55 | let accountName: string |
55 | 56 | ||
57 | const avatarPaths: { [ port: number ]: string } = {} | ||
58 | const bannerPaths: { [ port: number ]: string } = {} | ||
59 | |||
56 | before(async function () { | 60 | before(async function () { |
57 | this.timeout(60000) | 61 | this.timeout(60000) |
58 | 62 | ||
@@ -287,9 +291,11 @@ describe('Test video channels', function () { | |||
287 | for (const server of servers) { | 291 | for (const server of servers) { |
288 | const videoChannel = await findChannel(server, secondVideoChannelId) | 292 | const videoChannel = await findChannel(server, secondVideoChannelId) |
289 | 293 | ||
290 | await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') | 294 | avatarPaths[server.port] = videoChannel.avatar.path |
295 | await testImage(server.url, 'avatar-resized', avatarPaths[server.port], '.png') | ||
296 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true) | ||
291 | 297 | ||
292 | const row = await getActorImage(server.internalServerNumber, basename(videoChannel.avatar.path)) | 298 | const row = await getActorImage(server.internalServerNumber, basename(avatarPaths[server.port])) |
293 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height) | 299 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height) |
294 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width) | 300 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width) |
295 | } | 301 | } |
@@ -314,9 +320,11 @@ describe('Test video channels', function () { | |||
314 | const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host) | 320 | const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host) |
315 | const videoChannel = res.body | 321 | const videoChannel = res.body |
316 | 322 | ||
317 | await testImage(server.url, 'banner-resized', videoChannel.banner.path) | 323 | bannerPaths[server.port] = videoChannel.banner.path |
324 | await testImage(server.url, 'banner-resized', bannerPaths[server.port]) | ||
325 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true) | ||
318 | 326 | ||
319 | const row = await getActorImage(server.internalServerNumber, basename(videoChannel.banner.path)) | 327 | const row = await getActorImage(server.internalServerNumber, basename(bannerPaths[server.port])) |
320 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height) | 328 | expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height) |
321 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width) | 329 | expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width) |
322 | } | 330 | } |
@@ -336,6 +344,7 @@ describe('Test video channels', function () { | |||
336 | 344 | ||
337 | for (const server of servers) { | 345 | for (const server of servers) { |
338 | const videoChannel = await findChannel(server, secondVideoChannelId) | 346 | const videoChannel = await findChannel(server, secondVideoChannelId) |
347 | await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false) | ||
339 | 348 | ||
340 | expect(videoChannel.avatar).to.be.null | 349 | expect(videoChannel.avatar).to.be.null |
341 | } | 350 | } |
@@ -355,6 +364,7 @@ describe('Test video channels', function () { | |||
355 | 364 | ||
356 | for (const server of servers) { | 365 | for (const server of servers) { |
357 | const videoChannel = await findChannel(server, secondVideoChannelId) | 366 | const videoChannel = await findChannel(server, secondVideoChannelId) |
367 | await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false) | ||
358 | 368 | ||
359 | expect(videoChannel.banner).to.be.null | 369 | expect(videoChannel.banner).to.be.null |
360 | } | 370 | } |
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index 615e0ea45..b6b002307 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | 5 | import { VideoComment, VideoCommentAdmin, VideoCommentThreadTree } from '@shared/models' | |
6 | import { cleanupTests, testImage } from '../../../../shared/extra-utils' | 6 | import { cleanupTests, testImage } from '../../../../shared/extra-utils' |
7 | import { | 7 | import { |
8 | createUser, | 8 | createUser, |
@@ -22,7 +22,6 @@ import { | |||
22 | getVideoCommentThreads, | 22 | getVideoCommentThreads, |
23 | getVideoThreadComments | 23 | getVideoThreadComments |
24 | } from '../../../../shared/extra-utils/videos/video-comments' | 24 | } from '../../../../shared/extra-utils/videos/video-comments' |
25 | import { VideoComment, VideoCommentAdmin, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' | ||
26 | 25 | ||
27 | const expect = chai.expect | 26 | const expect = chai.expect |
28 | 27 | ||
@@ -232,7 +231,7 @@ describe('Test video comments', function () { | |||
232 | expect(res.body.data[0].isDeleted).to.be.true | 231 | expect(res.body.data[0].isDeleted).to.be.true |
233 | expect(res.body.data[0].deletedAt).to.not.be.null | 232 | expect(res.body.data[0].deletedAt).to.not.be.null |
234 | expect(res.body.data[0].account).to.be.null | 233 | expect(res.body.data[0].account).to.be.null |
235 | expect(res.body.data[0].totalReplies).to.equal(3) | 234 | expect(res.body.data[0].totalReplies).to.equal(2) |
236 | expect(res.body.data[1].text).to.equal('super thread 2') | 235 | expect(res.body.data[1].text).to.equal('super thread 2') |
237 | expect(res.body.data[1].totalReplies).to.equal(0) | 236 | expect(res.body.data[1].totalReplies).to.equal(0) |
238 | expect(res.body.data[2].text).to.equal('super thread 3') | 237 | expect(res.body.data[2].text).to.equal('super thread 3') |
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index 9dad58c8c..da8de054b 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -56,7 +56,7 @@ import { | |||
56 | removeServerFromServerBlocklist | 56 | removeServerFromServerBlocklist |
57 | } from '../../../../shared/extra-utils/users/blocklist' | 57 | } from '../../../../shared/extra-utils/users/blocklist' |
58 | import { User } from '../../../../shared/models/users' | 58 | import { User } from '../../../../shared/models/users' |
59 | import { VideoPrivacy } from '../../../../shared/models/videos' | 59 | import { VideoPlaylistCreateResult, VideoPrivacy } from '../../../../shared/models/videos' |
60 | import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' | 60 | import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' |
61 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../../shared/models/videos/playlist/video-playlist-element.model' | 61 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../../shared/models/videos/playlist/video-playlist-element.model' |
62 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 62 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
@@ -427,31 +427,45 @@ describe('Test video playlists', function () { | |||
427 | expect(data).to.have.lengthOf(0) | 427 | expect(data).to.have.lengthOf(0) |
428 | } | 428 | } |
429 | }) | 429 | }) |
430 | }) | ||
430 | 431 | ||
431 | it('Should not list unlisted or private playlists', async function () { | 432 | describe('Playlist rights', function () { |
433 | let unlistedPlaylist: VideoPlaylistCreateResult | ||
434 | let privatePlaylist: VideoPlaylistCreateResult | ||
435 | |||
436 | before(async function () { | ||
432 | this.timeout(30000) | 437 | this.timeout(30000) |
433 | 438 | ||
434 | await createVideoPlaylist({ | 439 | { |
435 | url: servers[1].url, | 440 | const res = await createVideoPlaylist({ |
436 | token: servers[1].accessToken, | 441 | url: servers[1].url, |
437 | playlistAttrs: { | 442 | token: servers[1].accessToken, |
438 | displayName: 'playlist unlisted', | 443 | playlistAttrs: { |
439 | privacy: VideoPlaylistPrivacy.UNLISTED | 444 | displayName: 'playlist unlisted', |
440 | } | 445 | privacy: VideoPlaylistPrivacy.UNLISTED, |
441 | }) | 446 | videoChannelId: servers[1].videoChannel.id |
447 | } | ||
448 | }) | ||
449 | unlistedPlaylist = res.body.videoPlaylist | ||
450 | } | ||
442 | 451 | ||
443 | await createVideoPlaylist({ | 452 | { |
444 | url: servers[1].url, | 453 | const res = await createVideoPlaylist({ |
445 | token: servers[1].accessToken, | 454 | url: servers[1].url, |
446 | playlistAttrs: { | 455 | token: servers[1].accessToken, |
447 | displayName: 'playlist private', | 456 | playlistAttrs: { |
448 | privacy: VideoPlaylistPrivacy.PRIVATE | 457 | displayName: 'playlist private', |
449 | } | 458 | privacy: VideoPlaylistPrivacy.PRIVATE |
450 | }) | 459 | } |
460 | }) | ||
461 | privatePlaylist = res.body.videoPlaylist | ||
462 | } | ||
451 | 463 | ||
452 | await waitJobs(servers) | 464 | await waitJobs(servers) |
453 | await wait(3000) | 465 | await wait(3000) |
466 | }) | ||
454 | 467 | ||
468 | it('Should not list unlisted or private playlists', async function () { | ||
455 | for (const server of servers) { | 469 | for (const server of servers) { |
456 | const results = [ | 470 | const results = [ |
457 | await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[1].port, 0, 5, '-createdAt'), | 471 | await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[1].port, 0, 5, '-createdAt'), |
@@ -469,6 +483,27 @@ describe('Test video playlists', function () { | |||
469 | } | 483 | } |
470 | } | 484 | } |
471 | }) | 485 | }) |
486 | |||
487 | it('Should not get unlisted playlist using only the id', async function () { | ||
488 | await getVideoPlaylist(servers[1].url, unlistedPlaylist.id, 404) | ||
489 | }) | ||
490 | |||
491 | it('Should get unlisted plyaylist using uuid or shortUUID', async function () { | ||
492 | await getVideoPlaylist(servers[1].url, unlistedPlaylist.uuid) | ||
493 | await getVideoPlaylist(servers[1].url, unlistedPlaylist.shortUUID) | ||
494 | }) | ||
495 | |||
496 | it('Should not get private playlist without token', async function () { | ||
497 | for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) { | ||
498 | await getVideoPlaylist(servers[1].url, id, 401) | ||
499 | } | ||
500 | }) | ||
501 | |||
502 | it('Should get private playlist with a token', async function () { | ||
503 | for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) { | ||
504 | await getVideoPlaylistWithToken(servers[1].url, servers[1].accessToken, id) | ||
505 | } | ||
506 | }) | ||
472 | }) | 507 | }) |
473 | 508 | ||
474 | describe('Update playlists', function () { | 509 | describe('Update playlists', function () { |
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts index fed6ca0e0..950aeb7cf 100644 --- a/server/tests/api/videos/video-privacy.ts +++ b/server/tests/api/videos/video-privacy.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
5 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' | 4 | import * as chai from 'chai' |
5 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
6 | import { Video, VideoCreateResult } from '@shared/models' | ||
6 | import { | 7 | import { |
7 | cleanupTests, | 8 | cleanupTests, |
8 | flushAndRunServer, | 9 | flushAndRunServer, |
@@ -13,12 +14,11 @@ import { | |||
13 | uploadVideo | 14 | uploadVideo |
14 | } from '../../../../shared/extra-utils/index' | 15 | } from '../../../../shared/extra-utils/index' |
15 | import { doubleFollow } from '../../../../shared/extra-utils/server/follows' | 16 | import { doubleFollow } from '../../../../shared/extra-utils/server/follows' |
17 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
16 | import { userLogin } from '../../../../shared/extra-utils/users/login' | 18 | import { userLogin } from '../../../../shared/extra-utils/users/login' |
17 | import { createUser } from '../../../../shared/extra-utils/users/users' | 19 | import { createUser } from '../../../../shared/extra-utils/users/users' |
18 | import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos' | 20 | import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos' |
19 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | 21 | import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' |
20 | import { Video } from '@shared/models' | ||
21 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
22 | 22 | ||
23 | const expect = chai.expect | 23 | const expect = chai.expect |
24 | 24 | ||
@@ -32,7 +32,7 @@ describe('Test video privacy', function () { | |||
32 | let internalVideoId: number | 32 | let internalVideoId: number |
33 | let internalVideoUUID: string | 33 | let internalVideoUUID: string |
34 | 34 | ||
35 | let unlistedVideoUUID: string | 35 | let unlistedVideo: VideoCreateResult |
36 | let nonFederatedUnlistedVideoUUID: string | 36 | let nonFederatedUnlistedVideoUUID: string |
37 | 37 | ||
38 | let now: number | 38 | let now: number |
@@ -59,231 +59,246 @@ describe('Test video privacy', function () { | |||
59 | await doubleFollow(servers[0], servers[1]) | 59 | await doubleFollow(servers[0], servers[1]) |
60 | }) | 60 | }) |
61 | 61 | ||
62 | it('Should upload a private and internal videos on server 1', async function () { | 62 | describe('Private and internal videos', function () { |
63 | this.timeout(10000) | ||
64 | 63 | ||
65 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | 64 | it('Should upload a private and internal videos on server 1', async function () { |
66 | const attributes = { privacy } | 65 | this.timeout(10000) |
67 | await uploadVideo(servers[0].url, servers[0].accessToken, attributes) | ||
68 | } | ||
69 | 66 | ||
70 | await waitJobs(servers) | 67 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { |
71 | }) | 68 | const attributes = { privacy } |
69 | await uploadVideo(servers[0].url, servers[0].accessToken, attributes) | ||
70 | } | ||
72 | 71 | ||
73 | it('Should not have these private and internal videos on server 2', async function () { | 72 | await waitJobs(servers) |
74 | const res = await getVideosList(servers[1].url) | 73 | }) |
75 | 74 | ||
76 | expect(res.body.total).to.equal(0) | 75 | it('Should not have these private and internal videos on server 2', async function () { |
77 | expect(res.body.data).to.have.lengthOf(0) | 76 | const res = await getVideosList(servers[1].url) |
78 | }) | ||
79 | 77 | ||
80 | it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () { | 78 | expect(res.body.total).to.equal(0) |
81 | const res = await getVideosList(servers[0].url) | 79 | expect(res.body.data).to.have.lengthOf(0) |
80 | }) | ||
82 | 81 | ||
83 | expect(res.body.total).to.equal(0) | 82 | it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () { |
84 | expect(res.body.data).to.have.lengthOf(0) | 83 | const res = await getVideosList(servers[0].url) |
85 | }) | 84 | |
85 | expect(res.body.total).to.equal(0) | ||
86 | expect(res.body.data).to.have.lengthOf(0) | ||
87 | }) | ||
86 | 88 | ||
87 | it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () { | 89 | it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () { |
88 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) | 90 | const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) |
89 | 91 | ||
90 | expect(res.body.total).to.equal(1) | 92 | expect(res.body.total).to.equal(1) |
91 | expect(res.body.data).to.have.lengthOf(1) | 93 | expect(res.body.data).to.have.lengthOf(1) |
92 | 94 | ||
93 | expect(res.body.data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL) | 95 | expect(res.body.data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL) |
94 | }) | 96 | }) |
95 | 97 | ||
96 | it('Should list my (private and internal) videos', async function () { | 98 | it('Should list my (private and internal) videos', async function () { |
97 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 10) | 99 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 10) |
98 | 100 | ||
99 | expect(res.body.total).to.equal(2) | 101 | expect(res.body.total).to.equal(2) |
100 | expect(res.body.data).to.have.lengthOf(2) | 102 | expect(res.body.data).to.have.lengthOf(2) |
101 | 103 | ||
102 | const videos: Video[] = res.body.data | 104 | const videos: Video[] = res.body.data |
103 | 105 | ||
104 | const privateVideo = videos.find(v => v.privacy.id === VideoPrivacy.PRIVATE) | 106 | const privateVideo = videos.find(v => v.privacy.id === VideoPrivacy.PRIVATE) |
105 | privateVideoId = privateVideo.id | 107 | privateVideoId = privateVideo.id |
106 | privateVideoUUID = privateVideo.uuid | 108 | privateVideoUUID = privateVideo.uuid |
107 | 109 | ||
108 | const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL) | 110 | const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL) |
109 | internalVideoId = internalVideo.id | 111 | internalVideoId = internalVideo.id |
110 | internalVideoUUID = internalVideo.uuid | 112 | internalVideoUUID = internalVideo.uuid |
111 | }) | 113 | }) |
112 | 114 | ||
113 | it('Should not be able to watch the private/internal video with non authenticated user', async function () { | 115 | it('Should not be able to watch the private/internal video with non authenticated user', async function () { |
114 | await getVideo(servers[0].url, privateVideoUUID, HttpStatusCode.UNAUTHORIZED_401) | 116 | await getVideo(servers[0].url, privateVideoUUID, HttpStatusCode.UNAUTHORIZED_401) |
115 | await getVideo(servers[0].url, internalVideoUUID, HttpStatusCode.UNAUTHORIZED_401) | 117 | await getVideo(servers[0].url, internalVideoUUID, HttpStatusCode.UNAUTHORIZED_401) |
116 | }) | 118 | }) |
117 | 119 | ||
118 | it('Should not be able to watch the private video with another user', async function () { | 120 | it('Should not be able to watch the private video with another user', async function () { |
119 | this.timeout(10000) | 121 | this.timeout(10000) |
120 | 122 | ||
121 | const user = { | 123 | const user = { |
122 | username: 'hello', | 124 | username: 'hello', |
123 | password: 'super password' | 125 | password: 'super password' |
124 | } | 126 | } |
125 | await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password }) | 127 | await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password }) |
126 | 128 | ||
127 | anotherUserToken = await userLogin(servers[0], user) | 129 | anotherUserToken = await userLogin(servers[0], user) |
128 | await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, HttpStatusCode.FORBIDDEN_403) | 130 | await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, HttpStatusCode.FORBIDDEN_403) |
129 | }) | 131 | }) |
130 | 132 | ||
131 | it('Should be able to watch the internal video with another user', async function () { | 133 | it('Should be able to watch the internal video with another user', async function () { |
132 | await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, HttpStatusCode.OK_200) | 134 | await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, HttpStatusCode.OK_200) |
133 | }) | 135 | }) |
134 | 136 | ||
135 | it('Should be able to watch the private video with the correct user', async function () { | 137 | it('Should be able to watch the private video with the correct user', async function () { |
136 | await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, HttpStatusCode.OK_200) | 138 | await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, HttpStatusCode.OK_200) |
139 | }) | ||
137 | }) | 140 | }) |
138 | 141 | ||
139 | it('Should upload an unlisted video on server 2', async function () { | 142 | describe('Unlisted videos', function () { |
140 | this.timeout(60000) | ||
141 | 143 | ||
142 | const attributes = { | 144 | it('Should upload an unlisted video on server 2', async function () { |
143 | name: 'unlisted video', | 145 | this.timeout(60000) |
144 | privacy: VideoPrivacy.UNLISTED | ||
145 | } | ||
146 | await uploadVideo(servers[1].url, servers[1].accessToken, attributes) | ||
147 | 146 | ||
148 | // Server 2 has transcoding enabled | 147 | const attributes = { |
149 | await waitJobs(servers) | 148 | name: 'unlisted video', |
150 | }) | 149 | privacy: VideoPrivacy.UNLISTED |
150 | } | ||
151 | await uploadVideo(servers[1].url, servers[1].accessToken, attributes) | ||
151 | 152 | ||
152 | it('Should not have this unlisted video listed on server 1 and 2', async function () { | 153 | // Server 2 has transcoding enabled |
153 | for (const server of servers) { | 154 | await waitJobs(servers) |
154 | const res = await getVideosList(server.url) | 155 | }) |
155 | 156 | ||
156 | expect(res.body.total).to.equal(0) | 157 | it('Should not have this unlisted video listed on server 1 and 2', async function () { |
157 | expect(res.body.data).to.have.lengthOf(0) | 158 | for (const server of servers) { |
158 | } | 159 | const res = await getVideosList(server.url) |
159 | }) | ||
160 | 160 | ||
161 | it('Should list my (unlisted) videos', async function () { | 161 | expect(res.body.total).to.equal(0) |
162 | const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 1) | 162 | expect(res.body.data).to.have.lengthOf(0) |
163 | } | ||
164 | }) | ||
163 | 165 | ||
164 | expect(res.body.total).to.equal(1) | 166 | it('Should list my (unlisted) videos', async function () { |
165 | expect(res.body.data).to.have.lengthOf(1) | 167 | const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 1) |
166 | 168 | ||
167 | unlistedVideoUUID = res.body.data[0].uuid | 169 | expect(res.body.total).to.equal(1) |
168 | }) | 170 | expect(res.body.data).to.have.lengthOf(1) |
169 | 171 | ||
170 | it('Should be able to get this unlisted video', async function () { | 172 | unlistedVideo = res.body.data[0] |
171 | for (const server of servers) { | 173 | }) |
172 | const res = await getVideo(server.url, unlistedVideoUUID) | ||
173 | 174 | ||
174 | expect(res.body.name).to.equal('unlisted video') | 175 | it('Should not be able to get this unlisted video using its id', async function () { |
175 | } | 176 | await getVideo(servers[1].url, unlistedVideo.id, 404) |
176 | }) | 177 | }) |
177 | 178 | ||
178 | it('Should upload a non-federating unlisted video to server 1', async function () { | 179 | it('Should be able to get this unlisted video using its uuid/shortUUID', async function () { |
179 | this.timeout(30000) | 180 | for (const server of servers) { |
181 | for (const id of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) { | ||
182 | const res = await getVideo(server.url, id) | ||
180 | 183 | ||
181 | const attributes = { | 184 | expect(res.body.name).to.equal('unlisted video') |
182 | name: 'unlisted video', | 185 | } |
183 | privacy: VideoPrivacy.UNLISTED | 186 | } |
184 | } | 187 | }) |
185 | await uploadVideo(servers[0].url, servers[0].accessToken, attributes) | ||
186 | 188 | ||
187 | await waitJobs(servers) | 189 | it('Should upload a non-federating unlisted video to server 1', async function () { |
188 | }) | 190 | this.timeout(30000) |
191 | |||
192 | const attributes = { | ||
193 | name: 'unlisted video', | ||
194 | privacy: VideoPrivacy.UNLISTED | ||
195 | } | ||
196 | await uploadVideo(servers[0].url, servers[0].accessToken, attributes) | ||
189 | 197 | ||
190 | it('Should list my new unlisted video', async function () { | 198 | await waitJobs(servers) |
191 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 3) | 199 | }) |
192 | 200 | ||
193 | expect(res.body.total).to.equal(3) | 201 | it('Should list my new unlisted video', async function () { |
194 | expect(res.body.data).to.have.lengthOf(3) | 202 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 3) |
195 | 203 | ||
196 | nonFederatedUnlistedVideoUUID = res.body.data[0].uuid | 204 | expect(res.body.total).to.equal(3) |
197 | }) | 205 | expect(res.body.data).to.have.lengthOf(3) |
198 | 206 | ||
199 | it('Should be able to get non-federated unlisted video from origin', async function () { | 207 | nonFederatedUnlistedVideoUUID = res.body.data[0].uuid |
200 | const res = await getVideo(servers[0].url, nonFederatedUnlistedVideoUUID) | 208 | }) |
201 | 209 | ||
202 | expect(res.body.name).to.equal('unlisted video') | 210 | it('Should be able to get non-federated unlisted video from origin', async function () { |
203 | }) | 211 | const res = await getVideo(servers[0].url, nonFederatedUnlistedVideoUUID) |
204 | 212 | ||
205 | it('Should not be able to get non-federated unlisted video from federated server', async function () { | 213 | expect(res.body.name).to.equal('unlisted video') |
206 | await getVideo(servers[1].url, nonFederatedUnlistedVideoUUID, HttpStatusCode.NOT_FOUND_404) | 214 | }) |
215 | |||
216 | it('Should not be able to get non-federated unlisted video from federated server', async function () { | ||
217 | await getVideo(servers[1].url, nonFederatedUnlistedVideoUUID, HttpStatusCode.NOT_FOUND_404) | ||
218 | }) | ||
207 | }) | 219 | }) |
208 | 220 | ||
209 | it('Should update the private and internal videos to public on server 1', async function () { | 221 | describe('Privacy update', function () { |
210 | this.timeout(10000) | ||
211 | 222 | ||
212 | now = Date.now() | 223 | it('Should update the private and internal videos to public on server 1', async function () { |
224 | this.timeout(10000) | ||
213 | 225 | ||
214 | { | 226 | now = Date.now() |
215 | const attribute = { | ||
216 | name: 'private video becomes public', | ||
217 | privacy: VideoPrivacy.PUBLIC | ||
218 | } | ||
219 | 227 | ||
220 | await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute) | 228 | { |
221 | } | 229 | const attribute = { |
230 | name: 'private video becomes public', | ||
231 | privacy: VideoPrivacy.PUBLIC | ||
232 | } | ||
222 | 233 | ||
223 | { | 234 | await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute) |
224 | const attribute = { | ||
225 | name: 'internal video becomes public', | ||
226 | privacy: VideoPrivacy.PUBLIC | ||
227 | } | 235 | } |
228 | await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, attribute) | ||
229 | } | ||
230 | 236 | ||
231 | await waitJobs(servers) | 237 | { |
232 | }) | 238 | const attribute = { |
239 | name: 'internal video becomes public', | ||
240 | privacy: VideoPrivacy.PUBLIC | ||
241 | } | ||
242 | await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, attribute) | ||
243 | } | ||
233 | 244 | ||
234 | it('Should have this new public video listed on server 1 and 2', async function () { | 245 | await waitJobs(servers) |
235 | for (const server of servers) { | 246 | }) |
236 | const res = await getVideosList(server.url) | ||
237 | expect(res.body.total).to.equal(2) | ||
238 | expect(res.body.data).to.have.lengthOf(2) | ||
239 | 247 | ||
240 | const videos: Video[] = res.body.data | 248 | it('Should have this new public video listed on server 1 and 2', async function () { |
241 | const privateVideo = videos.find(v => v.name === 'private video becomes public') | 249 | for (const server of servers) { |
242 | const internalVideo = videos.find(v => v.name === 'internal video becomes public') | 250 | const res = await getVideosList(server.url) |
251 | expect(res.body.total).to.equal(2) | ||
252 | expect(res.body.data).to.have.lengthOf(2) | ||
243 | 253 | ||
244 | expect(privateVideo).to.not.be.undefined | 254 | const videos: Video[] = res.body.data |
245 | expect(internalVideo).to.not.be.undefined | 255 | const privateVideo = videos.find(v => v.name === 'private video becomes public') |
256 | const internalVideo = videos.find(v => v.name === 'internal video becomes public') | ||
246 | 257 | ||
247 | expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now) | 258 | expect(privateVideo).to.not.be.undefined |
248 | // We don't change the publish date of internal videos | 259 | expect(internalVideo).to.not.be.undefined |
249 | expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now) | ||
250 | 260 | ||
251 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) | 261 | expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now) |
252 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) | 262 | // We don't change the publish date of internal videos |
253 | } | 263 | expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now) |
254 | }) | ||
255 | 264 | ||
256 | it('Should set these videos as private and internal', async function () { | 265 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) |
257 | this.timeout(10000) | 266 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) |
267 | } | ||
268 | }) | ||
258 | 269 | ||
259 | await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE }) | 270 | it('Should set these videos as private and internal', async function () { |
260 | await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL }) | 271 | this.timeout(10000) |
261 | 272 | ||
262 | await waitJobs(servers) | 273 | await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE }) |
274 | await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL }) | ||
263 | 275 | ||
264 | for (const server of servers) { | 276 | await waitJobs(servers) |
265 | const res = await getVideosList(server.url) | ||
266 | 277 | ||
267 | expect(res.body.total).to.equal(0) | 278 | for (const server of servers) { |
268 | expect(res.body.data).to.have.lengthOf(0) | 279 | const res = await getVideosList(server.url) |
269 | } | 280 | |
281 | expect(res.body.total).to.equal(0) | ||
282 | expect(res.body.data).to.have.lengthOf(0) | ||
283 | } | ||
270 | 284 | ||
271 | { | 285 | { |
272 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) | 286 | const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) |
273 | const videos = res.body.data | 287 | const videos = res.body.data |
274 | 288 | ||
275 | expect(res.body.total).to.equal(3) | 289 | expect(res.body.total).to.equal(3) |
276 | expect(videos).to.have.lengthOf(3) | 290 | expect(videos).to.have.lengthOf(3) |
277 | 291 | ||
278 | const privateVideo = videos.find(v => v.name === 'private video becomes public') | 292 | const privateVideo = videos.find(v => v.name === 'private video becomes public') |
279 | const internalVideo = videos.find(v => v.name === 'internal video becomes public') | 293 | const internalVideo = videos.find(v => v.name === 'internal video becomes public') |
280 | 294 | ||
281 | expect(privateVideo).to.not.be.undefined | 295 | expect(privateVideo).to.not.be.undefined |
282 | expect(internalVideo).to.not.be.undefined | 296 | expect(internalVideo).to.not.be.undefined |
283 | 297 | ||
284 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL) | 298 | expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL) |
285 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE) | 299 | expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE) |
286 | } | 300 | } |
301 | }) | ||
287 | }) | 302 | }) |
288 | 303 | ||
289 | after(async function () { | 304 | after(async function () { |
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts index 2961c8e78..7428b82c5 100644 --- a/server/tests/api/videos/videos-filter.ts +++ b/server/tests/api/videos/videos-filter.ts | |||
@@ -47,13 +47,13 @@ async function getVideosNames (server: ServerInfo, token: string, filter: string | |||
47 | return videosResults | 47 | return videosResults |
48 | } | 48 | } |
49 | 49 | ||
50 | describe('Test videos filter validator', function () { | 50 | describe('Test videos filter', function () { |
51 | let servers: ServerInfo[] | 51 | let servers: ServerInfo[] |
52 | 52 | ||
53 | // --------------------------------------------------------------- | 53 | // --------------------------------------------------------------- |
54 | 54 | ||
55 | before(async function () { | 55 | before(async function () { |
56 | this.timeout(120000) | 56 | this.timeout(160000) |
57 | 57 | ||
58 | servers = await flushAndRunMultipleServers(2) | 58 | servers = await flushAndRunMultipleServers(2) |
59 | 59 | ||
diff --git a/server/tests/api/videos/videos-overview.ts b/server/tests/api/videos/videos-overview.ts index 7889bcf80..c266a1dc5 100644 --- a/server/tests/api/videos/videos-overview.ts +++ b/server/tests/api/videos/videos-overview.ts | |||
@@ -45,7 +45,7 @@ describe('Test a videos overview', function () { | |||
45 | }) | 45 | }) |
46 | 46 | ||
47 | it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () { | 47 | it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () { |
48 | this.timeout(15000) | 48 | this.timeout(30000) |
49 | 49 | ||
50 | await wait(3000) | 50 | await wait(3000) |
51 | 51 | ||
@@ -61,7 +61,7 @@ describe('Test a videos overview', function () { | |||
61 | }) | 61 | }) |
62 | 62 | ||
63 | it('Should upload another video and include all videos in the overview', async function () { | 63 | it('Should upload another video and include all videos in the overview', async function () { |
64 | this.timeout(15000) | 64 | this.timeout(30000) |
65 | 65 | ||
66 | for (let i = 1; i < 6; i++) { | 66 | for (let i = 1; i < 6; i++) { |
67 | await uploadVideo(server.url, server.accessToken, { | 67 | await uploadVideo(server.url, server.accessToken, { |
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index 591ed217f..a0af09de8 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts | |||
@@ -2,7 +2,10 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' | 5 | import { createFile, readdir } from 'fs-extra' |
6 | import { join } from 'path' | ||
7 | import { buildUUID } from '@server/helpers/uuid' | ||
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
6 | import { | 9 | import { |
7 | buildServerDirectory, | 10 | buildServerDirectory, |
8 | cleanupTests, | 11 | cleanupTests, |
@@ -12,6 +15,7 @@ import { | |||
12 | flushAndRunMultipleServers, | 15 | flushAndRunMultipleServers, |
13 | getAccount, | 16 | getAccount, |
14 | getEnvCli, | 17 | getEnvCli, |
18 | killallServers, | ||
15 | makeGetRequest, | 19 | makeGetRequest, |
16 | ServerInfo, | 20 | ServerInfo, |
17 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
@@ -20,11 +24,8 @@ import { | |||
20 | uploadVideo, | 24 | uploadVideo, |
21 | wait | 25 | wait |
22 | } from '../../../shared/extra-utils' | 26 | } from '../../../shared/extra-utils' |
27 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' | ||
23 | import { Account, VideoPlaylistPrivacy } from '../../../shared/models' | 28 | import { Account, VideoPlaylistPrivacy } from '../../../shared/models' |
24 | import { createFile, readdir } from 'fs-extra' | ||
25 | import { v4 as uuidv4 } from 'uuid' | ||
26 | import { join } from 'path' | ||
27 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
28 | 29 | ||
29 | const expect = chai.expect | 30 | const expect = chai.expect |
30 | 31 | ||
@@ -116,6 +117,9 @@ describe('Test prune storage scripts', function () { | |||
116 | await wait(1000) | 117 | await wait(1000) |
117 | 118 | ||
118 | await waitJobs(servers) | 119 | await waitJobs(servers) |
120 | killallServers(servers) | ||
121 | |||
122 | await wait(1000) | ||
119 | }) | 123 | }) |
120 | 124 | ||
121 | it('Should have the files on the disk', async function () { | 125 | it('Should have the files on the disk', async function () { |
@@ -127,8 +131,8 @@ describe('Test prune storage scripts', function () { | |||
127 | { | 131 | { |
128 | const base = buildServerDirectory(servers[0], 'videos') | 132 | const base = buildServerDirectory(servers[0], 'videos') |
129 | 133 | ||
130 | const n1 = uuidv4() + '.mp4' | 134 | const n1 = buildUUID() + '.mp4' |
131 | const n2 = uuidv4() + '.webm' | 135 | const n2 = buildUUID() + '.webm' |
132 | 136 | ||
133 | await createFile(join(base, n1)) | 137 | await createFile(join(base, n1)) |
134 | await createFile(join(base, n2)) | 138 | await createFile(join(base, n2)) |
@@ -139,8 +143,8 @@ describe('Test prune storage scripts', function () { | |||
139 | { | 143 | { |
140 | const base = buildServerDirectory(servers[0], 'torrents') | 144 | const base = buildServerDirectory(servers[0], 'torrents') |
141 | 145 | ||
142 | const n1 = uuidv4() + '-240.torrent' | 146 | const n1 = buildUUID() + '-240.torrent' |
143 | const n2 = uuidv4() + '-480.torrent' | 147 | const n2 = buildUUID() + '-480.torrent' |
144 | 148 | ||
145 | await createFile(join(base, n1)) | 149 | await createFile(join(base, n1)) |
146 | await createFile(join(base, n2)) | 150 | await createFile(join(base, n2)) |
@@ -151,8 +155,8 @@ describe('Test prune storage scripts', function () { | |||
151 | { | 155 | { |
152 | const base = buildServerDirectory(servers[0], 'thumbnails') | 156 | const base = buildServerDirectory(servers[0], 'thumbnails') |
153 | 157 | ||
154 | const n1 = uuidv4() + '.jpg' | 158 | const n1 = buildUUID() + '.jpg' |
155 | const n2 = uuidv4() + '.jpg' | 159 | const n2 = buildUUID() + '.jpg' |
156 | 160 | ||
157 | await createFile(join(base, n1)) | 161 | await createFile(join(base, n1)) |
158 | await createFile(join(base, n2)) | 162 | await createFile(join(base, n2)) |
@@ -163,8 +167,8 @@ describe('Test prune storage scripts', function () { | |||
163 | { | 167 | { |
164 | const base = buildServerDirectory(servers[0], 'previews') | 168 | const base = buildServerDirectory(servers[0], 'previews') |
165 | 169 | ||
166 | const n1 = uuidv4() + '.jpg' | 170 | const n1 = buildUUID() + '.jpg' |
167 | const n2 = uuidv4() + '.jpg' | 171 | const n2 = buildUUID() + '.jpg' |
168 | 172 | ||
169 | await createFile(join(base, n1)) | 173 | await createFile(join(base, n1)) |
170 | await createFile(join(base, n2)) | 174 | await createFile(join(base, n2)) |
@@ -175,8 +179,8 @@ describe('Test prune storage scripts', function () { | |||
175 | { | 179 | { |
176 | const base = buildServerDirectory(servers[0], 'avatars') | 180 | const base = buildServerDirectory(servers[0], 'avatars') |
177 | 181 | ||
178 | const n1 = uuidv4() + '.png' | 182 | const n1 = buildUUID() + '.png' |
179 | const n2 = uuidv4() + '.jpg' | 183 | const n2 = buildUUID() + '.jpg' |
180 | 184 | ||
181 | await createFile(join(base, n1)) | 185 | await createFile(join(base, n1)) |
182 | await createFile(join(base, n2)) | 186 | await createFile(join(base, n2)) |
diff --git a/server/tests/client.ts b/server/tests/client.ts index 3c99bcd1f..7c4fb4e46 100644 --- a/server/tests/client.ts +++ b/server/tests/client.ts | |||
@@ -2,8 +2,9 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import * as request from 'supertest' | 5 | import { omit } from 'lodash' |
6 | import { Account, VideoPlaylistPrivacy } from '@shared/models' | 6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
7 | import { Account, CustomConfig, HTMLServerConfig, ServerConfig, VideoPlaylistCreateResult, VideoPlaylistPrivacy } from '@shared/models' | ||
7 | import { | 8 | import { |
8 | addVideoInPlaylist, | 9 | addVideoInPlaylist, |
9 | cleanupTests, | 10 | cleanupTests, |
@@ -11,8 +12,10 @@ import { | |||
11 | doubleFollow, | 12 | doubleFollow, |
12 | flushAndRunMultipleServers, | 13 | flushAndRunMultipleServers, |
13 | getAccount, | 14 | getAccount, |
15 | getConfig, | ||
14 | getCustomConfig, | 16 | getCustomConfig, |
15 | getVideosList, | 17 | getVideosList, |
18 | makeGetRequest, | ||
16 | makeHTMLRequest, | 19 | makeHTMLRequest, |
17 | ServerInfo, | 20 | ServerInfo, |
18 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
@@ -24,14 +27,16 @@ import { | |||
24 | uploadVideo, | 27 | uploadVideo, |
25 | waitJobs | 28 | waitJobs |
26 | } from '../../shared/extra-utils' | 29 | } from '../../shared/extra-utils' |
27 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
28 | 30 | ||
29 | const expect = chai.expect | 31 | const expect = chai.expect |
30 | 32 | ||
31 | function checkIndexTags (html: string, title: string, description: string, css: string) { | 33 | function checkIndexTags (html: string, title: string, description: string, css: string, config: ServerConfig) { |
32 | expect(html).to.contain('<title>' + title + '</title>') | 34 | expect(html).to.contain('<title>' + title + '</title>') |
33 | expect(html).to.contain('<meta name="description" content="' + description + '" />') | 35 | expect(html).to.contain('<meta name="description" content="' + description + '" />') |
34 | expect(html).to.contain('<style class="custom-css-style">' + css + '</style>') | 36 | expect(html).to.contain('<style class="custom-css-style">' + css + '</style>') |
37 | |||
38 | const htmlConfig: HTMLServerConfig = omit(config, 'signup') | ||
39 | expect(html).to.contain(`<script type="application/javascript">window.PeerTubeServerConfig = '${JSON.stringify(htmlConfig)}'</script>`) | ||
35 | } | 40 | } |
36 | 41 | ||
37 | describe('Test a client controllers', function () { | 42 | describe('Test a client controllers', function () { |
@@ -44,10 +49,16 @@ describe('Test a client controllers', function () { | |||
44 | 49 | ||
45 | const playlistName = 'super playlist name' | 50 | const playlistName = 'super playlist name' |
46 | const playlistDescription = 'super playlist description' | 51 | const playlistDescription = 'super playlist description' |
47 | let playlistUUID: string | 52 | let playlist: VideoPlaylistCreateResult |
48 | 53 | ||
49 | const channelDescription = 'my super channel description' | 54 | const channelDescription = 'my super channel description' |
50 | 55 | ||
56 | const watchVideoBasePaths = [ '/videos/watch/', '/w/' ] | ||
57 | const watchPlaylistBasePaths = [ '/videos/watch/playlist/', '/w/p/' ] | ||
58 | |||
59 | let videoIds: (string | number)[] = [] | ||
60 | let playlistIds: (string | number)[] = [] | ||
61 | |||
51 | before(async function () { | 62 | before(async function () { |
52 | this.timeout(120000) | 63 | this.timeout(120000) |
53 | 64 | ||
@@ -70,7 +81,9 @@ describe('Test a client controllers', function () { | |||
70 | const videos = resVideosRequest.body.data | 81 | const videos = resVideosRequest.body.data |
71 | expect(videos.length).to.equal(1) | 82 | expect(videos.length).to.equal(1) |
72 | 83 | ||
73 | servers[0].video = videos[0] | 84 | const video = videos[0] |
85 | servers[0].video = video | ||
86 | videoIds = [ video.id, video.uuid, video.shortUUID ] | ||
74 | 87 | ||
75 | // Playlist | 88 | // Playlist |
76 | 89 | ||
@@ -82,16 +95,14 @@ describe('Test a client controllers', function () { | |||
82 | } | 95 | } |
83 | 96 | ||
84 | const resVideoPlaylistRequest = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs }) | 97 | const resVideoPlaylistRequest = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs }) |
85 | 98 | playlist = resVideoPlaylistRequest.body.videoPlaylist | |
86 | const playlist = resVideoPlaylistRequest.body.videoPlaylist | 99 | playlistIds = [ playlist.id, playlist.shortUUID, playlist.uuid ] |
87 | const playlistId = playlist.id | ||
88 | playlistUUID = playlist.uuid | ||
89 | 100 | ||
90 | await addVideoInPlaylist({ | 101 | await addVideoInPlaylist({ |
91 | url: servers[0].url, | 102 | url: servers[0].url, |
92 | token: servers[0].accessToken, | 103 | token: servers[0].accessToken, |
93 | playlistId, | 104 | playlistId: playlist.shortUUID, |
94 | elementAttrs: { videoId: servers[0].video.id } | 105 | elementAttrs: { videoId: video.id } |
95 | }) | 106 | }) |
96 | 107 | ||
97 | // Account | 108 | // Account |
@@ -105,201 +116,277 @@ describe('Test a client controllers', function () { | |||
105 | }) | 116 | }) |
106 | 117 | ||
107 | describe('oEmbed', function () { | 118 | describe('oEmbed', function () { |
119 | |||
108 | it('Should have valid oEmbed discovery tags for videos', async function () { | 120 | it('Should have valid oEmbed discovery tags for videos', async function () { |
109 | const path = '/videos/watch/' + servers[0].video.uuid | 121 | for (const basePath of watchVideoBasePaths) { |
110 | const res = await request(servers[0].url) | 122 | for (const id of videoIds) { |
111 | .get(path) | 123 | const res = await makeGetRequest({ |
112 | .set('Accept', 'text/html') | 124 | url: servers[0].url, |
113 | .expect(HttpStatusCode.OK_200) | 125 | path: basePath + id, |
126 | accept: 'text/html', | ||
127 | statusCodeExpected: HttpStatusCode.OK_200 | ||
128 | }) | ||
129 | |||
130 | const port = servers[0].port | ||
131 | |||
132 | const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:' + port + '/services/oembed?' + | ||
133 | `url=http%3A%2F%2Flocalhost%3A${port}%2Fw%2F${servers[0].video.uuid}" ` + | ||
134 | `title="${servers[0].video.name}" />` | ||
135 | |||
136 | expect(res.text).to.contain(expectedLink) | ||
137 | } | ||
138 | } | ||
139 | }) | ||
114 | 140 | ||
115 | const port = servers[0].port | 141 | it('Should have valid oEmbed discovery tags for a playlist', async function () { |
142 | for (const basePath of watchPlaylistBasePaths) { | ||
143 | for (const id of playlistIds) { | ||
144 | const res = await makeGetRequest({ | ||
145 | url: servers[0].url, | ||
146 | path: basePath + id, | ||
147 | accept: 'text/html', | ||
148 | statusCodeExpected: HttpStatusCode.OK_200 | ||
149 | }) | ||
150 | |||
151 | const port = servers[0].port | ||
152 | |||
153 | const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:' + port + '/services/oembed?' + | ||
154 | `url=http%3A%2F%2Flocalhost%3A${port}%2Fw%2Fp%2F${playlist.uuid}" ` + | ||
155 | `title="${playlistName}" />` | ||
156 | |||
157 | expect(res.text).to.contain(expectedLink) | ||
158 | } | ||
159 | } | ||
160 | }) | ||
161 | }) | ||
116 | 162 | ||
117 | const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:' + port + '/services/oembed?' + | 163 | describe('Open Graph', function () { |
118 | `url=http%3A%2F%2Flocalhost%3A${port}%2Fvideos%2Fwatch%2F${servers[0].video.uuid}" ` + | ||
119 | `title="${servers[0].video.name}" />` | ||
120 | 164 | ||
121 | expect(res.text).to.contain(expectedLink) | 165 | async function accountPageTest (path: string) { |
122 | }) | 166 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
167 | const text = res.text | ||
123 | 168 | ||
124 | it('Should have valid oEmbed discovery tags for a playlist', async function () { | 169 | expect(text).to.contain(`<meta property="og:title" content="${account.displayName}" />`) |
125 | const res = await request(servers[0].url) | 170 | expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`) |
126 | .get('/videos/watch/playlist/' + playlistUUID) | 171 | expect(text).to.contain('<meta property="og:type" content="website" />') |
127 | .set('Accept', 'text/html') | 172 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`) |
128 | .expect(HttpStatusCode.OK_200) | 173 | } |
129 | 174 | ||
130 | const port = servers[0].port | 175 | async function channelPageTest (path: string) { |
176 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) | ||
177 | const text = res.text | ||
131 | 178 | ||
132 | const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:' + port + '/services/oembed?' + | 179 | expect(text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`) |
133 | `url=http%3A%2F%2Flocalhost%3A${port}%2Fvideos%2Fwatch%2Fplaylist%2F${playlistUUID}" ` + | 180 | expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`) |
134 | `title="${playlistName}" />` | 181 | expect(text).to.contain('<meta property="og:type" content="website" />') |
182 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`) | ||
183 | } | ||
135 | 184 | ||
136 | expect(res.text).to.contain(expectedLink) | 185 | async function watchVideoPageTest (path: string) { |
137 | }) | 186 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
138 | }) | 187 | const text = res.text |
139 | 188 | ||
140 | describe('Open Graph', function () { | 189 | expect(text).to.contain(`<meta property="og:title" content="${videoName}" />`) |
190 | expect(text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`) | ||
191 | expect(text).to.contain('<meta property="og:type" content="video" />') | ||
192 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/${servers[0].video.uuid}" />`) | ||
193 | } | ||
194 | |||
195 | async function watchPlaylistPageTest (path: string) { | ||
196 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) | ||
197 | const text = res.text | ||
198 | |||
199 | expect(text).to.contain(`<meta property="og:title" content="${playlistName}" />`) | ||
200 | expect(text).to.contain(`<meta property="og:description" content="${playlistDescription}" />`) | ||
201 | expect(text).to.contain('<meta property="og:type" content="video" />') | ||
202 | expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/w/p/${playlist.uuid}" />`) | ||
203 | } | ||
141 | 204 | ||
142 | it('Should have valid Open Graph tags on the account page', async function () { | 205 | it('Should have valid Open Graph tags on the account page', async function () { |
143 | const res = await request(servers[0].url) | 206 | await accountPageTest('/accounts/' + servers[0].user.username) |
144 | .get('/accounts/' + servers[0].user.username) | 207 | await accountPageTest('/a/' + servers[0].user.username) |
145 | .set('Accept', 'text/html') | 208 | await accountPageTest('/@' + servers[0].user.username) |
146 | .expect(HttpStatusCode.OK_200) | ||
147 | |||
148 | expect(res.text).to.contain(`<meta property="og:title" content="${account.displayName}" />`) | ||
149 | expect(res.text).to.contain(`<meta property="og:description" content="${account.description}" />`) | ||
150 | expect(res.text).to.contain('<meta property="og:type" content="website" />') | ||
151 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`) | ||
152 | }) | 209 | }) |
153 | 210 | ||
154 | it('Should have valid Open Graph tags on the channel page', async function () { | 211 | it('Should have valid Open Graph tags on the channel page', async function () { |
155 | const res = await request(servers[0].url) | 212 | await channelPageTest('/video-channels/' + servers[0].videoChannel.name) |
156 | .get('/video-channels/' + servers[0].videoChannel.name) | 213 | await channelPageTest('/c/' + servers[0].videoChannel.name) |
157 | .set('Accept', 'text/html') | 214 | await channelPageTest('/@' + servers[0].videoChannel.name) |
158 | .expect(HttpStatusCode.OK_200) | ||
159 | |||
160 | expect(res.text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`) | ||
161 | expect(res.text).to.contain(`<meta property="og:description" content="${channelDescription}" />`) | ||
162 | expect(res.text).to.contain('<meta property="og:type" content="website" />') | ||
163 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`) | ||
164 | }) | 215 | }) |
165 | 216 | ||
166 | it('Should have valid Open Graph tags on the watch page with video id', async function () { | 217 | it('Should have valid Open Graph tags on the watch page', async function () { |
167 | const res = await request(servers[0].url) | 218 | for (const path of watchVideoBasePaths) { |
168 | .get('/videos/watch/' + servers[0].video.id) | 219 | for (const id of videoIds) { |
169 | .set('Accept', 'text/html') | 220 | await watchVideoPageTest(path + id) |
170 | .expect(HttpStatusCode.OK_200) | 221 | } |
171 | 222 | } | |
172 | expect(res.text).to.contain(`<meta property="og:title" content="${videoName}" />`) | ||
173 | expect(res.text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`) | ||
174 | expect(res.text).to.contain('<meta property="og:type" content="video" />') | ||
175 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) | ||
176 | }) | ||
177 | |||
178 | it('Should have valid Open Graph tags on the watch page with video uuid', async function () { | ||
179 | const res = await request(servers[0].url) | ||
180 | .get('/videos/watch/' + servers[0].video.uuid) | ||
181 | .set('Accept', 'text/html') | ||
182 | .expect(HttpStatusCode.OK_200) | ||
183 | |||
184 | expect(res.text).to.contain(`<meta property="og:title" content="${videoName}" />`) | ||
185 | expect(res.text).to.contain(`<meta property="og:description" content="${videoDescriptionPlainText}" />`) | ||
186 | expect(res.text).to.contain('<meta property="og:type" content="video" />') | ||
187 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) | ||
188 | }) | 223 | }) |
189 | 224 | ||
190 | it('Should have valid Open Graph tags on the watch playlist page', async function () { | 225 | it('Should have valid Open Graph tags on the watch playlist page', async function () { |
191 | const res = await request(servers[0].url) | 226 | for (const path of watchPlaylistBasePaths) { |
192 | .get('/videos/watch/playlist/' + playlistUUID) | 227 | for (const id of playlistIds) { |
193 | .set('Accept', 'text/html') | 228 | await watchPlaylistPageTest(path + id) |
194 | .expect(HttpStatusCode.OK_200) | 229 | } |
195 | 230 | } | |
196 | expect(res.text).to.contain(`<meta property="og:title" content="${playlistName}" />`) | ||
197 | expect(res.text).to.contain(`<meta property="og:description" content="${playlistDescription}" />`) | ||
198 | expect(res.text).to.contain('<meta property="og:type" content="video" />') | ||
199 | expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/videos/watch/playlist/${playlistUUID}" />`) | ||
200 | }) | 231 | }) |
201 | }) | 232 | }) |
202 | 233 | ||
203 | describe('Twitter card', async function () { | 234 | describe('Twitter card', async function () { |
204 | 235 | ||
205 | it('Should have valid twitter card on the watch video page', async function () { | 236 | describe('Not whitelisted', function () { |
206 | const res = await request(servers[0].url) | ||
207 | .get('/videos/watch/' + servers[0].video.uuid) | ||
208 | .set('Accept', 'text/html') | ||
209 | .expect(HttpStatusCode.OK_200) | ||
210 | 237 | ||
211 | expect(res.text).to.contain('<meta property="twitter:card" content="summary_large_image" />') | 238 | async function accountPageTest (path: string) { |
212 | expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | 239 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
213 | expect(res.text).to.contain(`<meta property="twitter:title" content="${videoName}" />`) | 240 | const text = res.text |
214 | expect(res.text).to.contain(`<meta property="twitter:description" content="${videoDescriptionPlainText}" />`) | ||
215 | }) | ||
216 | 241 | ||
217 | it('Should have valid twitter card on the watch playlist page', async function () { | 242 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') |
218 | const res = await request(servers[0].url) | 243 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') |
219 | .get('/videos/watch/playlist/' + playlistUUID) | 244 | expect(text).to.contain(`<meta property="twitter:title" content="${account.name}" />`) |
220 | .set('Accept', 'text/html') | 245 | expect(text).to.contain(`<meta property="twitter:description" content="${account.description}" />`) |
221 | .expect(HttpStatusCode.OK_200) | 246 | } |
222 | 247 | ||
223 | expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') | 248 | async function channelPageTest (path: string) { |
224 | expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | 249 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
225 | expect(res.text).to.contain(`<meta property="twitter:title" content="${playlistName}" />`) | 250 | const text = res.text |
226 | expect(res.text).to.contain(`<meta property="twitter:description" content="${playlistDescription}" />`) | ||
227 | }) | ||
228 | 251 | ||
229 | it('Should have valid twitter card on the account page', async function () { | 252 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') |
230 | const res = await request(servers[0].url) | 253 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') |
231 | .get('/accounts/' + account.name) | 254 | expect(text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`) |
232 | .set('Accept', 'text/html') | 255 | expect(text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`) |
233 | .expect(HttpStatusCode.OK_200) | 256 | } |
234 | 257 | ||
235 | expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') | 258 | async function watchVideoPageTest (path: string) { |
236 | expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | 259 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
237 | expect(res.text).to.contain(`<meta property="twitter:title" content="${account.name}" />`) | 260 | const text = res.text |
238 | expect(res.text).to.contain(`<meta property="twitter:description" content="${account.description}" />`) | 261 | |
239 | }) | 262 | expect(text).to.contain('<meta property="twitter:card" content="summary_large_image" />') |
263 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | ||
264 | expect(text).to.contain(`<meta property="twitter:title" content="${videoName}" />`) | ||
265 | expect(text).to.contain(`<meta property="twitter:description" content="${videoDescriptionPlainText}" />`) | ||
266 | } | ||
240 | 267 | ||
241 | it('Should have valid twitter card on the channel page', async function () { | 268 | async function watchPlaylistPageTest (path: string) { |
242 | const res = await request(servers[0].url) | 269 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
243 | .get('/video-channels/' + servers[0].videoChannel.name) | 270 | const text = res.text |
244 | .set('Accept', 'text/html') | 271 | |
245 | .expect(HttpStatusCode.OK_200) | 272 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') |
273 | expect(text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | ||
274 | expect(text).to.contain(`<meta property="twitter:title" content="${playlistName}" />`) | ||
275 | expect(text).to.contain(`<meta property="twitter:description" content="${playlistDescription}" />`) | ||
276 | } | ||
246 | 277 | ||
247 | expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') | 278 | it('Should have valid twitter card on the watch video page', async function () { |
248 | expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') | 279 | for (const path of watchVideoBasePaths) { |
249 | expect(res.text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`) | 280 | for (const id of videoIds) { |
250 | expect(res.text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`) | 281 | await watchVideoPageTest(path + id) |
282 | } | ||
283 | } | ||
284 | }) | ||
285 | |||
286 | it('Should have valid twitter card on the watch playlist page', async function () { | ||
287 | for (const path of watchPlaylistBasePaths) { | ||
288 | for (const id of playlistIds) { | ||
289 | await watchPlaylistPageTest(path + id) | ||
290 | } | ||
291 | } | ||
292 | }) | ||
293 | |||
294 | it('Should have valid twitter card on the account page', async function () { | ||
295 | await accountPageTest('/accounts/' + account.name) | ||
296 | await accountPageTest('/a/' + account.name) | ||
297 | await accountPageTest('/@' + account.name) | ||
298 | }) | ||
299 | |||
300 | it('Should have valid twitter card on the channel page', async function () { | ||
301 | await channelPageTest('/video-channels/' + servers[0].videoChannel.name) | ||
302 | await channelPageTest('/c/' + servers[0].videoChannel.name) | ||
303 | await channelPageTest('/@' + servers[0].videoChannel.name) | ||
304 | }) | ||
251 | }) | 305 | }) |
252 | 306 | ||
253 | it('Should have valid twitter card if Twitter is whitelisted', async function () { | 307 | describe('Whitelisted', function () { |
254 | const res1 = await getCustomConfig(servers[0].url, servers[0].accessToken) | 308 | |
255 | const config = res1.body | 309 | before(async function () { |
256 | config.services.twitter = { | 310 | const res = await getCustomConfig(servers[0].url, servers[0].accessToken) |
257 | username: '@Kuja', | 311 | const config = res.body as CustomConfig |
258 | whitelisted: true | 312 | config.services.twitter = { |
313 | username: '@Kuja', | ||
314 | whitelisted: true | ||
315 | } | ||
316 | |||
317 | await updateCustomConfig(servers[0].url, servers[0].accessToken, config) | ||
318 | }) | ||
319 | |||
320 | async function accountPageTest (path: string) { | ||
321 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) | ||
322 | const text = res.text | ||
323 | |||
324 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') | ||
325 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') | ||
259 | } | 326 | } |
260 | await updateCustomConfig(servers[0].url, servers[0].accessToken, config) | ||
261 | 327 | ||
262 | const resVideoRequest = await request(servers[0].url) | 328 | async function channelPageTest (path: string) { |
263 | .get('/videos/watch/' + servers[0].video.uuid) | 329 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
264 | .set('Accept', 'text/html') | 330 | const text = res.text |
265 | .expect(HttpStatusCode.OK_200) | ||
266 | 331 | ||
267 | expect(resVideoRequest.text).to.contain('<meta property="twitter:card" content="player" />') | 332 | expect(text).to.contain('<meta property="twitter:card" content="summary" />') |
268 | expect(resVideoRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') | 333 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') |
334 | } | ||
269 | 335 | ||
270 | const resVideoPlaylistRequest = await request(servers[0].url) | 336 | async function watchVideoPageTest (path: string) { |
271 | .get('/videos/watch/playlist/' + playlistUUID) | 337 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
272 | .set('Accept', 'text/html') | 338 | const text = res.text |
273 | .expect(HttpStatusCode.OK_200) | ||
274 | 339 | ||
275 | expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:card" content="player" />') | 340 | expect(text).to.contain('<meta property="twitter:card" content="player" />') |
276 | expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') | 341 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') |
342 | } | ||
277 | 343 | ||
278 | const resAccountRequest = await request(servers[0].url) | 344 | async function watchPlaylistPageTest (path: string) { |
279 | .get('/accounts/' + account.name) | 345 | const res = await makeGetRequest({ url: servers[0].url, path, accept: 'text/html', statusCodeExpected: HttpStatusCode.OK_200 }) |
280 | .set('Accept', 'text/html') | 346 | const text = res.text |
281 | .expect(HttpStatusCode.OK_200) | ||
282 | 347 | ||
283 | expect(resAccountRequest.text).to.contain('<meta property="twitter:card" content="summary" />') | 348 | expect(text).to.contain('<meta property="twitter:card" content="player" />') |
284 | expect(resAccountRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') | 349 | expect(text).to.contain('<meta property="twitter:site" content="@Kuja" />') |
350 | } | ||
285 | 351 | ||
286 | const resChannelRequest = await request(servers[0].url) | 352 | it('Should have valid twitter card on the watch video page', async function () { |
287 | .get('/video-channels/' + servers[0].videoChannel.name) | 353 | for (const path of watchVideoBasePaths) { |
288 | .set('Accept', 'text/html') | 354 | for (const id of videoIds) { |
289 | .expect(HttpStatusCode.OK_200) | 355 | await watchVideoPageTest(path + id) |
356 | } | ||
357 | } | ||
358 | }) | ||
359 | |||
360 | it('Should have valid twitter card on the watch playlist page', async function () { | ||
361 | for (const path of watchPlaylistBasePaths) { | ||
362 | for (const id of playlistIds) { | ||
363 | await watchPlaylistPageTest(path + id) | ||
364 | } | ||
365 | } | ||
366 | }) | ||
290 | 367 | ||
291 | expect(resChannelRequest.text).to.contain('<meta property="twitter:card" content="summary" />') | 368 | it('Should have valid twitter card on the account page', async function () { |
292 | expect(resChannelRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') | 369 | await accountPageTest('/accounts/' + account.name) |
370 | await accountPageTest('/a/' + account.name) | ||
371 | await accountPageTest('/@' + account.name) | ||
372 | }) | ||
373 | |||
374 | it('Should have valid twitter card on the channel page', async function () { | ||
375 | await channelPageTest('/video-channels/' + servers[0].videoChannel.name) | ||
376 | await channelPageTest('/c/' + servers[0].videoChannel.name) | ||
377 | await channelPageTest('/@' + servers[0].videoChannel.name) | ||
378 | }) | ||
293 | }) | 379 | }) |
294 | }) | 380 | }) |
295 | 381 | ||
296 | describe('Index HTML', function () { | 382 | describe('Index HTML', function () { |
297 | 383 | ||
298 | it('Should have valid index html tags (title, description...)', async function () { | 384 | it('Should have valid index html tags (title, description...)', async function () { |
385 | const resConfig = await getConfig(servers[0].url) | ||
299 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') | 386 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') |
300 | 387 | ||
301 | const description = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.' | 388 | const description = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.' |
302 | checkIndexTags(res.text, 'PeerTube', description, '') | 389 | checkIndexTags(res.text, 'PeerTube', description, '', resConfig.body) |
303 | }) | 390 | }) |
304 | 391 | ||
305 | it('Should update the customized configuration and have the correct index html tags', async function () { | 392 | it('Should update the customized configuration and have the correct index html tags', async function () { |
@@ -318,35 +405,65 @@ describe('Test a client controllers', function () { | |||
318 | } | 405 | } |
319 | }) | 406 | }) |
320 | 407 | ||
408 | const resConfig = await getConfig(servers[0].url) | ||
321 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') | 409 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') |
322 | 410 | ||
323 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }') | 411 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body) |
324 | }) | 412 | }) |
325 | 413 | ||
326 | it('Should have valid index html updated tags (title, description...)', async function () { | 414 | it('Should have valid index html updated tags (title, description...)', async function () { |
415 | const resConfig = await getConfig(servers[0].url) | ||
327 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') | 416 | const res = await makeHTMLRequest(servers[0].url, '/videos/trending') |
328 | 417 | ||
329 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }') | 418 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body) |
330 | }) | 419 | }) |
331 | 420 | ||
332 | it('Should use the original video URL for the canonical tag', async function () { | 421 | it('Should use the original video URL for the canonical tag', async function () { |
333 | const res = await makeHTMLRequest(servers[1].url, '/videos/watch/' + servers[0].video.uuid) | 422 | for (const basePath of watchVideoBasePaths) { |
334 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) | 423 | for (const id of videoIds) { |
424 | const res = await makeHTMLRequest(servers[1].url, basePath + id) | ||
425 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/videos/watch/${servers[0].video.uuid}" />`) | ||
426 | } | ||
427 | } | ||
335 | }) | 428 | }) |
336 | 429 | ||
337 | it('Should use the original account URL for the canonical tag', async function () { | 430 | it('Should use the original account URL for the canonical tag', async function () { |
338 | const res = await makeHTMLRequest(servers[1].url, '/accounts/root@' + servers[0].host) | 431 | const accountURLtest = (res) => { |
339 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`) | 432 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`) |
433 | } | ||
434 | |||
435 | accountURLtest(await makeHTMLRequest(servers[1].url, '/accounts/root@' + servers[0].host)) | ||
436 | accountURLtest(await makeHTMLRequest(servers[1].url, '/a/root@' + servers[0].host)) | ||
437 | accountURLtest(await makeHTMLRequest(servers[1].url, '/@root@' + servers[0].host)) | ||
340 | }) | 438 | }) |
341 | 439 | ||
342 | it('Should use the original channel URL for the canonical tag', async function () { | 440 | it('Should use the original channel URL for the canonical tag', async function () { |
343 | const res = await makeHTMLRequest(servers[1].url, '/video-channels/root_channel@' + servers[0].host) | 441 | const channelURLtests = (res) => { |
344 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`) | 442 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`) |
443 | } | ||
444 | |||
445 | channelURLtests(await makeHTMLRequest(servers[1].url, '/video-channels/root_channel@' + servers[0].host)) | ||
446 | channelURLtests(await makeHTMLRequest(servers[1].url, '/c/root_channel@' + servers[0].host)) | ||
447 | channelURLtests(await makeHTMLRequest(servers[1].url, '/@root_channel@' + servers[0].host)) | ||
345 | }) | 448 | }) |
346 | 449 | ||
347 | it('Should use the original playlist URL for the canonical tag', async function () { | 450 | it('Should use the original playlist URL for the canonical tag', async function () { |
348 | const res = await makeHTMLRequest(servers[1].url, '/videos/watch/playlist/' + playlistUUID) | 451 | for (const basePath of watchPlaylistBasePaths) { |
349 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-playlists/${playlistUUID}" />`) | 452 | for (const id of playlistIds) { |
453 | const res = await makeHTMLRequest(servers[1].url, basePath + id) | ||
454 | expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-playlists/${playlist.uuid}" />`) | ||
455 | } | ||
456 | } | ||
457 | }) | ||
458 | }) | ||
459 | |||
460 | describe('Embed HTML', function () { | ||
461 | |||
462 | it('Should have the correct embed html tags', async function () { | ||
463 | const resConfig = await getConfig(servers[0].url) | ||
464 | const res = await makeHTMLRequest(servers[0].url, servers[0].video.embedPath) | ||
465 | |||
466 | checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body) | ||
350 | }) | 467 | }) |
351 | }) | 468 | }) |
352 | 469 | ||
diff --git a/server/tests/external-plugins/auto-block-videos.ts b/server/tests/external-plugins/auto-block-videos.ts index 1b91d141e..18ea17d78 100644 --- a/server/tests/external-plugins/auto-block-videos.ts +++ b/server/tests/external-plugins/auto-block-videos.ts | |||
@@ -40,6 +40,7 @@ describe('Official plugin auto-block videos', function () { | |||
40 | let blocklistServer: MockBlocklist | 40 | let blocklistServer: MockBlocklist |
41 | let server1Videos: Video[] = [] | 41 | let server1Videos: Video[] = [] |
42 | let server2Videos: Video[] = [] | 42 | let server2Videos: Video[] = [] |
43 | let port: number | ||
43 | 44 | ||
44 | before(async function () { | 45 | before(async function () { |
45 | this.timeout(60000) | 46 | this.timeout(60000) |
@@ -56,7 +57,7 @@ describe('Official plugin auto-block videos', function () { | |||
56 | } | 57 | } |
57 | 58 | ||
58 | blocklistServer = new MockBlocklist() | 59 | blocklistServer = new MockBlocklist() |
59 | await blocklistServer.initialize() | 60 | port = await blocklistServer.initialize() |
60 | 61 | ||
61 | await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' }) | 62 | await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' }) |
62 | await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' }) | 63 | await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' }) |
@@ -82,7 +83,7 @@ describe('Official plugin auto-block videos', function () { | |||
82 | accessToken: servers[0].accessToken, | 83 | accessToken: servers[0].accessToken, |
83 | npmName: 'peertube-plugin-auto-block-videos', | 84 | npmName: 'peertube-plugin-auto-block-videos', |
84 | settings: { | 85 | settings: { |
85 | 'blocklist-urls': 'http://localhost:42100/blocklist', | 86 | 'blocklist-urls': `http://localhost:${port}/blocklist`, |
86 | 'check-seconds-interval': 1 | 87 | 'check-seconds-interval': 1 |
87 | } | 88 | } |
88 | }) | 89 | }) |
diff --git a/server/tests/external-plugins/auto-mute.ts b/server/tests/external-plugins/auto-mute.ts index 687f56e98..09355d932 100644 --- a/server/tests/external-plugins/auto-mute.ts +++ b/server/tests/external-plugins/auto-mute.ts | |||
@@ -31,6 +31,7 @@ describe('Official plugin auto-mute', function () { | |||
31 | const autoMuteListPath = '/plugins/auto-mute/router/api/v1/mute-list' | 31 | const autoMuteListPath = '/plugins/auto-mute/router/api/v1/mute-list' |
32 | let servers: ServerInfo[] | 32 | let servers: ServerInfo[] |
33 | let blocklistServer: MockBlocklist | 33 | let blocklistServer: MockBlocklist |
34 | let port: number | ||
34 | 35 | ||
35 | before(async function () { | 36 | before(async function () { |
36 | this.timeout(30000) | 37 | this.timeout(30000) |
@@ -47,7 +48,7 @@ describe('Official plugin auto-mute', function () { | |||
47 | } | 48 | } |
48 | 49 | ||
49 | blocklistServer = new MockBlocklist() | 50 | blocklistServer = new MockBlocklist() |
50 | await blocklistServer.initialize() | 51 | port = await blocklistServer.initialize() |
51 | 52 | ||
52 | await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' }) | 53 | await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' }) |
53 | await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' }) | 54 | await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' }) |
@@ -61,7 +62,7 @@ describe('Official plugin auto-mute', function () { | |||
61 | accessToken: servers[0].accessToken, | 62 | accessToken: servers[0].accessToken, |
62 | npmName: 'peertube-plugin-auto-mute', | 63 | npmName: 'peertube-plugin-auto-mute', |
63 | settings: { | 64 | settings: { |
64 | 'blocklist-urls': 'http://localhost:42100/blocklist', | 65 | 'blocklist-urls': `http://localhost:${port}/blocklist`, |
65 | 'check-seconds-interval': 1 | 66 | 'check-seconds-interval': 1 |
66 | } | 67 | } |
67 | }) | 68 | }) |
diff --git a/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json b/server/tests/fixtures/ap-json/mastodon/bad-body-http-signature.json index 4e7bc3af5..4e7bc3af5 100644 --- a/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json +++ b/server/tests/fixtures/ap-json/mastodon/bad-body-http-signature.json | |||
diff --git a/server/tests/api/activitypub/json/mastodon/bad-http-signature.json b/server/tests/fixtures/ap-json/mastodon/bad-http-signature.json index 098597db0..098597db0 100644 --- a/server/tests/api/activitypub/json/mastodon/bad-http-signature.json +++ b/server/tests/fixtures/ap-json/mastodon/bad-http-signature.json | |||
diff --git a/server/tests/api/activitypub/json/mastodon/bad-public-key.json b/server/tests/fixtures/ap-json/mastodon/bad-public-key.json index 73d18b3ad..73d18b3ad 100644 --- a/server/tests/api/activitypub/json/mastodon/bad-public-key.json +++ b/server/tests/fixtures/ap-json/mastodon/bad-public-key.json | |||
diff --git a/server/tests/api/activitypub/json/mastodon/create-bad-signature.json b/server/tests/fixtures/ap-json/mastodon/create-bad-signature.json index 2cd037241..2cd037241 100644 --- a/server/tests/api/activitypub/json/mastodon/create-bad-signature.json +++ b/server/tests/fixtures/ap-json/mastodon/create-bad-signature.json | |||
diff --git a/server/tests/api/activitypub/json/mastodon/create.json b/server/tests/fixtures/ap-json/mastodon/create.json index 0be271bb8..0be271bb8 100644 --- a/server/tests/api/activitypub/json/mastodon/create.json +++ b/server/tests/fixtures/ap-json/mastodon/create.json | |||
diff --git a/server/tests/api/activitypub/json/mastodon/http-signature.json b/server/tests/fixtures/ap-json/mastodon/http-signature.json index 4e7bc3af5..4e7bc3af5 100644 --- a/server/tests/api/activitypub/json/mastodon/http-signature.json +++ b/server/tests/fixtures/ap-json/mastodon/http-signature.json | |||
diff --git a/server/tests/api/activitypub/json/mastodon/public-key.json b/server/tests/fixtures/ap-json/mastodon/public-key.json index b7b9b8308..b7b9b8308 100644 --- a/server/tests/api/activitypub/json/mastodon/public-key.json +++ b/server/tests/fixtures/ap-json/mastodon/public-key.json | |||
diff --git a/server/tests/api/activitypub/json/peertube/announce-without-context.json b/server/tests/fixtures/ap-json/peertube/announce-without-context.json index 5f2af0cde..5f2af0cde 100644 --- a/server/tests/api/activitypub/json/peertube/announce-without-context.json +++ b/server/tests/fixtures/ap-json/peertube/announce-without-context.json | |||
diff --git a/server/tests/api/activitypub/json/peertube/invalid-keys.json b/server/tests/fixtures/ap-json/peertube/invalid-keys.json index 0544e96b9..0544e96b9 100644 --- a/server/tests/api/activitypub/json/peertube/invalid-keys.json +++ b/server/tests/fixtures/ap-json/peertube/invalid-keys.json | |||
diff --git a/server/tests/api/activitypub/json/peertube/keys.json b/server/tests/fixtures/ap-json/peertube/keys.json index 1a7700865..1a7700865 100644 --- a/server/tests/api/activitypub/json/peertube/keys.json +++ b/server/tests/fixtures/ap-json/peertube/keys.json | |||
diff --git a/server/tests/fixtures/peertube-plugin-test-broken/main.js b/server/tests/fixtures/peertube-plugin-test-broken/main.js new file mode 100644 index 000000000..afdb6f7a0 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-broken/main.js | |||
@@ -0,0 +1,12 @@ | |||
1 | async function register (options) { | ||
2 | options.unknownFunction() | ||
3 | } | ||
4 | |||
5 | async function unregister () { | ||
6 | return | ||
7 | } | ||
8 | |||
9 | module.exports = { | ||
10 | register, | ||
11 | unregister | ||
12 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-three/package.json b/server/tests/fixtures/peertube-plugin-test-broken/package.json index 41d4c93fe..fd03df216 100644 --- a/server/tests/fixtures/peertube-plugin-test-three/package.json +++ b/server/tests/fixtures/peertube-plugin-test-broken/package.json | |||
@@ -1,7 +1,7 @@ | |||
1 | { | 1 | { |
2 | "name": "peertube-plugin-test-three", | 2 | "name": "peertube-plugin-test-broken", |
3 | "version": "0.0.1", | 3 | "version": "0.0.1", |
4 | "description": "Plugin test 3", | 4 | "description": "Plugin test broken", |
5 | "engine": { | 5 | "engine": { |
6 | "peertube": ">=1.3.0" | 6 | "peertube": ">=1.3.0" |
7 | }, | 7 | }, |
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/fr.json index 52d8313df..52d8313df 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/fr.json | |||
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/it.json b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/it.json index 9e187d83b..9e187d83b 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/languages/it.json +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/it.json | |||
diff --git a/server/tests/fixtures/peertube-plugin-test-two/main.js b/server/tests/fixtures/peertube-plugin-test-filter-translations/main.js index 71c11b2ba..71c11b2ba 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/main.js +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/main.js | |||
diff --git a/server/tests/fixtures/peertube-plugin-test-two/package.json b/server/tests/fixtures/peertube-plugin-test-filter-translations/package.json index 926f2d69b..2adce4743 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/package.json +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/package.json | |||
@@ -1,7 +1,7 @@ | |||
1 | { | 1 | { |
2 | "name": "peertube-plugin-test-two", | 2 | "name": "peertube-plugin-test-filter-translations", |
3 | "version": "0.0.1", | 3 | "version": "0.0.1", |
4 | "description": "Plugin test 2", | 4 | "description": "Plugin test filter and translations", |
5 | "engine": { | 5 | "engine": { |
6 | "peertube": ">=1.3.0" | 6 | "peertube": ">=1.3.0" |
7 | }, | 7 | }, |
diff --git a/server/tests/fixtures/peertube-plugin-test-three/main.js b/server/tests/fixtures/peertube-plugin-test-video-constants/main.js index f2b89bcf0..3e650e0a1 100644 --- a/server/tests/fixtures/peertube-plugin-test-three/main.js +++ b/server/tests/fixtures/peertube-plugin-test-video-constants/main.js | |||
@@ -11,8 +11,10 @@ async function register ({ | |||
11 | }) { | 11 | }) { |
12 | videoLanguageManager.addLanguage('al_bhed', 'Al Bhed') | 12 | videoLanguageManager.addLanguage('al_bhed', 'Al Bhed') |
13 | videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2') | 13 | videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2') |
14 | videoLanguageManager.addLanguage('al_bhed3', 'Al Bhed 3') | ||
14 | videoLanguageManager.deleteLanguage('en') | 15 | videoLanguageManager.deleteLanguage('en') |
15 | videoLanguageManager.deleteLanguage('fr') | 16 | videoLanguageManager.deleteLanguage('fr') |
17 | videoLanguageManager.deleteLanguage('al_bhed3') | ||
16 | 18 | ||
17 | videoCategoryManager.addCategory(42, 'Best category') | 19 | videoCategoryManager.addCategory(42, 'Best category') |
18 | videoCategoryManager.addCategory(43, 'High best category') | 20 | videoCategoryManager.addCategory(43, 'High best category') |
diff --git a/server/tests/fixtures/peertube-plugin-test-video-constants/package.json b/server/tests/fixtures/peertube-plugin-test-video-constants/package.json new file mode 100644 index 000000000..0fcf39933 --- /dev/null +++ b/server/tests/fixtures/peertube-plugin-test-video-constants/package.json | |||
@@ -0,0 +1,20 @@ | |||
1 | { | ||
2 | "name": "peertube-plugin-test-video-constants", | ||
3 | "version": "0.0.1", | ||
4 | "description": "Plugin test video constants", | ||
5 | "engine": { | ||
6 | "peertube": ">=1.3.0" | ||
7 | }, | ||
8 | "keywords": [ | ||
9 | "peertube", | ||
10 | "plugin" | ||
11 | ], | ||
12 | "homepage": "https://github.com/Chocobozzz/PeerTube", | ||
13 | "author": "Chocobozzz", | ||
14 | "bugs": "https://github.com/Chocobozzz/PeerTube/issues", | ||
15 | "library": "./main.js", | ||
16 | "staticDirs": {}, | ||
17 | "css": [], | ||
18 | "clientScripts": [], | ||
19 | "translations": {} | ||
20 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index ee0bc39f3..f8e6f0b98 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -19,7 +19,9 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
19 | 'action:api.user.created', | 19 | 'action:api.user.created', |
20 | 'action:api.user.deleted', | 20 | 'action:api.user.deleted', |
21 | 'action:api.user.updated', | 21 | 'action:api.user.updated', |
22 | 'action:api.user.oauth2-got-token' | 22 | 'action:api.user.oauth2-got-token', |
23 | |||
24 | 'action:api.video-playlist-element.created' | ||
23 | ] | 25 | ] |
24 | 26 | ||
25 | for (const h of actionHooks) { | 27 | for (const h of actionHooks) { |
@@ -241,6 +243,10 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
241 | 'filter:api.search.video-channels.local.list.result', | 243 | 'filter:api.search.video-channels.local.list.result', |
242 | 'filter:api.search.video-channels.index.list.params', | 244 | 'filter:api.search.video-channels.index.list.params', |
243 | 'filter:api.search.video-channels.index.list.result', | 245 | 'filter:api.search.video-channels.index.list.result', |
246 | 'filter:api.search.video-playlists.local.list.params', | ||
247 | 'filter:api.search.video-playlists.local.list.result', | ||
248 | 'filter:api.search.video-playlists.index.list.params', | ||
249 | 'filter:api.search.video-playlists.index.list.result' | ||
244 | ] | 250 | ] |
245 | 251 | ||
246 | for (const h of searchHooks) { | 252 | for (const h of searchHooks) { |
diff --git a/server/tests/plugins/action-hooks.ts b/server/tests/plugins/action-hooks.ts index ac9f2cea5..0f57ef7fe 100644 --- a/server/tests/plugins/action-hooks.ts +++ b/server/tests/plugins/action-hooks.ts | |||
@@ -1,13 +1,15 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { ServerHookName, VideoPrivacy } from '@shared/models' | 4 | import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' |
5 | import { | 5 | import { |
6 | addVideoCommentReply, | 6 | addVideoCommentReply, |
7 | addVideoCommentThread, | 7 | addVideoCommentThread, |
8 | addVideoInPlaylist, | ||
8 | blockUser, | 9 | blockUser, |
9 | createLive, | 10 | createLive, |
10 | createUser, | 11 | createUser, |
12 | createVideoPlaylist, | ||
11 | deleteVideoComment, | 13 | deleteVideoComment, |
12 | getPluginTestPath, | 14 | getPluginTestPath, |
13 | installPlugin, | 15 | installPlugin, |
@@ -69,6 +71,7 @@ describe('Test plugin action hooks', function () { | |||
69 | }) | 71 | }) |
70 | 72 | ||
71 | describe('Videos hooks', function () { | 73 | describe('Videos hooks', function () { |
74 | |||
72 | it('Should run action:api.video.uploaded', async function () { | 75 | it('Should run action:api.video.uploaded', async function () { |
73 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' }) | 76 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' }) |
74 | videoUUID = res.body.video.uuid | 77 | videoUUID = res.body.video.uuid |
@@ -177,6 +180,41 @@ describe('Test plugin action hooks', function () { | |||
177 | }) | 180 | }) |
178 | }) | 181 | }) |
179 | 182 | ||
183 | describe('Playlist hooks', function () { | ||
184 | let playlistId: number | ||
185 | let videoId: number | ||
186 | |||
187 | before(async function () { | ||
188 | { | ||
189 | const res = await createVideoPlaylist({ | ||
190 | url: servers[0].url, | ||
191 | token: servers[0].accessToken, | ||
192 | playlistAttrs: { | ||
193 | displayName: 'My playlist', | ||
194 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
195 | } | ||
196 | }) | ||
197 | playlistId = res.body.videoPlaylist.id | ||
198 | } | ||
199 | |||
200 | { | ||
201 | const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'my super name' }) | ||
202 | videoId = res.body.video.id | ||
203 | } | ||
204 | }) | ||
205 | |||
206 | it('Should run action:api.video-playlist-element.created', async function () { | ||
207 | await addVideoInPlaylist({ | ||
208 | url: servers[0].url, | ||
209 | token: servers[0].accessToken, | ||
210 | playlistId, | ||
211 | elementAttrs: { videoId } | ||
212 | }) | ||
213 | |||
214 | await checkHook('action:api.video-playlist-element.created') | ||
215 | }) | ||
216 | }) | ||
217 | |||
180 | after(async function () { | 218 | after(async function () { |
181 | await cleanupTests(servers) | 219 | await cleanupTests(servers) |
182 | }) | 220 | }) |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index 7d4f7abb4..644b41dea 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -8,6 +8,7 @@ import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-code | |||
8 | import { | 8 | import { |
9 | addVideoCommentReply, | 9 | addVideoCommentReply, |
10 | addVideoCommentThread, | 10 | addVideoCommentThread, |
11 | advancedVideoPlaylistSearch, | ||
11 | advancedVideosSearch, | 12 | advancedVideosSearch, |
12 | createLive, | 13 | createLive, |
13 | createVideoPlaylist, | 14 | createVideoPlaylist, |
@@ -38,6 +39,7 @@ import { | |||
38 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' | 39 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers' |
39 | import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' | 40 | import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' |
40 | import { | 41 | import { |
42 | VideoCommentThreadTree, | ||
41 | VideoDetails, | 43 | VideoDetails, |
42 | VideoImport, | 44 | VideoImport, |
43 | VideoImportState, | 45 | VideoImportState, |
@@ -45,7 +47,6 @@ import { | |||
45 | VideoPlaylistPrivacy, | 47 | VideoPlaylistPrivacy, |
46 | VideoPrivacy | 48 | VideoPrivacy |
47 | } from '../../../shared/models/videos' | 49 | } from '../../../shared/models/videos' |
48 | import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' | ||
49 | 50 | ||
50 | const expect = chai.expect | 51 | const expect = chai.expect |
51 | 52 | ||
@@ -71,7 +72,7 @@ describe('Test plugin filter hooks', function () { | |||
71 | await installPlugin({ | 72 | await installPlugin({ |
72 | url: servers[0].url, | 73 | url: servers[0].url, |
73 | accessToken: servers[0].accessToken, | 74 | accessToken: servers[0].accessToken, |
74 | path: getPluginTestPath('-two') | 75 | path: getPluginTestPath('-filter-translations') |
75 | }) | 76 | }) |
76 | 77 | ||
77 | for (let i = 0; i < 10; i++) { | 78 | for (let i = 0; i < 10; i++) { |
@@ -326,7 +327,7 @@ describe('Test plugin filter hooks', function () { | |||
326 | }) | 327 | }) |
327 | 328 | ||
328 | it('Should blacklist on remote upload', async function () { | 329 | it('Should blacklist on remote upload', async function () { |
329 | this.timeout(60000) | 330 | this.timeout(120000) |
330 | 331 | ||
331 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'remote please blacklist me' }) | 332 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'remote please blacklist me' }) |
332 | await waitJobs(servers) | 333 | await waitJobs(servers) |
@@ -335,7 +336,7 @@ describe('Test plugin filter hooks', function () { | |||
335 | }) | 336 | }) |
336 | 337 | ||
337 | it('Should blacklist on remote update', async function () { | 338 | it('Should blacklist on remote update', async function () { |
338 | this.timeout(60000) | 339 | this.timeout(120000) |
339 | 340 | ||
340 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video' }) | 341 | const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video' }) |
341 | await waitJobs(servers) | 342 | await waitJobs(servers) |
@@ -372,7 +373,7 @@ describe('Test plugin filter hooks', function () { | |||
372 | const downloadVideos: VideoDetails[] = [] | 373 | const downloadVideos: VideoDetails[] = [] |
373 | 374 | ||
374 | before(async function () { | 375 | before(async function () { |
375 | this.timeout(60000) | 376 | this.timeout(120000) |
376 | 377 | ||
377 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | 378 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { |
378 | transcoding: { | 379 | transcoding: { |
@@ -525,6 +526,27 @@ describe('Test plugin filter hooks', function () { | |||
525 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1) | 526 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1) |
526 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1) | 527 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1) |
527 | }) | 528 | }) |
529 | |||
530 | it('Should run filter:api.search.video-playlists.local.list.{params,result}', async function () { | ||
531 | await advancedVideoPlaylistSearch(servers[0].url, { | ||
532 | search: 'Sun Jian' | ||
533 | }) | ||
534 | |||
535 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.params', 1) | ||
536 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.result', 1) | ||
537 | }) | ||
538 | |||
539 | it('Should run filter:api.search.video-playlists.index.list.{params,result}', async function () { | ||
540 | await advancedVideoPlaylistSearch(servers[0].url, { | ||
541 | search: 'Sun Jian', | ||
542 | searchTarget: 'search-index' | ||
543 | }) | ||
544 | |||
545 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.params', 1) | ||
546 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.result', 1) | ||
547 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.index.list.params', 1) | ||
548 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.index.list.result', 1) | ||
549 | }) | ||
528 | }) | 550 | }) |
529 | 551 | ||
530 | after(async function () { | 552 | after(async function () { |
diff --git a/server/tests/plugins/html-injection.ts b/server/tests/plugins/html-injection.ts index 293c1df21..4fa8caa3a 100644 --- a/server/tests/plugins/html-injection.ts +++ b/server/tests/plugins/html-injection.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | 15 | ||
16 | const expect = chai.expect | 16 | const expect = chai.expect |
17 | 17 | ||
18 | describe('Test plugins HTML inection', function () { | 18 | describe('Test plugins HTML injection', function () { |
19 | let server: ServerInfo = null | 19 | let server: ServerInfo = null |
20 | 20 | ||
21 | before(async function () { | 21 | before(async function () { |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index f72de8229..0296d6eb7 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -144,7 +144,7 @@ describe('Test plugin helpers', function () { | |||
144 | let videoUUIDServer1: string | 144 | let videoUUIDServer1: string |
145 | 145 | ||
146 | before(async function () { | 146 | before(async function () { |
147 | this.timeout(30000) | 147 | this.timeout(60000) |
148 | 148 | ||
149 | { | 149 | { |
150 | const res = await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' }) | 150 | const res = await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' }) |
diff --git a/server/tests/plugins/translations.ts b/server/tests/plugins/translations.ts index 8dc2043b8..9fd2ba1c5 100644 --- a/server/tests/plugins/translations.ts +++ b/server/tests/plugins/translations.ts | |||
@@ -31,7 +31,7 @@ describe('Test plugin translations', function () { | |||
31 | await installPlugin({ | 31 | await installPlugin({ |
32 | url: server.url, | 32 | url: server.url, |
33 | accessToken: server.accessToken, | 33 | accessToken: server.accessToken, |
34 | path: getPluginTestPath('-two') | 34 | path: getPluginTestPath('-filter-translations') |
35 | }) | 35 | }) |
36 | }) | 36 | }) |
37 | 37 | ||
@@ -48,7 +48,7 @@ describe('Test plugin translations', function () { | |||
48 | 'peertube-plugin-test': { | 48 | 'peertube-plugin-test': { |
49 | Hi: 'Coucou' | 49 | Hi: 'Coucou' |
50 | }, | 50 | }, |
51 | 'peertube-plugin-test-two': { | 51 | 'peertube-plugin-test-filter-translations': { |
52 | 'Hello world': 'Bonjour le monde' | 52 | 'Hello world': 'Bonjour le monde' |
53 | } | 53 | } |
54 | }) | 54 | }) |
@@ -58,14 +58,14 @@ describe('Test plugin translations', function () { | |||
58 | const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' }) | 58 | const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' }) |
59 | 59 | ||
60 | expect(res.body).to.deep.equal({ | 60 | expect(res.body).to.deep.equal({ |
61 | 'peertube-plugin-test-two': { | 61 | 'peertube-plugin-test-filter-translations': { |
62 | 'Hello world': 'Ciao, mondo!' | 62 | 'Hello world': 'Ciao, mondo!' |
63 | } | 63 | } |
64 | }) | 64 | }) |
65 | }) | 65 | }) |
66 | 66 | ||
67 | it('Should remove the plugin and remove the locales', async function () { | 67 | it('Should remove the plugin and remove the locales', async function () { |
68 | await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-two' }) | 68 | await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-filter-translations' }) |
69 | 69 | ||
70 | { | 70 | { |
71 | const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' }) | 71 | const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' }) |
diff --git a/server/tests/plugins/video-constants.ts b/server/tests/plugins/video-constants.ts index 5ee41fee1..eb014c596 100644 --- a/server/tests/plugins/video-constants.ts +++ b/server/tests/plugins/video-constants.ts | |||
@@ -32,7 +32,7 @@ describe('Test plugin altering video constants', function () { | |||
32 | await installPlugin({ | 32 | await installPlugin({ |
33 | url: server.url, | 33 | url: server.url, |
34 | accessToken: server.accessToken, | 34 | accessToken: server.accessToken, |
35 | path: getPluginTestPath('-three') | 35 | path: getPluginTestPath('-video-constants') |
36 | }) | 36 | }) |
37 | }) | 37 | }) |
38 | 38 | ||
@@ -45,6 +45,7 @@ describe('Test plugin altering video constants', function () { | |||
45 | 45 | ||
46 | expect(languages['al_bhed']).to.equal('Al Bhed') | 46 | expect(languages['al_bhed']).to.equal('Al Bhed') |
47 | expect(languages['al_bhed2']).to.equal('Al Bhed 2') | 47 | expect(languages['al_bhed2']).to.equal('Al Bhed 2') |
48 | expect(languages['al_bhed3']).to.not.exist | ||
48 | }) | 49 | }) |
49 | 50 | ||
50 | it('Should have updated categories', async function () { | 51 | it('Should have updated categories', async function () { |
@@ -116,7 +117,7 @@ describe('Test plugin altering video constants', function () { | |||
116 | }) | 117 | }) |
117 | 118 | ||
118 | it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () { | 119 | it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () { |
119 | await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-three' }) | 120 | await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-video-constants' }) |
120 | 121 | ||
121 | { | 122 | { |
122 | const res = await getVideoLanguages(server.url) | 123 | const res = await getVideoLanguages(server.url) |
@@ -127,6 +128,7 @@ describe('Test plugin altering video constants', function () { | |||
127 | 128 | ||
128 | expect(languages['al_bhed']).to.not.exist | 129 | expect(languages['al_bhed']).to.not.exist |
129 | expect(languages['al_bhed2']).to.not.exist | 130 | expect(languages['al_bhed2']).to.not.exist |
131 | expect(languages['al_bhed3']).to.not.exist | ||
130 | } | 132 | } |
131 | 133 | ||
132 | { | 134 | { |
diff --git a/server/tests/register.ts b/server/tests/register.ts new file mode 100644 index 000000000..af6c8c644 --- /dev/null +++ b/server/tests/register.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | ||
2 | |||
3 | registerTSPaths() | ||
diff --git a/server/tools/cli.ts b/server/tools/cli.ts index cc89fe46e..7b94306cd 100644 --- a/server/tools/cli.ts +++ b/server/tools/cli.ts | |||
@@ -3,12 +3,12 @@ import { getAppNumber, isTestInstance } from '../helpers/core-utils' | |||
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { root } from '../../shared/extra-utils/miscs/miscs' | 4 | import { root } from '../../shared/extra-utils/miscs/miscs' |
5 | import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels' | 5 | import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels' |
6 | import { CommanderStatic } from 'commander' | ||
7 | import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' | 6 | import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' |
8 | import { createLogger, format, transports } from 'winston' | 7 | import { createLogger, format, transports } from 'winston' |
9 | import { getMyUserInformation } from '@shared/extra-utils/users/users' | 8 | import { getMyUserInformation } from '@shared/extra-utils/users/users' |
10 | import { User, UserRole } from '@shared/models' | 9 | import { User, UserRole } from '@shared/models' |
11 | import { getAccessToken } from '@shared/extra-utils/users/login' | 10 | import { getAccessToken } from '@shared/extra-utils/users/login' |
11 | import { Command } from 'commander' | ||
12 | 12 | ||
13 | let configName = 'PeerTube/CLI' | 13 | let configName = 'PeerTube/CLI' |
14 | if (isTestInstance()) configName += `-${getAppNumber()}` | 14 | if (isTestInstance()) configName += `-${getAppNumber()}` |
@@ -69,7 +69,7 @@ function deleteSettings () { | |||
69 | } | 69 | } |
70 | 70 | ||
71 | function getRemoteObjectOrDie ( | 71 | function getRemoteObjectOrDie ( |
72 | program: CommanderStatic, | 72 | program: Command, |
73 | settings: Settings, | 73 | settings: Settings, |
74 | netrc: Netrc | 74 | netrc: Netrc |
75 | ): { url: string, username: string, password: string } { | 75 | ): { url: string, username: string, password: string } { |
@@ -106,7 +106,7 @@ function getRemoteObjectOrDie ( | |||
106 | } | 106 | } |
107 | } | 107 | } |
108 | 108 | ||
109 | function buildCommonVideoOptions (command: CommanderStatic) { | 109 | function buildCommonVideoOptions (command: Command) { |
110 | function list (val) { | 110 | function list (val) { |
111 | return val.split(',') | 111 | return val.split(',') |
112 | } | 112 | } |
@@ -128,7 +128,7 @@ function buildCommonVideoOptions (command: CommanderStatic) { | |||
128 | .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info') | 128 | .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info') |
129 | } | 129 | } |
130 | 130 | ||
131 | async function buildVideoAttributesFromCommander (url: string, command: CommanderStatic, defaultAttributes: any = {}) { | 131 | async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) { |
132 | const options = command.opts() | 132 | const options = command.opts() |
133 | 133 | ||
134 | const defaultBooleanAttributes = { | 134 | const defaultBooleanAttributes = { |
@@ -177,7 +177,7 @@ async function buildVideoAttributesFromCommander (url: string, command: Commande | |||
177 | return videoAttributes | 177 | return videoAttributes |
178 | } | 178 | } |
179 | 179 | ||
180 | function getServerCredentials (program: CommanderStatic) { | 180 | function getServerCredentials (program: Command) { |
181 | return Promise.all([ getSettings(), getNetrc() ]) | 181 | return Promise.all([ getSettings(), getNetrc() ]) |
182 | .then(([ settings, netrc ]) => { | 182 | .then(([ settings, netrc ]) => { |
183 | return getRemoteObjectOrDie(program, settings, netrc) | 183 | return getRemoteObjectOrDie(program, settings, netrc) |
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts index e54649002..1934e7986 100644 --- a/server/tools/peertube-auth.ts +++ b/server/tools/peertube-auth.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import { registerTSPaths } from '../helpers/register-ts-paths' | 3 | import { registerTSPaths } from '../helpers/register-ts-paths' |
4 | registerTSPaths() | 4 | registerTSPaths() |
5 | 5 | ||
6 | import * as program from 'commander' | 6 | import { OptionValues, program } from 'commander' |
7 | import * as prompt from 'prompt' | 7 | import * as prompt from 'prompt' |
8 | import { getNetrc, getSettings, writeSettings } from './cli' | 8 | import { getNetrc, getSettings, writeSettings } from './cli' |
9 | import { isUserUsernameValid } from '../helpers/custom-validators/users' | 9 | import { isUserUsernameValid } from '../helpers/custom-validators/users' |
@@ -66,7 +66,7 @@ program | |||
66 | .option('-U, --username <username>', 'Username') | 66 | .option('-U, --username <username>', 'Username') |
67 | .option('-p, --password <token>', 'Password') | 67 | .option('-p, --password <token>', 'Password') |
68 | .option('--default', 'add the entry as the new default') | 68 | .option('--default', 'add the entry as the new default') |
69 | .action((options: program.OptionValues) => { | 69 | .action((options: OptionValues) => { |
70 | /* eslint-disable no-import-assign */ | 70 | /* eslint-disable no-import-assign */ |
71 | prompt.override = options | 71 | prompt.override = options |
72 | prompt.start() | 72 | prompt.start() |
diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts index b2c278c48..9488eba0e 100644 --- a/server/tools/peertube-get-access-token.ts +++ b/server/tools/peertube-get-access-token.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | 1 | import { registerTSPaths } from '../helpers/register-ts-paths' |
2 | registerTSPaths() | 2 | registerTSPaths() |
3 | 3 | ||
4 | import * as program from 'commander' | 4 | import { program } from 'commander' |
5 | import { getClient, Server, serverLogin } from '../../shared/extra-utils' | 5 | import { getClient, Server, serverLogin } from '../../shared/extra-utils' |
6 | 6 | ||
7 | program | 7 | program |
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 915995031..101a95b2a 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | 1 | import { registerTSPaths } from '../helpers/register-ts-paths' |
2 | registerTSPaths() | 2 | registerTSPaths() |
3 | 3 | ||
4 | import * as program from 'commander' | 4 | import { program } from 'commander' |
5 | import { accessSync, constants } from 'fs' | 5 | import { accessSync, constants } from 'fs' |
6 | import { remove } from 'fs-extra' | 6 | import { remove } from 'fs-extra' |
7 | import { truncate } from 'lodash' | 7 | import { truncate } from 'lodash' |
@@ -11,9 +11,9 @@ import { promisify } from 'util' | |||
11 | import { advancedVideosSearch, getClient, getVideoCategories, login, uploadVideo } from '../../shared/extra-utils/index' | 11 | import { advancedVideosSearch, getClient, getVideoCategories, login, uploadVideo } from '../../shared/extra-utils/index' |
12 | import { sha256 } from '../helpers/core-utils' | 12 | import { sha256 } from '../helpers/core-utils' |
13 | import { doRequestAndSaveToFile } from '../helpers/requests' | 13 | import { doRequestAndSaveToFile } from '../helpers/requests' |
14 | import { buildOriginallyPublishedAt, getYoutubeDLVideoFormat, safeGetYoutubeDL } from '../helpers/youtube-dl' | ||
15 | import { CONSTRAINTS_FIELDS } from '../initializers/constants' | 14 | import { CONSTRAINTS_FIELDS } from '../initializers/constants' |
16 | import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli' | 15 | import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli' |
16 | import { YoutubeDL } from '@server/helpers/youtube-dl' | ||
17 | 17 | ||
18 | type UserInfo = { | 18 | type UserInfo = { |
19 | username: string | 19 | username: string |
@@ -74,9 +74,9 @@ async function run (url: string, user: UserInfo) { | |||
74 | user.password = await promptPassword() | 74 | user.password = await promptPassword() |
75 | } | 75 | } |
76 | 76 | ||
77 | const youtubeDL = await safeGetYoutubeDL() | 77 | const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL() |
78 | 78 | ||
79 | let info = await getYoutubeDLInfo(youtubeDL, options.targetUrl, command.args) | 79 | let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args) |
80 | 80 | ||
81 | if (!Array.isArray(info)) info = [ info ] | 81 | if (!Array.isArray(info)) info = [ info ] |
82 | 82 | ||
@@ -86,7 +86,7 @@ async function run (url: string, user: UserInfo) { | |||
86 | if (uploadsObject) { | 86 | if (uploadsObject) { |
87 | console.log('Fixing URL to %s.', uploadsObject.url) | 87 | console.log('Fixing URL to %s.', uploadsObject.url) |
88 | 88 | ||
89 | info = await getYoutubeDLInfo(youtubeDL, uploadsObject.url, command.args) | 89 | info = await getYoutubeDLInfo(youtubeDLBinary, uploadsObject.url, command.args) |
90 | } | 90 | } |
91 | 91 | ||
92 | let infoArray: any[] | 92 | let infoArray: any[] |
@@ -130,13 +130,14 @@ async function processVideo (parameters: { | |||
130 | youtubeInfo: any | 130 | youtubeInfo: any |
131 | }) { | 131 | }) { |
132 | const { youtubeInfo, cwd, url, user } = parameters | 132 | const { youtubeInfo, cwd, url, user } = parameters |
133 | const youtubeDL = new YoutubeDL('', []) | ||
133 | 134 | ||
134 | log.debug('Fetching object.', youtubeInfo) | 135 | log.debug('Fetching object.', youtubeInfo) |
135 | 136 | ||
136 | const videoInfo = await fetchObject(youtubeInfo) | 137 | const videoInfo = await fetchObject(youtubeInfo) |
137 | log.debug('Fetched object.', videoInfo) | 138 | log.debug('Fetched object.', videoInfo) |
138 | 139 | ||
139 | const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo) | 140 | const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo) |
140 | if (options.since && originallyPublishedAt && originallyPublishedAt.getTime() < options.since.getTime()) { | 141 | if (options.since && originallyPublishedAt && originallyPublishedAt.getTime() < options.since.getTime()) { |
141 | log.info('Video "%s" has been published before "%s", don\'t upload it.\n', | 142 | log.info('Video "%s" has been published before "%s", don\'t upload it.\n', |
142 | videoInfo.title, formatDate(options.since)) | 143 | videoInfo.title, formatDate(options.since)) |
@@ -161,13 +162,14 @@ async function processVideo (parameters: { | |||
161 | 162 | ||
162 | log.info('Downloading video "%s"...', videoInfo.title) | 163 | log.info('Downloading video "%s"...', videoInfo.title) |
163 | 164 | ||
164 | const youtubeDLOptions = [ '-f', getYoutubeDLVideoFormat(), ...command.args, '-o', path ] | 165 | const youtubeDLOptions = [ '-f', youtubeDL.getYoutubeDLVideoFormat(), ...command.args, '-o', path ] |
165 | try { | 166 | try { |
166 | const youtubeDL = await safeGetYoutubeDL() | 167 | const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL() |
167 | const youtubeDLExec = promisify(youtubeDL.exec).bind(youtubeDL) | 168 | const youtubeDLExec = promisify(youtubeDLBinary.exec).bind(youtubeDLBinary) |
168 | const output = await youtubeDLExec(videoInfo.url, youtubeDLOptions, processOptions) | 169 | const output = await youtubeDLExec(videoInfo.url, youtubeDLOptions, processOptions) |
169 | log.info(output.join('\n')) | 170 | log.info(output.join('\n')) |
170 | await uploadVideoOnPeerTube({ | 171 | await uploadVideoOnPeerTube({ |
172 | youtubeDL, | ||
171 | cwd, | 173 | cwd, |
172 | url, | 174 | url, |
173 | user, | 175 | user, |
@@ -180,13 +182,14 @@ async function processVideo (parameters: { | |||
180 | } | 182 | } |
181 | 183 | ||
182 | async function uploadVideoOnPeerTube (parameters: { | 184 | async function uploadVideoOnPeerTube (parameters: { |
185 | youtubeDL: YoutubeDL | ||
183 | videoInfo: any | 186 | videoInfo: any |
184 | videoPath: string | 187 | videoPath: string |
185 | cwd: string | 188 | cwd: string |
186 | url: string | 189 | url: string |
187 | user: { username: string, password: string } | 190 | user: { username: string, password: string } |
188 | }) { | 191 | }) { |
189 | const { videoInfo, videoPath, cwd, url, user } = parameters | 192 | const { youtubeDL, videoInfo, videoPath, cwd, url, user } = parameters |
190 | 193 | ||
191 | const category = await getCategory(videoInfo.categories, url) | 194 | const category = await getCategory(videoInfo.categories, url) |
192 | const licence = getLicence(videoInfo.license) | 195 | const licence = getLicence(videoInfo.license) |
@@ -205,7 +208,7 @@ async function uploadVideoOnPeerTube (parameters: { | |||
205 | await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile) | 208 | await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile) |
206 | } | 209 | } |
207 | 210 | ||
208 | const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo) | 211 | const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo) |
209 | 212 | ||
210 | const defaultAttributes = { | 213 | const defaultAttributes = { |
211 | name: truncate(videoInfo.title, { | 214 | name: truncate(videoInfo.title, { |
@@ -304,7 +307,7 @@ function fetchObject (info: any) { | |||
304 | const url = buildUrl(info) | 307 | const url = buildUrl(info) |
305 | 308 | ||
306 | return new Promise<any>(async (res, rej) => { | 309 | return new Promise<any>(async (res, rej) => { |
307 | const youtubeDL = await safeGetYoutubeDL() | 310 | const youtubeDL = await YoutubeDL.safeGetYoutubeDL() |
308 | youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => { | 311 | youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => { |
309 | if (err) return rej(err) | 312 | if (err) return rej(err) |
310 | 313 | ||
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts index c8a576844..54ea1264d 100644 --- a/server/tools/peertube-plugins.ts +++ b/server/tools/peertube-plugins.ts | |||
@@ -3,14 +3,12 @@ | |||
3 | import { registerTSPaths } from '../helpers/register-ts-paths' | 3 | import { registerTSPaths } from '../helpers/register-ts-paths' |
4 | registerTSPaths() | 4 | registerTSPaths() |
5 | 5 | ||
6 | import * as program from 'commander' | 6 | import { program, Command, OptionValues } from 'commander' |
7 | import { PluginType } from '../../shared/models/plugins/plugin.type' | ||
8 | import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' | 7 | import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' |
9 | import { getAdminTokenOrDie, getServerCredentials } from './cli' | 8 | import { getAdminTokenOrDie, getServerCredentials } from './cli' |
10 | import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' | 9 | import { PeerTubePlugin, PluginType } from '../../shared/models' |
11 | import { isAbsolute } from 'path' | 10 | import { isAbsolute } from 'path' |
12 | import * as CliTable3 from 'cli-table3' | 11 | import * as CliTable3 from 'cli-table3' |
13 | import commander = require('commander') | ||
14 | 12 | ||
15 | program | 13 | program |
16 | .name('plugins') | 14 | .name('plugins') |
@@ -63,7 +61,7 @@ program.parse(process.argv) | |||
63 | 61 | ||
64 | // ---------------------------------------------------------------------------- | 62 | // ---------------------------------------------------------------------------- |
65 | 63 | ||
66 | async function pluginsListCLI (command: commander.CommanderStatic, options: commander.OptionValues) { | 64 | async function pluginsListCLI (command: Command, options: OptionValues) { |
67 | const { url, username, password } = await getServerCredentials(command) | 65 | const { url, username, password } = await getServerCredentials(command) |
68 | const accessToken = await getAdminTokenOrDie(url, username, password) | 66 | const accessToken = await getAdminTokenOrDie(url, username, password) |
69 | 67 | ||
@@ -102,7 +100,7 @@ async function pluginsListCLI (command: commander.CommanderStatic, options: comm | |||
102 | process.exit(0) | 100 | process.exit(0) |
103 | } | 101 | } |
104 | 102 | ||
105 | async function installPluginCLI (command: commander.CommanderStatic, options: commander.OptionValues) { | 103 | async function installPluginCLI (command: Command, options: OptionValues) { |
106 | if (!options.path && !options.npmName) { | 104 | if (!options.path && !options.npmName) { |
107 | console.error('You need to specify the npm name or the path of the plugin you want to install.\n') | 105 | console.error('You need to specify the npm name or the path of the plugin you want to install.\n') |
108 | program.outputHelp() | 106 | program.outputHelp() |
@@ -133,7 +131,7 @@ async function installPluginCLI (command: commander.CommanderStatic, options: co | |||
133 | process.exit(0) | 131 | process.exit(0) |
134 | } | 132 | } |
135 | 133 | ||
136 | async function updatePluginCLI (command: commander.CommanderStatic, options: commander.OptionValues) { | 134 | async function updatePluginCLI (command: Command, options: OptionValues) { |
137 | if (!options.path && !options.npmName) { | 135 | if (!options.path && !options.npmName) { |
138 | console.error('You need to specify the npm name or the path of the plugin you want to update.\n') | 136 | console.error('You need to specify the npm name or the path of the plugin you want to update.\n') |
139 | program.outputHelp() | 137 | program.outputHelp() |
@@ -164,7 +162,7 @@ async function updatePluginCLI (command: commander.CommanderStatic, options: com | |||
164 | process.exit(0) | 162 | process.exit(0) |
165 | } | 163 | } |
166 | 164 | ||
167 | async function uninstallPluginCLI (command: commander.CommanderStatic, options: commander.OptionValues) { | 165 | async function uninstallPluginCLI (command: Command, options: OptionValues) { |
168 | if (!options.npmName) { | 166 | if (!options.npmName) { |
169 | console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n') | 167 | console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n') |
170 | program.outputHelp() | 168 | program.outputHelp() |
diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts index 5bc80ddb9..4810deee0 100644 --- a/server/tools/peertube-redundancy.ts +++ b/server/tools/peertube-redundancy.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import { registerTSPaths } from '../helpers/register-ts-paths' | 3 | import { registerTSPaths } from '../helpers/register-ts-paths' |
4 | registerTSPaths() | 4 | registerTSPaths() |
5 | 5 | ||
6 | import * as program from 'commander' | 6 | import { program, Command } from 'commander' |
7 | import { getAdminTokenOrDie, getServerCredentials } from './cli' | 7 | import { getAdminTokenOrDie, getServerCredentials } from './cli' |
8 | import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' | 8 | import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' |
9 | import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy' | 9 | import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy' |
@@ -14,7 +14,6 @@ import { URL } from 'url' | |||
14 | import { uniq } from 'lodash' | 14 | import { uniq } from 'lodash' |
15 | 15 | ||
16 | import bytes = require('bytes') | 16 | import bytes = require('bytes') |
17 | import commander = require('commander') | ||
18 | 17 | ||
19 | program | 18 | program |
20 | .name('plugins') | 19 | .name('plugins') |
@@ -105,7 +104,7 @@ async function listRedundanciesCLI (target: VideoRedundanciesTarget) { | |||
105 | process.exit(0) | 104 | process.exit(0) |
106 | } | 105 | } |
107 | 106 | ||
108 | async function addRedundancyCLI (options: { video: number }, command: commander.CommanderStatic) { | 107 | async function addRedundancyCLI (options: { video: number }, command: Command) { |
109 | const { url, username, password } = await getServerCredentials(command) | 108 | const { url, username, password } = await getServerCredentials(command) |
110 | const accessToken = await getAdminTokenOrDie(url, username, password) | 109 | const accessToken = await getAdminTokenOrDie(url, username, password) |
111 | 110 | ||
@@ -138,7 +137,7 @@ async function addRedundancyCLI (options: { video: number }, command: commander. | |||
138 | } | 137 | } |
139 | } | 138 | } |
140 | 139 | ||
141 | async function removeRedundancyCLI (options: { video: number }, command: commander.CommanderStatic) { | 140 | async function removeRedundancyCLI (options: { video: number }, command: Command) { |
142 | const { url, username, password } = await getServerCredentials(command) | 141 | const { url, username, password } = await getServerCredentials(command) |
143 | const accessToken = await getAdminTokenOrDie(url, username, password) | 142 | const accessToken = await getAdminTokenOrDie(url, username, password) |
144 | 143 | ||
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts index a38d51801..eb0a776b8 100644 --- a/server/tools/peertube-repl.ts +++ b/server/tools/peertube-repl.ts | |||
@@ -4,7 +4,6 @@ registerTSPaths() | |||
4 | import * as repl from 'repl' | 4 | import * as repl from 'repl' |
5 | import * as path from 'path' | 5 | import * as path from 'path' |
6 | import * as _ from 'lodash' | 6 | import * as _ from 'lodash' |
7 | import { uuidv1, uuidv3, uuidv4, uuidv5 } from 'uuid' | ||
8 | import * as Sequelize from 'sequelize' | 7 | import * as Sequelize from 'sequelize' |
9 | import * as YoutubeDL from 'youtube-dl' | 8 | import * as YoutubeDL from 'youtube-dl' |
10 | import { initDatabaseModels, sequelizeTypescript } from '../initializers/database' | 9 | import { initDatabaseModels, sequelizeTypescript } from '../initializers/database' |
@@ -15,7 +14,6 @@ import * as modelsUtils from '../models/utils' | |||
15 | import * as coreUtils from '../helpers/core-utils' | 14 | import * as coreUtils from '../helpers/core-utils' |
16 | import * as ffmpegUtils from '../helpers/ffmpeg-utils' | 15 | import * as ffmpegUtils from '../helpers/ffmpeg-utils' |
17 | import * as peertubeCryptoUtils from '../helpers/peertube-crypto' | 16 | import * as peertubeCryptoUtils from '../helpers/peertube-crypto' |
18 | import * as signupUtils from '../helpers/signup' | ||
19 | import * as utils from '../helpers/utils' | 17 | import * as utils from '../helpers/utils' |
20 | import * as YoutubeDLUtils from '../helpers/youtube-dl' | 18 | import * as YoutubeDLUtils from '../helpers/youtube-dl' |
21 | 19 | ||
@@ -32,10 +30,6 @@ const start = async () => { | |||
32 | env: process.env, | 30 | env: process.env, |
33 | lodash: _, | 31 | lodash: _, |
34 | path, | 32 | path, |
35 | uuidv1, | ||
36 | uuidv3, | ||
37 | uuidv4, | ||
38 | uuidv5, | ||
39 | cli, | 33 | cli, |
40 | logger, | 34 | logger, |
41 | constants, | 35 | constants, |
@@ -50,7 +44,6 @@ const start = async () => { | |||
50 | coreUtils, | 44 | coreUtils, |
51 | ffmpegUtils, | 45 | ffmpegUtils, |
52 | peertubeCryptoUtils, | 46 | peertubeCryptoUtils, |
53 | signupUtils, | ||
54 | utils, | 47 | utils, |
55 | YoutubeDLUtils | 48 | YoutubeDLUtils |
56 | } | 49 | } |
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts index 86c7f3d91..02edbd809 100644 --- a/server/tools/peertube-upload.ts +++ b/server/tools/peertube-upload.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | 1 | import { registerTSPaths } from '../helpers/register-ts-paths' |
2 | registerTSPaths() | 2 | registerTSPaths() |
3 | 3 | ||
4 | import * as program from 'commander' | 4 | import { program } from 'commander' |
5 | import { access, constants } from 'fs-extra' | 5 | import { access, constants } from 'fs-extra' |
6 | import { isAbsolute } from 'path' | 6 | import { isAbsolute } from 'path' |
7 | import { getAccessToken } from '../../shared/extra-utils' | 7 | import { getAccessToken } from '../../shared/extra-utils' |
diff --git a/server/tools/peertube-watch.ts b/server/tools/peertube-watch.ts index 6d9cfa3b7..892c9e7a6 100644 --- a/server/tools/peertube-watch.ts +++ b/server/tools/peertube-watch.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { registerTSPaths } from '../helpers/register-ts-paths' | 1 | import { registerTSPaths } from '../helpers/register-ts-paths' |
2 | registerTSPaths() | 2 | registerTSPaths() |
3 | 3 | ||
4 | import * as program from 'commander' | 4 | import { program, Option, OptionValues } from 'commander' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { execSync } from 'child_process' | 6 | import { execSync } from 'child_process' |
7 | 7 | ||
@@ -9,7 +9,7 @@ program | |||
9 | .name('watch') | 9 | .name('watch') |
10 | .arguments('<url>') | 10 | .arguments('<url>') |
11 | .addOption( | 11 | .addOption( |
12 | new program.Option('-g, --gui <player>', 'player type') | 12 | new Option('-g, --gui <player>', 'player type') |
13 | .default('vlc') | 13 | .default('vlc') |
14 | .choices([ 'airplay', 'stdout', 'chromecast', 'mpv', 'vlc', 'mplayer', 'xbmc' ]) | 14 | .choices([ 'airplay', 'stdout', 'chromecast', 'mpv', 'vlc', 'mplayer', 'xbmc' ]) |
15 | ) | 15 | ) |
@@ -22,7 +22,7 @@ program | |||
22 | .action((url, options) => run(url, options)) | 22 | .action((url, options) => run(url, options)) |
23 | .parse(process.argv) | 23 | .parse(process.argv) |
24 | 24 | ||
25 | function run (url: string, options: program.OptionValues) { | 25 | function run (url: string, options: OptionValues) { |
26 | if (!url) { | 26 | if (!url) { |
27 | console.error('<url> positional argument is required.') | 27 | console.error('<url> positional argument is required.') |
28 | process.exit(-1) | 28 | process.exit(-1) |
@@ -30,7 +30,7 @@ function run (url: string, options: program.OptionValues) { | |||
30 | 30 | ||
31 | const cmd = 'node ' + join(__dirname, 'node_modules', 'webtorrent-hybrid', 'bin', 'cmd.js') | 31 | const cmd = 'node ' + join(__dirname, 'node_modules', 'webtorrent-hybrid', 'bin', 'cmd.js') |
32 | const args = ` --${options.gui} ` + | 32 | const args = ` --${options.gui} ` + |
33 | url.replace('videos/watch', 'download/torrents') + | 33 | url.replace(/(\/videos\/watch\/)|\/w\//, '/download/torrents/') + |
34 | `-${options.resolution}.torrent` | 34 | `-${options.resolution}.torrent` |
35 | 35 | ||
36 | try { | 36 | try { |
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts index 655f07f0c..a40c1332e 100644 --- a/server/tools/peertube.ts +++ b/server/tools/peertube.ts | |||
@@ -5,7 +5,7 @@ | |||
5 | import { registerTSPaths } from '../helpers/register-ts-paths' | 5 | import { registerTSPaths } from '../helpers/register-ts-paths' |
6 | registerTSPaths() | 6 | registerTSPaths() |
7 | 7 | ||
8 | import * as program from 'commander' | 8 | import { CommandOptions, program } from 'commander' |
9 | import { getSettings, version } from './cli' | 9 | import { getSettings, version } from './cli' |
10 | 10 | ||
11 | program | 11 | program |
@@ -28,11 +28,11 @@ program | |||
28 | .command( | 28 | .command( |
29 | 'diagnostic [action]', | 29 | 'diagnostic [action]', |
30 | 'like couple therapy, but for your instance', | 30 | 'like couple therapy, but for your instance', |
31 | { noHelp: true } as program.CommandOptions | 31 | { noHelp: true } as CommandOptions |
32 | ).alias('d') | 32 | ).alias('d') |
33 | .command('admin', | 33 | .command('admin', |
34 | 'manage an instance where you have elevated rights', | 34 | 'manage an instance where you have elevated rights', |
35 | { noHelp: true } as program.CommandOptions | 35 | { noHelp: true } as CommandOptions |
36 | ).alias('a') | 36 | ).alias('a') |
37 | 37 | ||
38 | // help on no command | 38 | // help on no command |
diff --git a/server/tools/test.ts b/server/tools/test.ts index fc7f8d769..fbdbae0b0 100644 --- a/server/tools/test.ts +++ b/server/tools/test.ts | |||
@@ -2,7 +2,7 @@ import { registerTSPaths } from '../helpers/register-ts-paths' | |||
2 | registerTSPaths() | 2 | registerTSPaths() |
3 | 3 | ||
4 | import { LiveVideo, LiveVideoCreate, VideoPrivacy } from '@shared/models' | 4 | import { LiveVideo, LiveVideoCreate, VideoPrivacy } from '@shared/models' |
5 | import * as program from 'commander' | 5 | import { program } from 'commander' |
6 | import { | 6 | import { |
7 | createLive, | 7 | createLive, |
8 | flushAndRunServer, | 8 | flushAndRunServer, |
diff --git a/server/tools/yarn.lock b/server/tools/yarn.lock index 065e32e3d..dceacb223 100644 --- a/server/tools/yarn.lock +++ b/server/tools/yarn.lock | |||
@@ -3,23 +3,23 @@ | |||
3 | 3 | ||
4 | 4 | ||
5 | "@babel/code-frame@^7.0.0": | 5 | "@babel/code-frame@^7.0.0": |
6 | version "7.12.13" | 6 | version "7.14.5" |
7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" | 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" |
8 | integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== | 8 | integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== |
9 | dependencies: | 9 | dependencies: |
10 | "@babel/highlight" "^7.12.13" | 10 | "@babel/highlight" "^7.14.5" |
11 | 11 | ||
12 | "@babel/helper-validator-identifier@^7.12.11": | 12 | "@babel/helper-validator-identifier@^7.14.5": |
13 | version "7.12.11" | 13 | version "7.14.5" |
14 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" | 14 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" |
15 | integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== | 15 | integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== |
16 | 16 | ||
17 | "@babel/highlight@^7.12.13": | 17 | "@babel/highlight@^7.14.5": |
18 | version "7.13.10" | 18 | version "7.14.5" |
19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" | 19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" |
20 | integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== | 20 | integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== |
21 | dependencies: | 21 | dependencies: |
22 | "@babel/helper-validator-identifier" "^7.12.11" | 22 | "@babel/helper-validator-identifier" "^7.14.5" |
23 | chalk "^2.0.0" | 23 | chalk "^2.0.0" |
24 | js-tokens "^4.0.0" | 24 | js-tokens "^4.0.0" |
25 | 25 | ||
@@ -81,10 +81,10 @@ | |||
81 | resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" | 81 | resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" |
82 | integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== | 82 | integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== |
83 | 83 | ||
84 | "@types/node@^13.7.0": | 84 | "@types/node@>=13.7.0": |
85 | version "13.13.48" | 85 | version "15.12.2" |
86 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.48.tgz#46a3df718aed5217277f2395a682e055a487e341" | 86 | resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d" |
87 | integrity sha512-z8wvSsgWQzkr4sVuMEEOvwMdOQjiRY2Y/ZW4fDfjfe3+TfQrZqFKOthBgk2RnVEmtOKrkwdZ7uTvsxTBLjKGDQ== | 87 | integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww== |
88 | 88 | ||
89 | abbrev@1: | 89 | abbrev@1: |
90 | version "1.1.1" | 90 | version "1.1.1" |
@@ -170,7 +170,7 @@ bencode@^2.0.0, bencode@^2.0.1: | |||
170 | dependencies: | 170 | dependencies: |
171 | safe-buffer "^5.1.1" | 171 | safe-buffer "^5.1.1" |
172 | 172 | ||
173 | bep53-range@^1.0.0: | 173 | bep53-range@^1.1.0: |
174 | version "1.1.0" | 174 | version "1.1.0" |
175 | resolved "https://registry.yarnpkg.com/bep53-range/-/bep53-range-1.1.0.tgz#a009311710c955d27eb3a30cf329e8c139693d27" | 175 | resolved "https://registry.yarnpkg.com/bep53-range/-/bep53-range-1.1.0.tgz#a009311710c955d27eb3a30cf329e8c139693d27" |
176 | integrity sha512-yGQTG4NtwTciX0Bkgk1FqQL4p+NiCQKpTSFho2lrxvUkXIlzyJDwraj8aYxAxRZMnnOhRr7QlIBoMRPEnIR34Q== | 176 | integrity sha512-yGQTG4NtwTciX0Bkgk1FqQL4p+NiCQKpTSFho2lrxvUkXIlzyJDwraj8aYxAxRZMnnOhRr7QlIBoMRPEnIR34Q== |
@@ -208,55 +208,58 @@ bittorrent-lsd@^1.0.0: | |||
208 | chrome-dgram "^3.0.6" | 208 | chrome-dgram "^3.0.6" |
209 | debug "^4.2.0" | 209 | debug "^4.2.0" |
210 | 210 | ||
211 | bittorrent-peerid@^1.3.2: | 211 | bittorrent-peerid@^1.3.3: |
212 | version "1.3.3" | 212 | version "1.3.3" |
213 | resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.3.tgz#b8dc79e421f8136d2ffd0b163a18e9d70da09949" | 213 | resolved "https://registry.yarnpkg.com/bittorrent-peerid/-/bittorrent-peerid-1.3.3.tgz#b8dc79e421f8136d2ffd0b163a18e9d70da09949" |
214 | integrity sha512-tSh9HdQgwyEAfo1jzoGEis6o/zs4CcdRTchG93XVl5jct+DCAN90M5MVUV76k2vJ9Xg3GAzLB5NLsY/vnVTh6w== | 214 | integrity sha512-tSh9HdQgwyEAfo1jzoGEis6o/zs4CcdRTchG93XVl5jct+DCAN90M5MVUV76k2vJ9Xg3GAzLB5NLsY/vnVTh6w== |
215 | 215 | ||
216 | bittorrent-protocol@^3.2.0: | 216 | bittorrent-protocol@^3.3.1: |
217 | version "3.3.1" | 217 | version "3.4.1" |
218 | resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.3.1.tgz#b7a8e66babc423c1eb8e379c1cf7ded26a400a73" | 218 | resolved "https://registry.yarnpkg.com/bittorrent-protocol/-/bittorrent-protocol-3.4.1.tgz#b481d09dbf910fa7fcca5f06a7c1c4246151d4d1" |
219 | integrity sha512-DJy0/jjqJD62PPJY79duCccmPMihp3KPowlmd7BLEU8FTtnDsYjso6BAx+pWwCKOeDORdc9RiJ7L72x3taCh6g== | 219 | integrity sha512-3qBW4ZZrUZKN7HzHbX4+kbiphrTNeraMp3i9n3wobicysjibAV8SBDY+sGiBN4SgXV6WvEW4kyRPIjoSqW+khw== |
220 | dependencies: | 220 | dependencies: |
221 | bencode "^2.0.1" | 221 | bencode "^2.0.1" |
222 | bitfield "^4.0.0" | 222 | bitfield "^4.0.0" |
223 | buffer-xor "^2.0.2" | ||
223 | debug "^4.3.1" | 224 | debug "^4.3.1" |
224 | randombytes "^2.1.0" | 225 | randombytes "^2.1.0" |
226 | rc4 "^0.1.5" | ||
225 | readable-stream "^3.6.0" | 227 | readable-stream "^3.6.0" |
228 | simple-sha1 "^3.0.0" | ||
226 | speedometer "^1.1.0" | 229 | speedometer "^1.1.0" |
227 | unordered-array-remove "^1.0.2" | 230 | unordered-array-remove "^1.0.2" |
228 | 231 | ||
229 | bittorrent-tracker@^9.0.0: | 232 | bittorrent-tracker@^9.0.0: |
230 | version "9.17.0" | 233 | version "9.17.2" |
231 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.17.0.tgz#8b4b6f6a49efa9023267c3ca22e1a5f63216fc1f" | 234 | resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.17.2.tgz#1afb02d3d2fb474c13389c45e8a2b6919bff40bd" |
232 | integrity sha512-ErpOx8AAUW8eLwxnEHp15vs0LDJECLADHISEBM+HXclG3J2/9kMBJ31IjwlB8kUNigknSwm8odAThjJEeyL1yA== | 235 | integrity sha512-hXjed0OnB16da+ScJUZnrAZbf9gMgSLKqh5rJebtYnTRgN4o1mX0DOPH3Nf5RFCs935ibhSmZN5nwbkh+3MdEA== |
233 | dependencies: | 236 | dependencies: |
234 | bencode "^2.0.1" | 237 | bencode "^2.0.1" |
235 | bittorrent-peerid "^1.3.2" | 238 | bittorrent-peerid "^1.3.3" |
236 | bn.js "^5.1.1" | 239 | bn.js "^5.2.0" |
237 | chrome-dgram "^3.0.4" | 240 | chrome-dgram "^3.0.6" |
238 | compact2string "^1.4.1" | 241 | compact2string "^1.4.1" |
239 | debug "^4.1.1" | 242 | debug "^4.1.1" |
240 | ip "^1.1.5" | 243 | ip "^1.1.5" |
241 | lru "^3.1.0" | 244 | lru "^3.1.0" |
242 | minimist "^1.2.5" | 245 | minimist "^1.2.5" |
243 | once "^1.4.0" | 246 | once "^1.4.0" |
244 | queue-microtask "^1.2.2" | 247 | queue-microtask "^1.2.3" |
245 | random-iterate "^1.0.1" | 248 | random-iterate "^1.0.1" |
246 | randombytes "^2.1.0" | 249 | randombytes "^2.1.0" |
247 | run-parallel "^1.1.9" | 250 | run-parallel "^1.2.0" |
248 | run-series "^1.1.8" | 251 | run-series "^1.1.9" |
249 | simple-get "^4.0.0" | 252 | simple-get "^4.0.0" |
250 | simple-peer "^9.7.1" | 253 | simple-peer "^9.11.0" |
251 | simple-websocket "^9.0.0" | 254 | simple-websocket "^9.1.0" |
252 | string2compact "^1.3.0" | 255 | string2compact "^1.3.0" |
253 | unordered-array-remove "^1.0.2" | 256 | unordered-array-remove "^1.0.2" |
254 | ws "^7.3.0" | 257 | ws "^7.4.5" |
255 | optionalDependencies: | 258 | optionalDependencies: |
256 | bufferutil "^4.0.1" | 259 | bufferutil "^4.0.3" |
257 | utf-8-validate "^5.0.2" | 260 | utf-8-validate "^5.0.5" |
258 | 261 | ||
259 | blob-to-buffer@^1.2.6, blob-to-buffer@^1.2.9: | 262 | blob-to-buffer@^1.2.9: |
260 | version "1.2.9" | 263 | version "1.2.9" |
261 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" | 264 | resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" |
262 | integrity sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA== | 265 | integrity sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA== |
@@ -268,7 +271,7 @@ block-stream2@^2.0.0, block-stream2@^2.1.0: | |||
268 | dependencies: | 271 | dependencies: |
269 | readable-stream "^3.4.0" | 272 | readable-stream "^3.4.0" |
270 | 273 | ||
271 | bn.js@^5.1.1: | 274 | bn.js@^5.2.0: |
272 | version "5.2.0" | 275 | version "5.2.0" |
273 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" | 276 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" |
274 | integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== | 277 | integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== |
@@ -314,6 +317,13 @@ buffer-indexof@^1.0.0: | |||
314 | resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" | 317 | resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" |
315 | integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== | 318 | integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== |
316 | 319 | ||
320 | buffer-xor@^2.0.2: | ||
321 | version "2.0.2" | ||
322 | resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-2.0.2.tgz#34f7c64f04c777a1f8aac5e661273bb9dd320289" | ||
323 | integrity sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ== | ||
324 | dependencies: | ||
325 | safe-buffer "^5.1.1" | ||
326 | |||
317 | buffer@^6.0.3: | 327 | buffer@^6.0.3: |
318 | version "6.0.3" | 328 | version "6.0.3" |
319 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" | 329 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" |
@@ -322,7 +332,7 @@ buffer@^6.0.3: | |||
322 | base64-js "^1.3.1" | 332 | base64-js "^1.3.1" |
323 | ieee754 "^1.2.1" | 333 | ieee754 "^1.2.1" |
324 | 334 | ||
325 | bufferutil@^4.0.1: | 335 | bufferutil@^4.0.3: |
326 | version "4.0.3" | 336 | version "4.0.3" |
327 | resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.3.tgz#66724b756bed23cd7c28c4d306d7994f9943cc6b" | 337 | resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.3.tgz#66724b756bed23cd7c28c4d306d7994f9943cc6b" |
328 | integrity sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw== | 338 | integrity sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw== |
@@ -364,7 +374,7 @@ chownr@^1.1.1: | |||
364 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" | 374 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" |
365 | integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== | 375 | integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== |
366 | 376 | ||
367 | chrome-dgram@^3.0.2, chrome-dgram@^3.0.4, chrome-dgram@^3.0.6: | 377 | chrome-dgram@^3.0.2, chrome-dgram@^3.0.6: |
368 | version "3.0.6" | 378 | version "3.0.6" |
369 | resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.6.tgz#2288b5c7471f66f073691206d36319dda713cf55" | 379 | resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.6.tgz#2288b5c7471f66f073691206d36319dda713cf55" |
370 | integrity sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA== | 380 | integrity sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA== |
@@ -387,22 +397,22 @@ chrome-net@^3.3.2, chrome-net@^3.3.3, chrome-net@^3.3.4: | |||
387 | inherits "^2.0.1" | 397 | inherits "^2.0.1" |
388 | 398 | ||
389 | chromecasts@^1.9.1: | 399 | chromecasts@^1.9.1: |
390 | version "1.9.1" | 400 | version "1.10.0" |
391 | resolved "https://registry.yarnpkg.com/chromecasts/-/chromecasts-1.9.1.tgz#67b162e8414d57d6106c49fe4a0e9b08f20bbd12" | 401 | resolved "https://registry.yarnpkg.com/chromecasts/-/chromecasts-1.10.0.tgz#7016c9f31b99e40636b21a75976c9364e3fbabbb" |
392 | integrity sha512-nsXv7ufgrpC8s5DUm6FJEa2XJ2VvE9FmbTVi6r4zGreTFTTSRSJjvqVEqLUFX/fGo/zbSre3zdoV+Pu9DGLz0A== | 402 | integrity sha512-vrOiuHxqLb0bWRBlvyL18cHU8PcbZ7iJvwDB6aHdbtdIDVWuzWWZwDyAWHu54j4JNqyaAyYBJiJ6bbHInVcqBQ== |
393 | dependencies: | 403 | dependencies: |
394 | castv2-client "^1.1.0" | 404 | castv2-client "^1.1.0" |
395 | debug "^2.1.3" | 405 | debug "^2.1.3" |
396 | dns-txt "^2.0.2" | 406 | dns-txt "^2.0.2" |
397 | mime "^1.3.4" | 407 | mime "^1.3.4" |
398 | multicast-dns "^6.0.1" | 408 | multicast-dns "^7.2.2" |
399 | simple-get "^2.0.0" | 409 | simple-get "^2.0.0" |
400 | thunky "^0.1.0" | 410 | thunky "^0.1.0" |
401 | xml2js "^0.4.8" | 411 | xml2js "^0.4.8" |
402 | optionalDependencies: | 412 | optionalDependencies: |
403 | node-ssdp "^2.2.0" | 413 | node-ssdp "^2.2.0" |
404 | 414 | ||
405 | chunk-store-stream@^4.2.0: | 415 | chunk-store-stream@^4.3.0: |
406 | version "4.3.0" | 416 | version "4.3.0" |
407 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e" | 417 | resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e" |
408 | integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw== | 418 | integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw== |
@@ -452,7 +462,7 @@ common-tags@^1.8.0: | |||
452 | resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" | 462 | resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" |
453 | integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== | 463 | integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== |
454 | 464 | ||
455 | compact2string@^1.2.0, compact2string@^1.4.1: | 465 | compact2string@^1.4.1: |
456 | version "1.4.1" | 466 | version "1.4.1" |
457 | resolved "https://registry.yarnpkg.com/compact2string/-/compact2string-1.4.1.tgz#8d34929055f8300a13cfc030ad1832e2e53c2e25" | 467 | resolved "https://registry.yarnpkg.com/compact2string/-/compact2string-1.4.1.tgz#8d34929055f8300a13cfc030ad1832e2e53c2e25" |
458 | integrity sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og== | 468 | integrity sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og== |
@@ -489,7 +499,7 @@ cpus@^1.0.3: | |||
489 | resolved "https://registry.yarnpkg.com/cpus/-/cpus-1.0.3.tgz#4ef6deea461968d6329d07dd01205685df2934a2" | 499 | resolved "https://registry.yarnpkg.com/cpus/-/cpus-1.0.3.tgz#4ef6deea461968d6329d07dd01205685df2934a2" |
490 | integrity sha512-PXHBvGLuL69u55IkLa5e5838fLhIMHxmkV4ge42a8alGyn7BtawYgI0hQ849EedvtHIOLNNH3i6eQU1BiE9SUA== | 500 | integrity sha512-PXHBvGLuL69u55IkLa5e5838fLhIMHxmkV4ge42a8alGyn7BtawYgI0hQ849EedvtHIOLNNH3i6eQU1BiE9SUA== |
491 | 501 | ||
492 | create-torrent@^4.4.2, create-torrent@^4.4.4: | 502 | create-torrent@^4.4.2, create-torrent@^4.7.0: |
493 | version "4.7.0" | 503 | version "4.7.0" |
494 | resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-4.7.0.tgz#ba5d52d41e7621d0d61c895c8026d3fb22aa4333" | 504 | resolved "https://registry.yarnpkg.com/create-torrent/-/create-torrent-4.7.0.tgz#ba5d52d41e7621d0d61c895c8026d3fb22aa4333" |
495 | integrity sha512-Pb3XjZNKdCs0Nk46yFKb82y+a3xRQeMvGi1AlJfIV40y/iwkgBqzS5EfqdnakEOvh2jzTOx3v8QxZpkz4hPzyw== | 505 | integrity sha512-Pb3XjZNKdCs0Nk46yFKb82y+a3xRQeMvGi1AlJfIV40y/iwkgBqzS5EfqdnakEOvh2jzTOx3v8QxZpkz4hPzyw== |
@@ -547,13 +557,6 @@ decompress-response@^3.3.0: | |||
547 | dependencies: | 557 | dependencies: |
548 | mimic-response "^1.0.0" | 558 | mimic-response "^1.0.0" |
549 | 559 | ||
550 | decompress-response@^4.2.0: | ||
551 | version "4.2.1" | ||
552 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" | ||
553 | integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== | ||
554 | dependencies: | ||
555 | mimic-response "^2.0.0" | ||
556 | |||
557 | decompress-response@^6.0.0: | 560 | decompress-response@^6.0.0: |
558 | version "6.0.0" | 561 | version "6.0.0" |
559 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" | 562 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" |
@@ -566,15 +569,20 @@ deep-extend@^0.6.0: | |||
566 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" | 569 | resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" |
567 | integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== | 570 | integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== |
568 | 571 | ||
572 | define-lazy-prop@^2.0.0: | ||
573 | version "2.0.0" | ||
574 | resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" | ||
575 | integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== | ||
576 | |||
569 | delegates@^1.0.0: | 577 | delegates@^1.0.0: |
570 | version "1.0.0" | 578 | version "1.0.0" |
571 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" | 579 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" |
572 | integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= | 580 | integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= |
573 | 581 | ||
574 | detect-indent@^6.0.0: | 582 | detect-indent@^6.0.0: |
575 | version "6.0.0" | 583 | version "6.1.0" |
576 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" | 584 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" |
577 | integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== | 585 | integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== |
578 | 586 | ||
579 | detect-libc@^1.0.2: | 587 | detect-libc@^1.0.2: |
580 | version "1.0.3" | 588 | version "1.0.3" |
@@ -595,13 +603,12 @@ dlnacasts@^0.1.0: | |||
595 | upnp-mediarenderer-client "^1.2.2" | 603 | upnp-mediarenderer-client "^1.2.2" |
596 | xml2js "^0.4.8" | 604 | xml2js "^0.4.8" |
597 | 605 | ||
598 | dns-packet@^1.3.1: | 606 | dns-packet@^5.2.2: |
599 | version "1.3.1" | 607 | version "5.2.4" |
600 | resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" | 608 | resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.2.4.tgz#e004f409eadfa8ec861964dcb9eb395884fcf67d" |
601 | integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== | 609 | integrity sha512-vgu5Bx5IV8mXmh/9cn1lzn+J7okFlXe1vBRp+kCBJXg1nBED6Z/Q4e+QaDxQRSozMr14p/VQmdXwsf/I2wGjUA== |
602 | dependencies: | 610 | dependencies: |
603 | ip "^1.1.0" | 611 | ip "^1.1.5" |
604 | safe-buffer "^5.0.1" | ||
605 | 612 | ||
606 | dns-txt@^2.0.2: | 613 | dns-txt@^2.0.2: |
607 | version "2.0.2" | 614 | version "2.0.2" |
@@ -708,7 +715,7 @@ freelist@^1.0.3: | |||
708 | resolved "https://registry.yarnpkg.com/freelist/-/freelist-1.0.3.tgz#006775509f3935701784d3ed2fc9f12c9df1bab2" | 715 | resolved "https://registry.yarnpkg.com/freelist/-/freelist-1.0.3.tgz#006775509f3935701784d3ed2fc9f12c9df1bab2" |
709 | integrity sha1-AGd1UJ85NXAXhNPtL8nxLJ3xurI= | 716 | integrity sha1-AGd1UJ85NXAXhNPtL8nxLJ3xurI= |
710 | 717 | ||
711 | fs-chunk-store@^2.0.2: | 718 | fs-chunk-store@^2.0.3: |
712 | version "2.0.3" | 719 | version "2.0.3" |
713 | resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-2.0.3.tgz#21e51f1833a84a07cb5e911d058dae084030375a" | 720 | resolved "https://registry.yarnpkg.com/fs-chunk-store/-/fs-chunk-store-2.0.3.tgz#21e51f1833a84a07cb5e911d058dae084030375a" |
714 | integrity sha512-qQi93nHX3880gtoQPt1hKQcuYBNVfCbMk8OVRDqR0cJ0riheELW25ry9yl7pII8E9gOAONTGKBD5N/zGHFSVQg== | 721 | integrity sha512-qQi93nHX3880gtoQPt1hKQcuYBNVfCbMk8OVRDqR0cJ0riheELW25ry9yl7pII8E9gOAONTGKBD5N/zGHFSVQg== |
@@ -751,11 +758,6 @@ get-browser-rtc@^1.1.0: | |||
751 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz#d1494e299b00f33fc8e9d6d3343ba4ba99711a2c" | 758 | resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz#d1494e299b00f33fc8e9d6d3343ba4ba99711a2c" |
752 | integrity sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ== | 759 | integrity sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ== |
753 | 760 | ||
754 | get-stdin@^7.0.0: | ||
755 | version "7.0.0" | ||
756 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" | ||
757 | integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== | ||
758 | |||
759 | get-stdin@^8.0.0: | 761 | get-stdin@^8.0.0: |
760 | version "8.0.0" | 762 | version "8.0.0" |
761 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" | 763 | resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" |
@@ -767,9 +769,9 @@ get-stream@^3.0.0: | |||
767 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= | 769 | integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= |
768 | 770 | ||
769 | glob@^7.1.3: | 771 | glob@^7.1.3: |
770 | version "7.1.6" | 772 | version "7.1.7" |
771 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" | 773 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" |
772 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== | 774 | integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== |
773 | dependencies: | 775 | dependencies: |
774 | fs.realpath "^1.0.0" | 776 | fs.realpath "^1.0.0" |
775 | inflight "^1.0.4" | 777 | inflight "^1.0.4" |
@@ -824,18 +826,18 @@ ieee754@^1.2.1: | |||
824 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== | 826 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== |
825 | 827 | ||
826 | ignore-walk@^3.0.1: | 828 | ignore-walk@^3.0.1: |
827 | version "3.0.3" | 829 | version "3.0.4" |
828 | resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" | 830 | resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" |
829 | integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== | 831 | integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== |
830 | dependencies: | 832 | dependencies: |
831 | minimatch "^3.0.4" | 833 | minimatch "^3.0.4" |
832 | 834 | ||
833 | immediate-chunk-store@^2.1.1: | 835 | immediate-chunk-store@^2.2.0: |
834 | version "2.1.1" | 836 | version "2.2.0" |
835 | resolved "https://registry.yarnpkg.com/immediate-chunk-store/-/immediate-chunk-store-2.1.1.tgz#4b9f001beaab38d62e4aae630ec7ffb98be805ce" | 837 | resolved "https://registry.yarnpkg.com/immediate-chunk-store/-/immediate-chunk-store-2.2.0.tgz#f56d30ecc7171f6cfcf632b0eb8395a89f92c03c" |
836 | integrity sha512-y5AxkxqpPTj2dkaAEkDnrMuSX4JNicXHD6yTpLfFnflVejL6yJpzf27obrnlf2PSSQiWUf3735Y9tJEjxvqnoA== | 838 | integrity sha512-1bHBna0hCa6arRXicu91IiL9RvvkbNYLVq+mzWdaLGZC3hXvX4doh8e1dLhMKez5siu63CYgO5NrGJbRX5lbPA== |
837 | dependencies: | 839 | dependencies: |
838 | queue-microtask "^1.2.0" | 840 | queue-microtask "^1.2.3" |
839 | 841 | ||
840 | imurmurhash@^0.1.4: | 842 | imurmurhash@^0.1.4: |
841 | version "0.1.4" | 843 | version "0.1.4" |
@@ -867,15 +869,15 @@ ip-set@^2.1.0: | |||
867 | dependencies: | 869 | dependencies: |
868 | ip "^1.1.5" | 870 | ip "^1.1.5" |
869 | 871 | ||
870 | ip@^1.0.1, ip@^1.1.0, ip@^1.1.5: | 872 | ip@^1.0.1, ip@^1.1.5: |
871 | version "1.1.5" | 873 | version "1.1.5" |
872 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" | 874 | resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" |
873 | integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= | 875 | integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= |
874 | 876 | ||
875 | "ipaddr.js@>= 0.1.5": | 877 | "ipaddr.js@>= 0.1.5": |
876 | version "2.0.0" | 878 | version "2.0.1" |
877 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.0.tgz#77ccccc8063ae71ab65c55f21b090698e763fc6e" | 879 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" |
878 | integrity sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w== | 880 | integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== |
879 | 881 | ||
880 | ipaddr.js@^1.0.1: | 882 | ipaddr.js@^1.0.1: |
881 | version "1.9.1" | 883 | version "1.9.1" |
@@ -892,7 +894,7 @@ is-ascii@^1.0.0: | |||
892 | resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" | 894 | resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" |
893 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= | 895 | integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= |
894 | 896 | ||
895 | is-docker@^2.0.0: | 897 | is-docker@^2.0.0, is-docker@^2.1.1: |
896 | version "2.2.1" | 898 | version "2.2.1" |
897 | resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" | 899 | resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" |
898 | integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== | 900 | integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== |
@@ -934,7 +936,7 @@ is-typedarray@^1.0.0: | |||
934 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" | 936 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" |
935 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= | 937 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= |
936 | 938 | ||
937 | is-wsl@^2.1.1: | 939 | is-wsl@^2.2.0: |
938 | version "2.2.0" | 940 | version "2.2.0" |
939 | resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" | 941 | resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" |
940 | integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== | 942 | integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== |
@@ -1002,7 +1004,7 @@ lines-and-columns@^1.1.6: | |||
1002 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" | 1004 | resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" |
1003 | integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= | 1005 | integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= |
1004 | 1006 | ||
1005 | load-ip-set@^2.1.2: | 1007 | load-ip-set@^2.2.1: |
1006 | version "2.2.1" | 1008 | version "2.2.1" |
1007 | resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.2.1.tgz#9496ab8aa14ebf81aeb7c8bb38e7abdf50af3563" | 1009 | resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.2.1.tgz#9496ab8aa14ebf81aeb7c8bb38e7abdf50af3563" |
1008 | integrity sha512-G3hQXehU2LTOp52e+lPffpK4EvidfjwbvHaGqmFcp4ptiZagR4xFdL+D08kMX906dxeqZyWhfonEjdUxrWcldg== | 1010 | integrity sha512-G3hQXehU2LTOp52e+lPffpK4EvidfjwbvHaGqmFcp4ptiZagR4xFdL+D08kMX906dxeqZyWhfonEjdUxrWcldg== |
@@ -1035,20 +1037,20 @@ lru@^3.1.0: | |||
1035 | dependencies: | 1037 | dependencies: |
1036 | inherits "^2.0.1" | 1038 | inherits "^2.0.1" |
1037 | 1039 | ||
1038 | magnet-uri@^5.1.3: | 1040 | lt_donthave@^1.0.1: |
1039 | version "5.4.0" | 1041 | version "1.0.1" |
1040 | resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-5.4.0.tgz#5c64d3b8853eafb2f31840df09fbfc90c00f0e1d" | 1042 | resolved "https://registry.yarnpkg.com/lt_donthave/-/lt_donthave-1.0.1.tgz#a160e08bdf15b9e092172063688855a6c031d8b3" |
1041 | integrity sha512-ZpqciThlbvE6KkyT5oxAup/6CwjePw1hdtR8NU5+vq2hn9Sp5b7w3bRiJRvo9fMHUj2dWSuVCdkqt9p4ed1V9Q== | 1043 | integrity sha512-PfOXfDN9GnUjlNHjjxKQuMxPC8s12iSrnmg+Ff1BU1uLn7S1BFAKzpZCu6Gwg3WsCUvTZrZoDSHvy6B/j+N4/Q== |
1042 | dependencies: | 1044 | dependencies: |
1043 | bep53-range "^1.0.0" | 1045 | debug "^4.2.0" |
1044 | thirty-two "^1.0.2" | 1046 | unordered-array-remove "^1.0.2" |
1045 | 1047 | ||
1046 | magnet-uri@^6.0.0: | 1048 | magnet-uri@^6.0.0: |
1047 | version "6.1.0" | 1049 | version "6.2.0" |
1048 | resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-6.1.0.tgz#fe73026ba1ee77c955097a4979d1003f4fb7ecf7" | 1050 | resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-6.2.0.tgz#10f7be050bf23452df210838239b118463c3eeff" |
1049 | integrity sha512-731qLviHaqN/Ni96wm6gNKuvoip+QHWTznjHNz/4qDlsHh3/CWJoL8fZ18IIRhGJgnWoKJp8RVE5lZvQ60Khhw== | 1051 | integrity sha512-O9AgdDwT771fnUj0giPYu/rACpz8173y8UXCSOdLITjOVfBenZ9H9q3FqQmveK+ORUMuD+BkKNSZP8C3+IMAKQ== |
1050 | dependencies: | 1052 | dependencies: |
1051 | bep53-range "^1.0.0" | 1053 | bep53-range "^1.1.0" |
1052 | thirty-two "^1.0.2" | 1054 | thirty-two "^1.0.2" |
1053 | 1055 | ||
1054 | make-dir@^3.0.0: | 1056 | make-dir@^3.0.0: |
@@ -1084,19 +1086,19 @@ mediasource@^2.2.2, mediasource@^2.4.0: | |||
1084 | readable-stream "^3.6.0" | 1086 | readable-stream "^3.6.0" |
1085 | to-arraybuffer "^1.0.1" | 1087 | to-arraybuffer "^1.0.1" |
1086 | 1088 | ||
1087 | memory-chunk-store@^1.3.1: | 1089 | memory-chunk-store@^1.3.5: |
1088 | version "1.3.2" | 1090 | version "1.3.5" |
1089 | resolved "https://registry.yarnpkg.com/memory-chunk-store/-/memory-chunk-store-1.3.2.tgz#3bde573c957c0260d8116e6e2c0ce62ff2032894" | 1091 | resolved "https://registry.yarnpkg.com/memory-chunk-store/-/memory-chunk-store-1.3.5.tgz#700f712415895600bc5466007333efa19f1de07c" |
1090 | integrity sha512-EBcbwpdQlzT5aNV0FTT+RAfh1cGEssjiCcRGcTk57mKsnZlRMOtH4Cfk/AqQnkz8xP2dUF+/lgpmErSGwwE1FA== | 1092 | integrity sha512-E1Xc1U4ifk/FkC2ZsWhCaW1xg9HbE/OBmQTLe2Tr9c27YPSLbW7kw1cnb3kQWD1rDtErFJHa7mB9EVrs7aTx9g== |
1091 | dependencies: | 1093 | dependencies: |
1092 | queue-microtask "^1.2.2" | 1094 | queue-microtask "^1.2.3" |
1093 | 1095 | ||
1094 | mime@^1.3.4: | 1096 | mime@^1.3.4: |
1095 | version "1.6.0" | 1097 | version "1.6.0" |
1096 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" | 1098 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" |
1097 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== | 1099 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== |
1098 | 1100 | ||
1099 | mime@^2.4.1, mime@^2.4.6, mime@^2.5.0: | 1101 | mime@^2.4.1, mime@^2.4.6, mime@^2.5.2: |
1100 | version "2.5.2" | 1102 | version "2.5.2" |
1101 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" | 1103 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" |
1102 | integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== | 1104 | integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== |
@@ -1106,11 +1108,6 @@ mimic-response@^1.0.0: | |||
1106 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" | 1108 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" |
1107 | integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== | 1109 | integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== |
1108 | 1110 | ||
1109 | mimic-response@^2.0.0: | ||
1110 | version "2.1.0" | ||
1111 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" | ||
1112 | integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== | ||
1113 | |||
1114 | mimic-response@^3.1.0: | 1111 | mimic-response@^3.1.0: |
1115 | version "3.1.0" | 1112 | version "3.1.0" |
1116 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" | 1113 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" |
@@ -1192,12 +1189,12 @@ ms@^2.1.1: | |||
1192 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" | 1189 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" |
1193 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== | 1190 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== |
1194 | 1191 | ||
1195 | multicast-dns@^6.0.1: | 1192 | multicast-dns@^7.2.2: |
1196 | version "6.2.3" | 1193 | version "7.2.3" |
1197 | resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" | 1194 | resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.3.tgz#cbd07571dda41807b36f71067681f19e85ccc2cd" |
1198 | integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== | 1195 | integrity sha512-TzxgGSLRLB7tqAlzjgd2x2ZE0cDsGFq4rs9W4yE5xp+7hlRXeUQGtXZsTGfGw2FwWB45rfe8DtXMYBpZGMLUng== |
1199 | dependencies: | 1196 | dependencies: |
1200 | dns-packet "^1.3.1" | 1197 | dns-packet "^5.2.2" |
1201 | thunky "^1.0.2" | 1198 | thunky "^1.0.2" |
1202 | 1199 | ||
1203 | multistream@^4.0.1, multistream@^4.1.0: | 1200 | multistream@^4.0.1, multistream@^4.1.0: |
@@ -1295,9 +1292,9 @@ nopt@^4.0.1: | |||
1295 | osenv "^0.1.4" | 1292 | osenv "^0.1.4" |
1296 | 1293 | ||
1297 | npm-bundled@^1.0.1: | 1294 | npm-bundled@^1.0.1: |
1298 | version "1.1.1" | 1295 | version "1.1.2" |
1299 | resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" | 1296 | resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" |
1300 | integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== | 1297 | integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== |
1301 | dependencies: | 1298 | dependencies: |
1302 | npm-normalize-package-bin "^1.0.1" | 1299 | npm-normalize-package-bin "^1.0.1" |
1303 | 1300 | ||
@@ -1356,13 +1353,14 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: | |||
1356 | dependencies: | 1353 | dependencies: |
1357 | wrappy "1" | 1354 | wrappy "1" |
1358 | 1355 | ||
1359 | open@^7.1.0: | 1356 | open@^8.0.0: |
1360 | version "7.4.2" | 1357 | version "8.2.0" |
1361 | resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" | 1358 | resolved "https://registry.yarnpkg.com/open/-/open-8.2.0.tgz#d6a4788b00009a9d60df471ecb89842a15fdcfc1" |
1362 | integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== | 1359 | integrity sha512-O8uInONB4asyY3qUcEytpgwxQG3O0fJ/hlssoUHsBboOIRVZzT6Wq+Rwj5nffbeUhOdMjpXeISpDDzHCMRDuOQ== |
1363 | dependencies: | 1360 | dependencies: |
1364 | is-docker "^2.0.0" | 1361 | define-lazy-prop "^2.0.0" |
1365 | is-wsl "^2.1.1" | 1362 | is-docker "^2.1.1" |
1363 | is-wsl "^2.2.0" | ||
1366 | 1364 | ||
1367 | os-homedir@^1.0.0: | 1365 | os-homedir@^1.0.0: |
1368 | version "1.0.2" | 1366 | version "1.0.2" |
@@ -1404,19 +1402,7 @@ parse-json@^5.0.0: | |||
1404 | json-parse-even-better-errors "^2.3.0" | 1402 | json-parse-even-better-errors "^2.3.0" |
1405 | lines-and-columns "^1.1.6" | 1403 | lines-and-columns "^1.1.6" |
1406 | 1404 | ||
1407 | parse-torrent@^7.1.3: | 1405 | parse-torrent@^9.0.0, parse-torrent@^9.1.3: |
1408 | version "7.1.3" | ||
1409 | resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-7.1.3.tgz#5981a0d9381b16297a7af053763068e8da5c9610" | ||
1410 | integrity sha512-to8zT7+o6bVTyP35r2QgG1svuFGMFO1mE6ri1oWHgL9tlgnOtfjmfHmA3mOuP9HTIU/8OiZw0NG5zbyqVLxhvA== | ||
1411 | dependencies: | ||
1412 | bencode "^2.0.0" | ||
1413 | blob-to-buffer "^1.2.6" | ||
1414 | get-stdin "^7.0.0" | ||
1415 | magnet-uri "^5.1.3" | ||
1416 | simple-get "^3.0.1" | ||
1417 | simple-sha1 "^3.0.0" | ||
1418 | |||
1419 | parse-torrent@^9.1.1: | ||
1420 | version "9.1.3" | 1406 | version "9.1.3" |
1421 | resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-9.1.3.tgz#9b4bc8dca243b356bf449938d6d38a259a2a707c" | 1407 | resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-9.1.3.tgz#9b4bc8dca243b356bf449938d6d38a259a2a707c" |
1422 | integrity sha512-/Yr951CvJM8S6TjMaqrsmMxeQEAjDeCX+MZ3hGXXc7DG2wqzp/rzOsHtDzIVqN6NsFRCqy6wYLF/W7Sgvq7bXw== | 1408 | integrity sha512-/Yr951CvJM8S6TjMaqrsmMxeQEAjDeCX+MZ3hGXXc7DG2wqzp/rzOsHtDzIVqN6NsFRCqy6wYLF/W7Sgvq7bXw== |
@@ -1468,9 +1454,9 @@ process-nextick-args@~2.0.0: | |||
1468 | integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== | 1454 | integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== |
1469 | 1455 | ||
1470 | protobufjs@^6.8.8: | 1456 | protobufjs@^6.8.8: |
1471 | version "6.10.2" | 1457 | version "6.11.2" |
1472 | resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.10.2.tgz#b9cb6bd8ec8f87514592ba3fdfd28e93f33a469b" | 1458 | resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" |
1473 | integrity sha512-27yj+04uF6ya9l+qfpH187aqEzfCF4+Uit0I9ZBQVqK09hk/SQzKa2MUqUpXaVa7LOFRg1TSSr3lVxGOk6c0SQ== | 1459 | integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== |
1474 | dependencies: | 1460 | dependencies: |
1475 | "@protobufjs/aspromise" "^1.1.2" | 1461 | "@protobufjs/aspromise" "^1.1.2" |
1476 | "@protobufjs/base64" "^1.1.2" | 1462 | "@protobufjs/base64" "^1.1.2" |
@@ -1483,7 +1469,7 @@ protobufjs@^6.8.8: | |||
1483 | "@protobufjs/pool" "^1.1.0" | 1469 | "@protobufjs/pool" "^1.1.0" |
1484 | "@protobufjs/utf8" "^1.1.0" | 1470 | "@protobufjs/utf8" "^1.1.0" |
1485 | "@types/long" "^4.0.1" | 1471 | "@types/long" "^4.0.1" |
1486 | "@types/node" "^13.7.0" | 1472 | "@types/node" ">=13.7.0" |
1487 | long "^4.0.0" | 1473 | long "^4.0.0" |
1488 | 1474 | ||
1489 | pump@^3.0.0: | 1475 | pump@^3.0.0: |
@@ -1499,7 +1485,7 @@ qap@^3.1.2: | |||
1499 | resolved "https://registry.yarnpkg.com/qap/-/qap-3.3.1.tgz#11f9e8fa8890fe7cb99210c0f44d0613b7372cac" | 1485 | resolved "https://registry.yarnpkg.com/qap/-/qap-3.3.1.tgz#11f9e8fa8890fe7cb99210c0f44d0613b7372cac" |
1500 | integrity sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw= | 1486 | integrity sha1-Efno+oiQ/ny5khDA9E0GE7c3LKw= |
1501 | 1487 | ||
1502 | queue-microtask@^1.2.0, queue-microtask@^1.2.2, queue-microtask@^1.2.3: | 1488 | queue-microtask@^1.2.2, queue-microtask@^1.2.3: |
1503 | version "1.2.3" | 1489 | version "1.2.3" |
1504 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" | 1490 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" |
1505 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== | 1491 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== |
@@ -1543,6 +1529,11 @@ range-slice-stream@^2.0.0: | |||
1543 | dependencies: | 1529 | dependencies: |
1544 | readable-stream "^3.0.2" | 1530 | readable-stream "^3.0.2" |
1545 | 1531 | ||
1532 | rc4@^0.1.5: | ||
1533 | version "0.1.5" | ||
1534 | resolved "https://registry.yarnpkg.com/rc4/-/rc4-0.1.5.tgz#08c6e04a0168f6eb621c22ab6cb1151bd9f4a64d" | ||
1535 | integrity sha1-CMbgSgFo9utiHCKrbLEVG9n0pk0= | ||
1536 | |||
1546 | rc@^1.2.7: | 1537 | rc@^1.2.7: |
1547 | version "1.2.8" | 1538 | version "1.2.8" |
1548 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" | 1539 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" |
@@ -1576,9 +1567,9 @@ readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.4.0, readable | |||
1576 | util-deprecate "^1.0.1" | 1567 | util-deprecate "^1.0.1" |
1577 | 1568 | ||
1578 | record-cache@^1.0.2: | 1569 | record-cache@^1.0.2: |
1579 | version "1.1.0" | 1570 | version "1.1.1" |
1580 | resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.1.0.tgz#f8a467a691a469584b26e88d36b18afdb3932037" | 1571 | resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.1.1.tgz#ba3088a489f50491a4af7b14d410822c394fb811" |
1581 | integrity sha512-u8rbtLEJV7HRacl/ZYwSBFD8NFyB3PfTTfGLP37IW3hftQCwu6z4Q2RLyxo1YJUNRTEzJfpLpGwVuEYdaIkG9Q== | 1572 | integrity sha512-L5hZlgWc7CmGbztnemQoKE1bLu9rtI2skOB0ttE4C5+TVszLE8Rd0YLTROSgvXKLAqPumS/soyN5tJW5wJLmJQ== |
1582 | 1573 | ||
1583 | render-media@^4.1.0: | 1574 | render-media@^4.1.0: |
1584 | version "4.1.0" | 1575 | version "4.1.0" |
@@ -1605,31 +1596,31 @@ rimraf@^3.0.0: | |||
1605 | dependencies: | 1596 | dependencies: |
1606 | glob "^7.1.3" | 1597 | glob "^7.1.3" |
1607 | 1598 | ||
1608 | run-parallel-limit@^1.0.6: | 1599 | run-parallel-limit@^1.1.0: |
1609 | version "1.1.0" | 1600 | version "1.1.0" |
1610 | resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" | 1601 | resolved "https://registry.yarnpkg.com/run-parallel-limit/-/run-parallel-limit-1.1.0.tgz#be80e936f5768623a38a963262d6bef8ff11e7ba" |
1611 | integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== | 1602 | integrity sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw== |
1612 | dependencies: | 1603 | dependencies: |
1613 | queue-microtask "^1.2.2" | 1604 | queue-microtask "^1.2.2" |
1614 | 1605 | ||
1615 | run-parallel@^1.1.10, run-parallel@^1.1.2, run-parallel@^1.1.6, run-parallel@^1.1.9: | 1606 | run-parallel@^1.1.10, run-parallel@^1.1.2, run-parallel@^1.1.6, run-parallel@^1.2.0: |
1616 | version "1.2.0" | 1607 | version "1.2.0" |
1617 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" | 1608 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" |
1618 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== | 1609 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== |
1619 | dependencies: | 1610 | dependencies: |
1620 | queue-microtask "^1.2.2" | 1611 | queue-microtask "^1.2.2" |
1621 | 1612 | ||
1622 | run-series@^1.1.8, run-series@^1.1.9: | 1613 | run-series@^1.1.9: |
1623 | version "1.1.9" | 1614 | version "1.1.9" |
1624 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" | 1615 | resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" |
1625 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== | 1616 | integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== |
1626 | 1617 | ||
1627 | rusha@^0.8.13: | 1618 | rusha@^0.8.13: |
1628 | version "0.8.13" | 1619 | version "0.8.14" |
1629 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a" | 1620 | resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.14.tgz#a977d0de9428406138b7bb90d3de5dcd024e2f68" |
1630 | integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo= | 1621 | integrity sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA== |
1631 | 1622 | ||
1632 | safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: | 1623 | safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: |
1633 | version "5.2.1" | 1624 | version "5.2.1" |
1634 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" | 1625 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" |
1635 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== | 1626 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== |
@@ -1705,15 +1696,6 @@ simple-get@^2.0.0, simple-get@^2.1.0: | |||
1705 | once "^1.3.1" | 1696 | once "^1.3.1" |
1706 | simple-concat "^1.0.0" | 1697 | simple-concat "^1.0.0" |
1707 | 1698 | ||
1708 | simple-get@^3.0.1: | ||
1709 | version "3.1.0" | ||
1710 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" | ||
1711 | integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== | ||
1712 | dependencies: | ||
1713 | decompress-response "^4.2.0" | ||
1714 | once "^1.3.1" | ||
1715 | simple-concat "^1.0.0" | ||
1716 | |||
1717 | simple-get@^4.0.0: | 1699 | simple-get@^4.0.0: |
1718 | version "4.0.0" | 1700 | version "4.0.0" |
1719 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675" | 1701 | resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675" |
@@ -1723,7 +1705,7 @@ simple-get@^4.0.0: | |||
1723 | once "^1.3.1" | 1705 | once "^1.3.1" |
1724 | simple-concat "^1.0.0" | 1706 | simple-concat "^1.0.0" |
1725 | 1707 | ||
1726 | simple-peer@^9.7.1, simple-peer@^9.9.3: | 1708 | simple-peer@^9.11.0: |
1727 | version "9.11.0" | 1709 | version "9.11.0" |
1728 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571" | 1710 | resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.11.0.tgz#e8d27609c7a610c3ddd75767da868e8daab67571" |
1729 | integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg== | 1711 | integrity sha512-qvdNu/dGMHBm2uQ7oLhQBMhYlrOZC1ywXNCH/i8I4etxR1vrjCnU6ZSQBptndB1gcakjo2+w4OHo7Sjza1SHxg== |
@@ -1736,7 +1718,7 @@ simple-peer@^9.7.1, simple-peer@^9.9.3: | |||
1736 | randombytes "^2.1.0" | 1718 | randombytes "^2.1.0" |
1737 | readable-stream "^3.6.0" | 1719 | readable-stream "^3.6.0" |
1738 | 1720 | ||
1739 | simple-sha1@^3.0.0, simple-sha1@^3.0.1: | 1721 | simple-sha1@^3.0.0, simple-sha1@^3.0.1, simple-sha1@^3.1.0: |
1740 | version "3.1.0" | 1722 | version "3.1.0" |
1741 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131" | 1723 | resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131" |
1742 | integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg== | 1724 | integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg== |
@@ -1744,7 +1726,7 @@ simple-sha1@^3.0.0, simple-sha1@^3.0.1: | |||
1744 | queue-microtask "^1.2.2" | 1726 | queue-microtask "^1.2.2" |
1745 | rusha "^0.8.13" | 1727 | rusha "^0.8.13" |
1746 | 1728 | ||
1747 | simple-websocket@^9.0.0: | 1729 | simple-websocket@^9.1.0: |
1748 | version "9.1.0" | 1730 | version "9.1.0" |
1749 | resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-9.1.0.tgz#91cbb39eafefbe7e66979da6c639109352786a7f" | 1731 | resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-9.1.0.tgz#91cbb39eafefbe7e66979da6c639109352786a7f" |
1750 | integrity sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ== | 1732 | integrity sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ== |
@@ -1819,7 +1801,7 @@ string-width@^4.2.0: | |||
1819 | is-fullwidth-code-point "^3.0.0" | 1801 | is-fullwidth-code-point "^3.0.0" |
1820 | strip-ansi "^6.0.0" | 1802 | strip-ansi "^6.0.0" |
1821 | 1803 | ||
1822 | string2compact@^1.2.5, string2compact@^1.3.0: | 1804 | string2compact@^1.3.0: |
1823 | version "1.3.0" | 1805 | version "1.3.0" |
1824 | resolved "https://registry.yarnpkg.com/string2compact/-/string2compact-1.3.0.tgz#22d946127b082d1203c51316af60117a337423c3" | 1806 | resolved "https://registry.yarnpkg.com/string2compact/-/string2compact-1.3.0.tgz#22d946127b082d1203c51316af60117a337423c3" |
1825 | integrity sha512-004ulKKANDuQilQsNxy2lisrpMG0qUJxBU+2YCEF7KziRyNR0Nredm2qk0f1V82nva59H3y9GWeHXE63HzGRFw== | 1807 | integrity sha512-004ulKKANDuQilQsNxy2lisrpMG0qUJxBU+2YCEF7KziRyNR0Nredm2qk0f1V82nva59H3y9GWeHXE63HzGRFw== |
@@ -1938,7 +1920,7 @@ torrent-discovery@^9.4.0: | |||
1938 | debug "^4.0.0" | 1920 | debug "^4.0.0" |
1939 | run-parallel "^1.1.2" | 1921 | run-parallel "^1.1.2" |
1940 | 1922 | ||
1941 | torrent-piece@^2.0.0: | 1923 | torrent-piece@^2.0.1: |
1942 | version "2.0.1" | 1924 | version "2.0.1" |
1943 | resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.1.tgz#a1a50fffa589d9bf9560e38837230708bc3afdc6" | 1925 | resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.1.tgz#a1a50fffa589d9bf9560e38837230708bc3afdc6" |
1944 | integrity sha512-JLSOyvQVLI6JTWqioY4vFL0JkEUKQcaHQsU3loxkCvPTSttw8ePs2tFwsP4XIjw99Fz8EdOzt/4faykcbnPbCQ== | 1926 | integrity sha512-JLSOyvQVLI6JTWqioY4vFL0JkEUKQcaHQsU3loxkCvPTSttw8ePs2tFwsP4XIjw99Fz8EdOzt/4faykcbnPbCQ== |
@@ -2011,19 +1993,19 @@ ut_metadata@^3.5.2: | |||
2011 | debug "^4.2.0" | 1993 | debug "^4.2.0" |
2012 | simple-sha1 "^3.0.1" | 1994 | simple-sha1 "^3.0.1" |
2013 | 1995 | ||
2014 | ut_pex@^2.0.1: | 1996 | ut_pex@^3.0.0: |
2015 | version "2.0.1" | 1997 | version "3.0.1" |
2016 | resolved "https://registry.yarnpkg.com/ut_pex/-/ut_pex-2.0.1.tgz#30d3cc19ee32f9513b06ed2b03851ba508566da1" | 1998 | resolved "https://registry.yarnpkg.com/ut_pex/-/ut_pex-3.0.1.tgz#fb8b6e066f8f6f6de3e6b3e28e7d18e697be5854" |
2017 | integrity sha512-kI1/y1IhbuTqjyVqekSZCd3afPQTpdIRCrON1WXc9jGdcIAaze3FAoZ1ssYJmGBuJbdg7LQO42daJGCaoRXl+A== | 1999 | integrity sha512-t1MHIDHSISgOJcmq8UM6Qv9/hRQYVaUvzqSNnXa5ATDbS9hXfhBpyBo2HcSyJtwPSHsmMtNui8G6yKirwJ8vow== |
2018 | dependencies: | 2000 | dependencies: |
2019 | bencode "^2.0.0" | 2001 | bencode "^2.0.1" |
2020 | compact2string "^1.2.0" | 2002 | compact2string "^1.4.1" |
2021 | string2compact "^1.2.5" | 2003 | string2compact "^1.3.0" |
2022 | 2004 | ||
2023 | utf-8-validate@^5.0.2: | 2005 | utf-8-validate@^5.0.5: |
2024 | version "5.0.4" | 2006 | version "5.0.5" |
2025 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.4.tgz#72a1735983ddf7a05a43a9c6b67c5ce1c910f9b8" | 2007 | resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.5.tgz#dd32c2e82c72002dc9f02eb67ba6761f43456ca1" |
2026 | integrity sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q== | 2008 | integrity sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ== |
2027 | dependencies: | 2009 | dependencies: |
2028 | node-gyp-build "^4.2.0" | 2010 | node-gyp-build "^4.2.0" |
2029 | 2011 | ||
@@ -2032,10 +2014,10 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: | |||
2032 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" | 2014 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" |
2033 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= | 2015 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= |
2034 | 2016 | ||
2035 | utp-native@^2.3.0: | 2017 | utp-native@^2.4.0: |
2036 | version "2.4.0" | 2018 | version "2.5.0" |
2037 | resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.4.0.tgz#7010de2134e9d767be0ec34e817c3300592befc0" | 2019 | resolved "https://registry.yarnpkg.com/utp-native/-/utp-native-2.5.0.tgz#3d8321760108b30cb15391196c8cc93db85b61ce" |
2038 | integrity sha512-jKwpFiEaDUuNH5S4vVk/+waAX+yA6f3Lw4flqOROH1ZE/jcT4mh0/hjIGSuPP9j9RbQcsBG6Fu6LaFk4ojXFxw== | 2020 | integrity sha512-HoHPE6gwLxC0xlpYJUl+Xw2sh809lhXx3TexHsb2/xY8vEd6NwuvAxOI/X27dBTc/TOT5diWUpCJWDaunkcVvA== |
2039 | dependencies: | 2021 | dependencies: |
2040 | napi-macros "^2.0.0" | 2022 | napi-macros "^2.0.0" |
2041 | node-gyp-build "^4.2.0" | 2023 | node-gyp-build "^4.2.0" |
@@ -2069,9 +2051,9 @@ webidl-conversions@^4.0.2: | |||
2069 | integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== | 2051 | integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== |
2070 | 2052 | ||
2071 | webtorrent-cli@^3.2.0: | 2053 | webtorrent-cli@^3.2.0: |
2072 | version "3.2.1" | 2054 | version "3.3.0" |
2073 | resolved "https://registry.yarnpkg.com/webtorrent-cli/-/webtorrent-cli-3.2.1.tgz#a39e49fe7d8a4e3c00fe34d22f113f1e021c9bec" | 2055 | resolved "https://registry.yarnpkg.com/webtorrent-cli/-/webtorrent-cli-3.3.0.tgz#465e9fb82373c6b279904b5876d4745cb8ac6b43" |
2074 | integrity sha512-DHUtDymD5ZGv/h35FY4n9YdlHoNOy07X7ibexi+19AL/+MFdGuIIQEEnJQT/wA05mrzR6ubeJmcLXXqZISwLFQ== | 2056 | integrity sha512-E0gb1fXb8xNScgewQmvAmNLrnEto6MOaKlfzITVzR+bbU80LeV/YdrLw536ffcwiUv0arKQyfR/WIweBFrKWbg== |
2075 | dependencies: | 2057 | dependencies: |
2076 | clivas "^0.2.0" | 2058 | clivas "^0.2.0" |
2077 | common-tags "^1.8.0" | 2059 | common-tags "^1.8.0" |
@@ -2083,11 +2065,11 @@ webtorrent-cli@^3.2.0: | |||
2083 | minimist "^1.2.5" | 2065 | minimist "^1.2.5" |
2084 | moment "^2.27.0" | 2066 | moment "^2.27.0" |
2085 | network-address "^1.1.2" | 2067 | network-address "^1.1.2" |
2086 | open "^7.1.0" | 2068 | open "^8.0.0" |
2087 | parse-torrent "^7.1.3" | 2069 | parse-torrent "^9.0.0" |
2088 | prettier-bytes "^1.0.4" | 2070 | prettier-bytes "^1.0.4" |
2089 | vlc-command "^1.2.0" | 2071 | vlc-command "^1.2.0" |
2090 | webtorrent ">=0.108.6" | 2072 | webtorrent "^1.0.0" |
2091 | winreg "^1.2.4" | 2073 | winreg "^1.2.4" |
2092 | optionalDependencies: | 2074 | optionalDependencies: |
2093 | airplay-js "^0.3.0" | 2075 | airplay-js "^0.3.0" |
@@ -2095,63 +2077,64 @@ webtorrent-cli@^3.2.0: | |||
2095 | nodebmc "0.0.7" | 2077 | nodebmc "0.0.7" |
2096 | 2078 | ||
2097 | webtorrent-hybrid@^4.0.3: | 2079 | webtorrent-hybrid@^4.0.3: |
2098 | version "4.0.3" | 2080 | version "4.1.0" |
2099 | resolved "https://registry.yarnpkg.com/webtorrent-hybrid/-/webtorrent-hybrid-4.0.3.tgz#d47d47824e3f8c7a5a5a70cc1b5398059c7fbcc0" | 2081 | resolved "https://registry.yarnpkg.com/webtorrent-hybrid/-/webtorrent-hybrid-4.1.0.tgz#86e397a8f051de225c60ce751f47d28a906cbfdc" |
2100 | integrity sha512-D8/Fmxt/xWUwrA/qJ5SrLDucOYE9B4AhWzuLgfP1y6ZlHI+Sl0PXnqblO1yT97odW0mPO12Qy+7fQM6vxXJMqA== | 2082 | integrity sha512-IqRWVI+gXHjv/ybj3YK6Q4gJM1OaIgy3Nw3ec0iPS7TeBrG6R3yhfHupF39DIwyKFERZDnYoxVnxHOt8TAaucw== |
2101 | dependencies: | 2083 | dependencies: |
2102 | create-torrent "^4.4.2" | 2084 | create-torrent "^4.4.2" |
2103 | webtorrent ">=0.111.0" | 2085 | webtorrent "^1.0.0" |
2104 | webtorrent-cli "^3.2.0" | 2086 | webtorrent-cli "^3.2.0" |
2105 | wrtc "^0.4.6" | 2087 | wrtc "^0.4.6" |
2106 | 2088 | ||
2107 | webtorrent@>=0.108.6, webtorrent@>=0.111.0: | 2089 | webtorrent@^1.0.0: |
2108 | version "0.116.2" | 2090 | version "1.0.0" |
2109 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.116.2.tgz#5f7a851443947cf72ea09c4bca68371ea442a952" | 2091 | resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-1.0.0.tgz#1d8fd388726ca013feb17d2437b1b9df2d24b5e8" |
2110 | integrity sha512-u6ctyPEwUvbFKZsT9HRU1Q+SSqKWoNMlXWbaPTUlGsPrNZ3mCCeHtn8Hcf61jr1e4hna5oQBtVjg2N5/2V8d9g== | 2092 | integrity sha512-htwcY5OBOWL/OMwaw3xi1Mp2gE9k5UmGTKeO3n1ixQDH9QgeqqRlBJz2ZLEyOL8yN1FdS/D9z+ijm06bZ3oW5w== |
2111 | dependencies: | 2093 | dependencies: |
2112 | addr-to-ip-port "^1.5.1" | 2094 | addr-to-ip-port "^1.5.1" |
2113 | bitfield "^4.0.0" | 2095 | bitfield "^4.0.0" |
2114 | bittorrent-dht "^10.0.0" | 2096 | bittorrent-dht "^10.0.0" |
2115 | bittorrent-protocol "^3.2.0" | 2097 | bittorrent-protocol "^3.3.1" |
2116 | chrome-net "^3.3.4" | 2098 | chrome-net "^3.3.4" |
2117 | chunk-store-stream "^4.2.0" | 2099 | chunk-store-stream "^4.3.0" |
2118 | cpus "^1.0.3" | 2100 | cpus "^1.0.3" |
2119 | create-torrent "^4.4.4" | 2101 | create-torrent "^4.7.0" |
2120 | debug "^4.3.1" | 2102 | debug "^4.3.1" |
2121 | end-of-stream "^1.4.4" | 2103 | end-of-stream "^1.4.4" |
2122 | escape-html "^1.0.3" | 2104 | escape-html "^1.0.3" |
2123 | fs-chunk-store "^2.0.2" | 2105 | fs-chunk-store "^2.0.3" |
2124 | http-node "github:feross/http-node#webtorrent" | 2106 | http-node "github:feross/http-node#webtorrent" |
2125 | immediate-chunk-store "^2.1.1" | 2107 | immediate-chunk-store "^2.2.0" |
2126 | load-ip-set "^2.1.2" | 2108 | load-ip-set "^2.2.1" |
2127 | memory-chunk-store "^1.3.1" | 2109 | lt_donthave "^1.0.1" |
2128 | mime "^2.5.0" | 2110 | memory-chunk-store "^1.3.5" |
2111 | mime "^2.5.2" | ||
2129 | multistream "^4.1.0" | 2112 | multistream "^4.1.0" |
2130 | package-json-versionify "^1.0.4" | 2113 | package-json-versionify "^1.0.4" |
2131 | parse-torrent "^9.1.1" | 2114 | parse-torrent "^9.1.3" |
2132 | pump "^3.0.0" | 2115 | pump "^3.0.0" |
2133 | queue-microtask "^1.2.2" | 2116 | queue-microtask "^1.2.3" |
2134 | random-iterate "^1.0.1" | 2117 | random-iterate "^1.0.1" |
2135 | randombytes "^2.1.0" | 2118 | randombytes "^2.1.0" |
2136 | range-parser "^1.2.1" | 2119 | range-parser "^1.2.1" |
2137 | readable-stream "^3.6.0" | 2120 | readable-stream "^3.6.0" |
2138 | render-media "^4.1.0" | 2121 | render-media "^4.1.0" |
2139 | run-parallel "^1.1.10" | 2122 | run-parallel "^1.2.0" |
2140 | run-parallel-limit "^1.0.6" | 2123 | run-parallel-limit "^1.1.0" |
2141 | simple-concat "^1.0.1" | 2124 | simple-concat "^1.0.1" |
2142 | simple-get "^4.0.0" | 2125 | simple-get "^4.0.0" |
2143 | simple-peer "^9.9.3" | 2126 | simple-peer "^9.11.0" |
2144 | simple-sha1 "^3.0.1" | 2127 | simple-sha1 "^3.1.0" |
2145 | speedometer "^1.1.0" | 2128 | speedometer "^1.1.0" |
2146 | stream-to-blob "^2.0.1" | 2129 | stream-to-blob "^2.0.1" |
2147 | stream-to-blob-url "^3.0.2" | 2130 | stream-to-blob-url "^3.0.2" |
2148 | stream-with-known-length-to-buffer "^1.0.4" | 2131 | stream-with-known-length-to-buffer "^1.0.4" |
2149 | torrent-discovery "^9.4.0" | 2132 | torrent-discovery "^9.4.0" |
2150 | torrent-piece "^2.0.0" | 2133 | torrent-piece "^2.0.1" |
2151 | unordered-array-remove "^1.0.2" | 2134 | unordered-array-remove "^1.0.2" |
2152 | ut_metadata "^3.5.2" | 2135 | ut_metadata "^3.5.2" |
2153 | ut_pex "^2.0.1" | 2136 | ut_pex "^3.0.0" |
2154 | utp-native "^2.3.0" | 2137 | utp-native "^2.4.0" |
2155 | 2138 | ||
2156 | which@^1.2.9: | 2139 | which@^1.2.9: |
2157 | version "1.3.1" | 2140 | version "1.3.1" |
@@ -2208,10 +2191,10 @@ wrtc@^0.4.6: | |||
2208 | optionalDependencies: | 2191 | optionalDependencies: |
2209 | domexception "^1.0.1" | 2192 | domexception "^1.0.1" |
2210 | 2193 | ||
2211 | ws@^7.3.0, ws@^7.4.2: | 2194 | ws@^7.4.2, ws@^7.4.5: |
2212 | version "7.4.4" | 2195 | version "7.4.6" |
2213 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" | 2196 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" |
2214 | integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== | 2197 | integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== |
2215 | 2198 | ||
2216 | xml2js@^0.4.8: | 2199 | xml2js@^0.4.8: |
2217 | version "0.4.23" | 2200 | version "0.4.23" |
diff --git a/server/types/models/moderation/abuse-message.ts b/server/types/models/abuse/abuse-message.ts index 565eca706..565eca706 100644 --- a/server/types/models/moderation/abuse-message.ts +++ b/server/types/models/abuse/abuse-message.ts | |||
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/abuse/abuse.ts index 6fd83684c..6fd83684c 100644 --- a/server/types/models/moderation/abuse.ts +++ b/server/types/models/abuse/abuse.ts | |||
diff --git a/server/types/models/moderation/index.ts b/server/types/models/abuse/index.ts index 1ed91b249..1ed91b249 100644 --- a/server/types/models/moderation/index.ts +++ b/server/types/models/abuse/index.ts | |||
diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts index 9513acad8..984841291 100644 --- a/server/types/models/account/account.ts +++ b/server/types/models/account/account.ts | |||
@@ -1,7 +1,5 @@ | |||
1 | import { FunctionProperties, PickWith } from '@shared/core-utils' | 1 | import { FunctionProperties, PickWith } from '@shared/core-utils' |
2 | import { AccountModel } from '../../../models/account/account' | 2 | import { AccountModel } from '../../../models/account/account' |
3 | import { MChannelDefault } from '../video/video-channels' | ||
4 | import { MAccountBlocklistId } from './account-blocklist' | ||
5 | import { | 3 | import { |
6 | MActor, | 4 | MActor, |
7 | MActorAPAccount, | 5 | MActorAPAccount, |
@@ -15,7 +13,9 @@ import { | |||
15 | MActorSummary, | 13 | MActorSummary, |
16 | MActorSummaryFormattable, | 14 | MActorSummaryFormattable, |
17 | MActorUrl | 15 | MActorUrl |
18 | } from './actor' | 16 | } from '../actor' |
17 | import { MChannelDefault } from '../video/video-channels' | ||
18 | import { MAccountBlocklistId } from './account-blocklist' | ||
19 | 19 | ||
20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> | 20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> |
21 | 21 | ||
diff --git a/server/types/models/account/actor-custom-page.ts b/server/types/models/account/actor-custom-page.ts new file mode 100644 index 000000000..2cb8aa7e4 --- /dev/null +++ b/server/types/models/account/actor-custom-page.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | |||
2 | import { ActorCustomPageModel } from '../../../models/account/actor-custom-page' | ||
3 | |||
4 | export type MActorCustomPage = Omit<ActorCustomPageModel, 'Actor'> | ||
diff --git a/server/types/models/account/index.ts b/server/types/models/account/index.ts index e3fc00f94..9679c01e4 100644 --- a/server/types/models/account/index.ts +++ b/server/types/models/account/index.ts | |||
@@ -1,5 +1,3 @@ | |||
1 | export * from './account' | 1 | export * from './account' |
2 | export * from './actor-custom-page' | ||
2 | export * from './account-blocklist' | 3 | export * from './account-blocklist' |
3 | export * from './actor-follow' | ||
4 | export * from './actor-image' | ||
5 | export * from './actor' | ||
diff --git a/server/types/models/account/actor-follow.ts b/server/types/models/actor/actor-follow.ts index 8e19c6140..98a6ca8a5 100644 --- a/server/types/models/account/actor-follow.ts +++ b/server/types/models/actor/actor-follow.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { PickWith } from '@shared/core-utils' | 1 | import { PickWith } from '@shared/core-utils' |
2 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 2 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
3 | import { | 3 | import { |
4 | MActor, | 4 | MActor, |
5 | MActorChannelAccountActor, | 5 | MActorChannelAccountActor, |
diff --git a/server/types/models/account/actor-image.ts b/server/types/models/actor/actor-image.ts index e59f8b141..89adb01ae 100644 --- a/server/types/models/account/actor-image.ts +++ b/server/types/models/actor/actor-image.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { ActorImageModel } from '../../../models/account/actor-image' | ||
2 | import { FunctionProperties } from '@shared/core-utils' | 1 | import { FunctionProperties } from '@shared/core-utils' |
2 | import { ActorImageModel } from '../../../models/actor/actor-image' | ||
3 | 3 | ||
4 | export type MActorImage = ActorImageModel | 4 | export type MActorImage = ActorImageModel |
5 | 5 | ||
diff --git a/server/types/models/account/actor.ts b/server/types/models/actor/actor.ts index 0b620872e..b3a70cbce 100644 --- a/server/types/models/account/actor.ts +++ b/server/types/models/actor/actor.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | |||
2 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' | 1 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' |
3 | import { ActorModel } from '../../../models/activitypub/actor' | 2 | import { ActorModel } from '../../../models/actor/actor' |
3 | import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from '../account' | ||
4 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' | 4 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' |
5 | import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' | 5 | import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' |
6 | import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' | ||
7 | import { MActorImage, MActorImageFormattable } from './actor-image' | 6 | import { MActorImage, MActorImageFormattable } from './actor-image' |
8 | 7 | ||
9 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> | 8 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> |
diff --git a/server/types/models/actor/index.ts b/server/types/models/actor/index.ts new file mode 100644 index 000000000..b27815255 --- /dev/null +++ b/server/types/models/actor/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './actor-follow' | ||
2 | export * from './actor-image' | ||
3 | export * from './actor' | ||
diff --git a/server/types/models/index.ts b/server/types/models/index.ts index b4fdb1ff3..704cb9844 100644 --- a/server/types/models/index.ts +++ b/server/types/models/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | export * from './abuse' | ||
1 | export * from './account' | 2 | export * from './account' |
3 | export * from './actor' | ||
2 | export * from './application' | 4 | export * from './application' |
3 | export * from './moderation' | ||
4 | export * from './oauth' | 5 | export * from './oauth' |
5 | export * from './server' | 6 | export * from './server' |
6 | export * from './user' | 7 | export * from './user' |
diff --git a/server/types/models/user/user-notification-setting.ts b/server/types/models/user/user-notification-setting.ts index c674add1b..d1db645e7 100644 --- a/server/types/models/user/user-notification-setting.ts +++ b/server/types/models/user/user-notification-setting.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { UserNotificationSettingModel } from '@server/models/account/user-notification-setting' | 1 | import { UserNotificationSettingModel } from '@server/models/user/user-notification-setting' |
2 | 2 | ||
3 | export type MNotificationSetting = Omit<UserNotificationSettingModel, 'User'> | 3 | export type MNotificationSetting = Omit<UserNotificationSettingModel, 'User'> |
4 | 4 | ||
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts index 7ebb0485d..918614dd1 100644 --- a/server/types/models/user/user-notification.ts +++ b/server/types/models/user/user-notification.ts | |||
@@ -2,13 +2,13 @@ import { VideoAbuseModel } from '@server/models/abuse/video-abuse' | |||
2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' | 2 | import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' |
3 | import { ApplicationModel } from '@server/models/application/application' | 3 | import { ApplicationModel } from '@server/models/application/application' |
4 | import { PluginModel } from '@server/models/server/plugin' | 4 | import { PluginModel } from '@server/models/server/plugin' |
5 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 6 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
6 | import { AbuseModel } from '../../../models/abuse/abuse' | 7 | import { AbuseModel } from '../../../models/abuse/abuse' |
7 | import { AccountModel } from '../../../models/account/account' | 8 | import { AccountModel } from '../../../models/account/account' |
8 | import { ActorImageModel } from '../../../models/account/actor-image' | 9 | import { ActorModel } from '../../../models/actor/actor' |
9 | import { UserNotificationModel } from '../../../models/account/user-notification' | 10 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
10 | import { ActorModel } from '../../../models/activitypub/actor' | 11 | import { ActorImageModel } from '../../../models/actor/actor-image' |
11 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
12 | import { ServerModel } from '../../../models/server/server' | 12 | import { ServerModel } from '../../../models/server/server' |
13 | import { VideoModel } from '../../../models/video/video' | 13 | import { VideoModel } from '../../../models/video/video' |
14 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | 14 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' |
diff --git a/server/types/models/user/user-video-history.ts b/server/types/models/user/user-video-history.ts index 62673ab1b..34e2930e7 100644 --- a/server/types/models/user/user-video-history.ts +++ b/server/types/models/user/user-video-history.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { UserVideoHistoryModel } from '../../../models/account/user-video-history' | 1 | import { UserVideoHistoryModel } from '../../../models/user/user-video-history' |
2 | 2 | ||
3 | export type MUserVideoHistory = Omit<UserVideoHistoryModel, 'Video' | 'User'> | 3 | export type MUserVideoHistory = Omit<UserVideoHistoryModel, 'Video' | 'User'> |
4 | 4 | ||
diff --git a/server/types/models/user/user.ts b/server/types/models/user/user.ts index fa7de9c52..f79220e11 100644 --- a/server/types/models/user/user.ts +++ b/server/types/models/user/user.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { AccountModel } from '@server/models/account/account' | 1 | import { AccountModel } from '@server/models/account/account' |
2 | import { UserModel } from '@server/models/user/user' | ||
2 | import { MVideoPlaylist } from '@server/types/models' | 3 | import { MVideoPlaylist } from '@server/types/models' |
3 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 4 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
4 | import { UserModel } from '../../../models/account/user' | ||
5 | import { | 5 | import { |
6 | MAccount, | 6 | MAccount, |
7 | MAccountDefault, | 7 | MAccountDefault, |
diff --git a/server/types/models/video/schedule-video-update.ts b/server/types/models/video/schedule-video-update.ts index 5d2936000..39fd73501 100644 --- a/server/types/models/video/schedule-video-update.ts +++ b/server/types/models/video/schedule-video-update.ts | |||
@@ -1,8 +1,4 @@ | |||
1 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' | 1 | import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' |
2 | import { PickWith } from '@shared/core-utils' | ||
3 | import { MVideoAPWithoutCaption, MVideoWithBlacklistLight } from './video' | ||
4 | |||
5 | type Use<K extends keyof ScheduleVideoUpdateModel, M> = PickWith<ScheduleVideoUpdateModel, K, M> | ||
6 | 2 | ||
7 | // ############################################################################ | 3 | // ############################################################################ |
8 | 4 | ||
@@ -10,10 +6,6 @@ export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'> | |||
10 | 6 | ||
11 | // ############################################################################ | 7 | // ############################################################################ |
12 | 8 | ||
13 | export type MScheduleVideoUpdateVideoAll = | ||
14 | MScheduleVideoUpdate & | ||
15 | Use<'Video', MVideoAPWithoutCaption & MVideoWithBlacklistLight> | ||
16 | |||
17 | // Format for API or AP object | 9 | // Format for API or AP object |
18 | 10 | ||
19 | export type MScheduleVideoUpdateFormattable = Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'> | 11 | export type MScheduleVideoUpdateFormattable = Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'> |
diff --git a/server/types/models/video/video-channels.ts b/server/types/models/video/video-channels.ts index f577807ca..c147567d9 100644 --- a/server/types/models/video/video-channels.ts +++ b/server/types/models/video/video-channels.ts | |||
@@ -9,7 +9,9 @@ import { | |||
9 | MAccountSummaryBlocks, | 9 | MAccountSummaryBlocks, |
10 | MAccountSummaryFormattable, | 10 | MAccountSummaryFormattable, |
11 | MAccountUrl, | 11 | MAccountUrl, |
12 | MAccountUserId, | 12 | MAccountUserId |
13 | } from '../account' | ||
14 | import { | ||
13 | MActor, | 15 | MActor, |
14 | MActorAccountChannelId, | 16 | MActorAccountChannelId, |
15 | MActorAPChannel, | 17 | MActorAPChannel, |
@@ -23,7 +25,7 @@ import { | |||
23 | MActorSummary, | 25 | MActorSummary, |
24 | MActorSummaryFormattable, | 26 | MActorSummaryFormattable, |
25 | MActorUrl | 27 | MActorUrl |
26 | } from '../account' | 28 | } from '../actor' |
27 | import { MVideo } from './video' | 29 | import { MVideo } from './video' |
28 | 30 | ||
29 | type Use<K extends keyof VideoChannelModel, M> = PickWith<VideoChannelModel, K, M> | 31 | type Use<K extends keyof VideoChannelModel, M> = PickWith<VideoChannelModel, K, M> |
diff --git a/server/types/models/video/video-playlist.ts b/server/types/models/video/video-playlist.ts index 79e2daebf..2f9537cf5 100644 --- a/server/types/models/video/video-playlist.ts +++ b/server/types/models/video/video-playlist.ts | |||
@@ -69,7 +69,7 @@ export type MVideoPlaylistAccountChannelDefault = | |||
69 | // With all associations | 69 | // With all associations |
70 | 70 | ||
71 | export type MVideoPlaylistFull = | 71 | export type MVideoPlaylistFull = |
72 | MVideoPlaylist & | 72 | MVideoPlaylistVideosLength & |
73 | Use<'OwnerAccount', MAccountDefault> & | 73 | Use<'OwnerAccount', MAccountDefault> & |
74 | Use<'VideoChannel', MChannelDefault> & | 74 | Use<'VideoChannel', MChannelDefault> & |
75 | Use<'Thumbnail', MThumbnail> | 75 | Use<'Thumbnail', MThumbnail> |
@@ -84,7 +84,7 @@ export type MVideoPlaylistAccountChannelSummary = | |||
84 | Use<'VideoChannel', MChannelSummary> | 84 | Use<'VideoChannel', MChannelSummary> |
85 | 85 | ||
86 | export type MVideoPlaylistFullSummary = | 86 | export type MVideoPlaylistFullSummary = |
87 | MVideoPlaylist & | 87 | MVideoPlaylistVideosLength & |
88 | Use<'Thumbnail', MThumbnail> & | 88 | Use<'Thumbnail', MThumbnail> & |
89 | Use<'OwnerAccount', MAccountSummary> & | 89 | Use<'OwnerAccount', MAccountSummary> & |
90 | Use<'VideoChannel', MChannelSummary> | 90 | Use<'VideoChannel', MChannelSummary> |
diff --git a/server/types/models/video/video-share.ts b/server/types/models/video/video-share.ts index b7a783bb6..78f44e58c 100644 --- a/server/types/models/video/video-share.ts +++ b/server/types/models/video/video-share.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { VideoShareModel } from '../../../models/video/video-share' | ||
2 | import { PickWith } from '@shared/core-utils' | 1 | import { PickWith } from '@shared/core-utils' |
3 | import { MActorDefault } from '../account' | 2 | import { VideoShareModel } from '../../../models/video/video-share' |
3 | import { MActorDefault } from '../actor' | ||
4 | import { MVideo } from './video' | 4 | import { MVideo } from './video' |
5 | 5 | ||
6 | type Use<K extends keyof VideoShareModel, M> = PickWith<VideoShareModel, K, M> | 6 | type Use<K extends keyof VideoShareModel, M> = PickWith<VideoShareModel, K, M> |
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 2432b7ac4..8774bcd8c 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Router, Response } from 'express' | 1 | import { Response, Router } from 'express' |
2 | import { Logger } from 'winston' | 2 | import { Logger } from 'winston' |
3 | import { ActorModel } from '@server/models/activitypub/actor' | 3 | import { ActorModel } from '@server/models/actor/actor' |
4 | import { | 4 | import { |
5 | PluginPlaylistPrivacyManager, | 5 | PluginPlaylistPrivacyManager, |
6 | PluginSettingsManager, | 6 | PluginSettingsManager, |
diff --git a/server/types/sequelize.ts b/server/types/sequelize.ts index 9cd83612d..535113d01 100644 --- a/server/types/sequelize.ts +++ b/server/types/sequelize.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Model } from 'sequelize-typescript' | 1 | import { AttributesOnly } from '@shared/core-utils' |
2 | import { Model } from 'sequelize' | ||
2 | 3 | ||
3 | // Thanks to sequelize-typescript: https://github.com/RobinBuschmann/sequelize-typescript | 4 | // Thanks to sequelize-typescript: https://github.com/RobinBuschmann/sequelize-typescript |
4 | 5 | ||
@@ -9,7 +10,7 @@ export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] } | |||
9 | 10 | ||
10 | export type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> } | 11 | export type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> } |
11 | 12 | ||
12 | export type FilteredModelAttributes<T extends Model<T>> = RecursivePartial<Omit<T, keyof Model<any>>> & { | 13 | export type FilteredModelAttributes<T extends Model<any>> = Partial<AttributesOnly<T>> & { |
13 | id?: number | any | 14 | id?: number | any |
14 | createdAt?: Date | any | 15 | createdAt?: Date | any |
15 | updatedAt?: Date | any | 16 | updatedAt?: Date | any |
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts index 55b6e0039..1a8dc3430 100644 --- a/server/typings/express/index.d.ts +++ b/server/typings/express/index.d.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | |||
1 | import { RegisterServerAuthExternalOptions } from '@server/types' | 2 | import { RegisterServerAuthExternalOptions } from '@server/types' |
2 | import { | 3 | import { |
3 | MAbuseMessage, | 4 | MAbuseMessage, |
@@ -9,6 +10,8 @@ import { | |||
9 | MStreamingPlaylist, | 10 | MStreamingPlaylist, |
10 | MVideoChangeOwnershipFull, | 11 | MVideoChangeOwnershipFull, |
11 | MVideoFile, | 12 | MVideoFile, |
13 | MVideoFormattableDetails, | ||
14 | MVideoId, | ||
12 | MVideoImmutable, | 15 | MVideoImmutable, |
13 | MVideoLive, | 16 | MVideoLive, |
14 | MVideoPlaylistFull, | 17 | MVideoPlaylistFull, |
@@ -20,7 +23,7 @@ import { MVideoImportDefault } from '@server/types/models/video/video-import' | |||
20 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' | 23 | import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' |
21 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' | 24 | import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' |
22 | import { HttpMethod } from '@shared/core-utils/miscs/http-methods' | 25 | import { HttpMethod } from '@shared/core-utils/miscs/http-methods' |
23 | import { VideoCreate } from '@shared/models' | 26 | import { PeerTubeProblemDocumentData, ServerErrorCode, VideoCreate } from '@shared/models' |
24 | import { File as UploadXFile, Metadata } from '@uploadx/core' | 27 | import { File as UploadXFile, Metadata } from '@uploadx/core' |
25 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' | 28 | import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' |
26 | import { | 29 | import { |
@@ -34,12 +37,11 @@ import { | |||
34 | MVideoBlacklist, | 37 | MVideoBlacklist, |
35 | MVideoCaptionVideo, | 38 | MVideoCaptionVideo, |
36 | MVideoFullLight, | 39 | MVideoFullLight, |
37 | MVideoIdThumbnail, | ||
38 | MVideoRedundancyVideo, | 40 | MVideoRedundancyVideo, |
39 | MVideoShareActor, | 41 | MVideoShareActor, |
40 | MVideoThumbnail, | 42 | MVideoThumbnail |
41 | MVideoWithRights | ||
42 | } from '../../types/models' | 43 | } from '../../types/models' |
44 | |||
43 | declare module 'express' { | 45 | declare module 'express' { |
44 | export interface Request { | 46 | export interface Request { |
45 | query: any | 47 | query: any |
@@ -83,14 +85,27 @@ declare module 'express' { | |||
83 | filename: string | 85 | filename: string |
84 | } | 86 | } |
85 | 87 | ||
86 | // Extends locals property from Response | 88 | // Extends Response with added functions and potential variables passed by middlewares |
87 | interface Response { | 89 | interface Response { |
90 | fail: (options: { | ||
91 | message: string | ||
92 | |||
93 | title?: string | ||
94 | status?: number | ||
95 | type?: ServerErrorCode | ||
96 | instance?: string | ||
97 | |||
98 | data?: PeerTubeProblemDocumentData | ||
99 | }) => void | ||
100 | |||
88 | locals: { | 101 | locals: { |
102 | docUrl?: string | ||
103 | |||
104 | videoAPI?: MVideoFormattableDetails | ||
89 | videoAll?: MVideoFullLight | 105 | videoAll?: MVideoFullLight |
90 | onlyImmutableVideo?: MVideoImmutable | 106 | onlyImmutableVideo?: MVideoImmutable |
91 | onlyVideo?: MVideoThumbnail | 107 | onlyVideo?: MVideoThumbnail |
92 | onlyVideoWithRights?: MVideoWithRights | 108 | videoId?: MVideoId |
93 | videoId?: MVideoIdThumbnail | ||
94 | 109 | ||
95 | videoLive?: MVideoLive | 110 | videoLive?: MVideoLive |
96 | 111 | ||