diff options
Diffstat (limited to 'server/controllers/api')
35 files changed, 1335 insertions, 900 deletions
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) |