From 3a4992633ee62d5edfbb484d9c6bcb3cf158489d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 31 Jul 2023 14:34:36 +0200 Subject: Migrate server to ESM Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports) --- server/controllers/api/abuse.ts | 259 ----------- server/controllers/api/accounts.ts | 266 ----------- server/controllers/api/blocklist.ts | 110 ----- server/controllers/api/bulk.ts | 44 -- server/controllers/api/config.ts | 377 --------------- server/controllers/api/custom-page.ts | 48 -- server/controllers/api/index.ts | 73 --- server/controllers/api/jobs.ts | 109 ----- server/controllers/api/metrics.ts | 34 -- server/controllers/api/oauth-clients.ts | 54 --- server/controllers/api/overviews.ts | 139 ------ server/controllers/api/plugins.ts | 230 --------- server/controllers/api/runners/index.ts | 20 - server/controllers/api/runners/jobs-files.ts | 112 ----- server/controllers/api/runners/jobs.ts | 416 ----------------- server/controllers/api/runners/manage-runners.ts | 112 ----- .../controllers/api/runners/registration-tokens.ts | 91 ---- server/controllers/api/search/index.ts | 19 - .../api/search/search-video-channels.ts | 152 ------ .../api/search/search-video-playlists.ts | 131 ------ server/controllers/api/search/search-videos.ts | 167 ------- server/controllers/api/search/shared/index.ts | 1 - server/controllers/api/search/shared/utils.ts | 16 - server/controllers/api/server/contact.ts | 34 -- server/controllers/api/server/debug.ts | 56 --- server/controllers/api/server/follows.ts | 214 --------- server/controllers/api/server/index.ts | 27 -- server/controllers/api/server/logs.ts | 203 -------- server/controllers/api/server/redundancy.ts | 116 ----- server/controllers/api/server/server-blocklist.ts | 158 ------- server/controllers/api/server/stats.ts | 26 -- server/controllers/api/users/email-verification.ts | 72 --- server/controllers/api/users/index.ts | 319 ------------- server/controllers/api/users/me.ts | 277 ----------- server/controllers/api/users/my-abuses.ts | 48 -- server/controllers/api/users/my-blocklist.ts | 149 ------ server/controllers/api/users/my-history.ts | 75 --- server/controllers/api/users/my-notifications.ts | 116 ----- server/controllers/api/users/my-subscriptions.ts | 193 -------- server/controllers/api/users/my-video-playlists.ts | 51 -- server/controllers/api/users/registrations.ts | 249 ---------- server/controllers/api/users/token.ts | 131 ------ server/controllers/api/users/two-factor.ts | 95 ---- server/controllers/api/video-channel-sync.ts | 79 ---- server/controllers/api/video-channel.ts | 431 ----------------- server/controllers/api/video-playlist.ts | 514 --------------------- server/controllers/api/videos/blacklist.ts | 112 ----- server/controllers/api/videos/captions.ts | 93 ---- server/controllers/api/videos/comment.ts | 234 ---------- server/controllers/api/videos/files.ts | 122 ----- server/controllers/api/videos/import.ts | 262 ----------- server/controllers/api/videos/index.ts | 228 --------- server/controllers/api/videos/live.ts | 224 --------- server/controllers/api/videos/ownership.ts | 138 ------ server/controllers/api/videos/passwords.ts | 105 ----- server/controllers/api/videos/rate.ts | 87 ---- server/controllers/api/videos/source.ts | 206 --------- server/controllers/api/videos/stats.ts | 75 --- server/controllers/api/videos/storyboard.ts | 29 -- server/controllers/api/videos/studio.ts | 143 ------ server/controllers/api/videos/token.ts | 33 -- server/controllers/api/videos/transcoding.ts | 60 --- server/controllers/api/videos/update.ts | 210 --------- server/controllers/api/videos/upload.ts | 287 ------------ server/controllers/api/videos/view.ts | 60 --- 65 files changed, 9321 deletions(-) delete mode 100644 server/controllers/api/abuse.ts delete mode 100644 server/controllers/api/accounts.ts delete mode 100644 server/controllers/api/blocklist.ts delete mode 100644 server/controllers/api/bulk.ts delete mode 100644 server/controllers/api/config.ts delete mode 100644 server/controllers/api/custom-page.ts delete mode 100644 server/controllers/api/index.ts delete mode 100644 server/controllers/api/jobs.ts delete mode 100644 server/controllers/api/metrics.ts delete mode 100644 server/controllers/api/oauth-clients.ts delete mode 100644 server/controllers/api/overviews.ts delete mode 100644 server/controllers/api/plugins.ts delete mode 100644 server/controllers/api/runners/index.ts delete mode 100644 server/controllers/api/runners/jobs-files.ts delete mode 100644 server/controllers/api/runners/jobs.ts delete mode 100644 server/controllers/api/runners/manage-runners.ts delete mode 100644 server/controllers/api/runners/registration-tokens.ts delete mode 100644 server/controllers/api/search/index.ts delete mode 100644 server/controllers/api/search/search-video-channels.ts delete mode 100644 server/controllers/api/search/search-video-playlists.ts delete mode 100644 server/controllers/api/search/search-videos.ts delete mode 100644 server/controllers/api/search/shared/index.ts delete mode 100644 server/controllers/api/search/shared/utils.ts delete mode 100644 server/controllers/api/server/contact.ts delete mode 100644 server/controllers/api/server/debug.ts delete mode 100644 server/controllers/api/server/follows.ts delete mode 100644 server/controllers/api/server/index.ts delete mode 100644 server/controllers/api/server/logs.ts delete mode 100644 server/controllers/api/server/redundancy.ts delete mode 100644 server/controllers/api/server/server-blocklist.ts delete mode 100644 server/controllers/api/server/stats.ts delete mode 100644 server/controllers/api/users/email-verification.ts delete mode 100644 server/controllers/api/users/index.ts delete mode 100644 server/controllers/api/users/me.ts delete mode 100644 server/controllers/api/users/my-abuses.ts delete mode 100644 server/controllers/api/users/my-blocklist.ts delete mode 100644 server/controllers/api/users/my-history.ts delete mode 100644 server/controllers/api/users/my-notifications.ts delete mode 100644 server/controllers/api/users/my-subscriptions.ts delete mode 100644 server/controllers/api/users/my-video-playlists.ts delete mode 100644 server/controllers/api/users/registrations.ts delete mode 100644 server/controllers/api/users/token.ts delete mode 100644 server/controllers/api/users/two-factor.ts delete mode 100644 server/controllers/api/video-channel-sync.ts delete mode 100644 server/controllers/api/video-channel.ts delete mode 100644 server/controllers/api/video-playlist.ts delete mode 100644 server/controllers/api/videos/blacklist.ts delete mode 100644 server/controllers/api/videos/captions.ts delete mode 100644 server/controllers/api/videos/comment.ts delete mode 100644 server/controllers/api/videos/files.ts delete mode 100644 server/controllers/api/videos/import.ts delete mode 100644 server/controllers/api/videos/index.ts delete mode 100644 server/controllers/api/videos/live.ts delete mode 100644 server/controllers/api/videos/ownership.ts delete mode 100644 server/controllers/api/videos/passwords.ts delete mode 100644 server/controllers/api/videos/rate.ts delete mode 100644 server/controllers/api/videos/source.ts delete mode 100644 server/controllers/api/videos/stats.ts delete mode 100644 server/controllers/api/videos/storyboard.ts delete mode 100644 server/controllers/api/videos/studio.ts delete mode 100644 server/controllers/api/videos/token.ts delete mode 100644 server/controllers/api/videos/transcoding.ts delete mode 100644 server/controllers/api/videos/update.ts delete mode 100644 server/controllers/api/videos/upload.ts delete mode 100644 server/controllers/api/videos/view.ts (limited to 'server/controllers/api') diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts deleted file mode 100644 index d582f198d..000000000 --- a/server/controllers/api/abuse.ts +++ /dev/null @@ -1,259 +0,0 @@ -import express from 'express' -import { logger } from '@server/helpers/logger' -import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation' -import { Notifier } from '@server/lib/notifier' -import { AbuseModel } from '@server/models/abuse/abuse' -import { AbuseMessageModel } from '@server/models/abuse/abuse-message' -import { getServerActor } from '@server/models/application/application' -import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' -import { AbuseCreate, AbuseState, HttpStatusCode, UserRight } from '@shared/models' -import { getFormattedObjects } from '../../helpers/utils' -import { sequelizeTypescript } from '../../initializers/database' -import { - abuseGetValidator, - abuseListForAdminsValidator, - abuseReportValidator, - abusesSortValidator, - abuseUpdateValidator, - addAbuseMessageValidator, - apiRateLimiter, - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - checkAbuseValidForMessagesValidator, - deleteAbuseMessageValidator, - ensureUserHasRight, - getAbuseValidator, - openapiOperationDoc, - paginationValidator, - setDefaultPagination, - setDefaultSort -} from '../../middlewares' -import { AccountModel } from '../../models/account/account' - -const abuseRouter = express.Router() - -abuseRouter.use(apiRateLimiter) - -abuseRouter.get('/', - openapiOperationDoc({ operationId: 'getAbuses' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_ABUSES), - paginationValidator, - abusesSortValidator, - setDefaultSort, - setDefaultPagination, - abuseListForAdminsValidator, - asyncMiddleware(listAbusesForAdmins) -) -abuseRouter.put('/:id', - authenticate, - ensureUserHasRight(UserRight.MANAGE_ABUSES), - asyncMiddleware(abuseUpdateValidator), - asyncRetryTransactionMiddleware(updateAbuse) -) -abuseRouter.post('/', - authenticate, - asyncMiddleware(abuseReportValidator), - asyncRetryTransactionMiddleware(reportAbuse) -) -abuseRouter.delete('/:id', - authenticate, - ensureUserHasRight(UserRight.MANAGE_ABUSES), - asyncMiddleware(abuseGetValidator), - asyncRetryTransactionMiddleware(deleteAbuse) -) - -abuseRouter.get('/:id/messages', - authenticate, - asyncMiddleware(getAbuseValidator), - checkAbuseValidForMessagesValidator, - asyncRetryTransactionMiddleware(listAbuseMessages) -) - -abuseRouter.post('/:id/messages', - authenticate, - asyncMiddleware(getAbuseValidator), - checkAbuseValidForMessagesValidator, - addAbuseMessageValidator, - asyncRetryTransactionMiddleware(addAbuseMessage) -) - -abuseRouter.delete('/:id/messages/:messageId', - authenticate, - asyncMiddleware(getAbuseValidator), - checkAbuseValidForMessagesValidator, - asyncMiddleware(deleteAbuseMessageValidator), - asyncRetryTransactionMiddleware(deleteAbuseMessage) -) - -// --------------------------------------------------------------------------- - -export { - abuseRouter -} - -// --------------------------------------------------------------------------- - -async function listAbusesForAdmins (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.user - const serverActor = await getServerActor() - - const resultList = await AbuseModel.listForAdminApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - id: req.query.id, - filter: req.query.filter, - predefinedReason: req.query.predefinedReason, - search: req.query.search, - state: req.query.state, - videoIs: req.query.videoIs, - searchReporter: req.query.searchReporter, - searchReportee: req.query.searchReportee, - searchVideo: req.query.searchVideo, - searchVideoChannel: req.query.searchVideoChannel, - serverAccountId: serverActor.Account.id, - user - }) - - return res.json({ - total: resultList.total, - data: resultList.data.map(d => d.toFormattedAdminJSON()) - }) -} - -async function updateAbuse (req: express.Request, res: express.Response) { - const abuse = res.locals.abuse - let stateUpdated = false - - if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment - - if (req.body.state !== undefined) { - abuse.state = req.body.state - stateUpdated = true - } - - await sequelizeTypescript.transaction(t => { - return abuse.save({ transaction: t }) - }) - - if (stateUpdated === true) { - AbuseModel.loadFull(abuse.id) - .then(abuseFull => Notifier.Instance.notifyOnAbuseStateChange(abuseFull)) - .catch(err => logger.error('Cannot notify on abuse state change', { err })) - } - - // Do not send the delete to other instances, we updated OUR copy of this abuse - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function deleteAbuse (req: express.Request, res: express.Response) { - const abuse = res.locals.abuse - - await sequelizeTypescript.transaction(t => { - return abuse.destroy({ transaction: t }) - }) - - // Do not send the delete to other instances, we delete OUR copy of this abuse - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function reportAbuse (req: express.Request, res: express.Response) { - const videoInstance = res.locals.videoAll - const commentInstance = res.locals.videoCommentFull - const accountInstance = res.locals.account - - const body: AbuseCreate = req.body - - const { id } = await sequelizeTypescript.transaction(async t => { - const user = res.locals.oauth.token.User - // Don't send abuse notification if reporter is an admin/moderator - const skipNotification = user.hasRight(UserRight.MANAGE_ABUSES) - - const reporterAccount = await AccountModel.load(user.Account.id, t) - const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r]) - - const baseAbuse = { - reporterAccountId: reporterAccount.id, - reason: body.reason, - state: AbuseState.PENDING, - predefinedReasons - } - - if (body.video) { - return createVideoAbuse({ - baseAbuse, - videoInstance, - reporterAccount, - transaction: t, - startAt: body.video.startAt, - endAt: body.video.endAt, - skipNotification - }) - } - - if (body.comment) { - return createVideoCommentAbuse({ - baseAbuse, - commentInstance, - reporterAccount, - transaction: t, - skipNotification - }) - } - - // Account report - return createAccountAbuse({ - baseAbuse, - accountInstance, - reporterAccount, - transaction: t, - skipNotification - }) - }) - - return res.json({ abuse: { id } }) -} - -async function listAbuseMessages (req: express.Request, res: express.Response) { - const abuse = res.locals.abuse - - const resultList = await AbuseMessageModel.listForApi(abuse.id) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function addAbuseMessage (req: express.Request, res: express.Response) { - const abuse = res.locals.abuse - const user = res.locals.oauth.token.user - - const abuseMessage = await AbuseMessageModel.create({ - message: req.body.message, - byModerator: abuse.reporterAccountId !== user.Account.id, - accountId: user.Account.id, - abuseId: abuse.id - }) - - AbuseModel.loadFull(abuse.id) - .then(abuseFull => Notifier.Instance.notifyOnAbuseMessage(abuseFull, abuseMessage)) - .catch(err => logger.error('Cannot notify on new abuse message', { err })) - - return res.json({ - abuseMessage: { - id: abuseMessage.id - } - }) -} - -async function deleteAbuseMessage (req: express.Request, res: express.Response) { - const abuseMessage = res.locals.abuseMessage - - await sequelizeTypescript.transaction(t => { - return abuseMessage.destroy({ transaction: t }) - }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts deleted file mode 100644 index 49cd7559a..000000000 --- a/server/controllers/api/accounts.ts +++ /dev/null @@ -1,266 +0,0 @@ -import express from 'express' -import { pickCommonVideoQuery } from '@server/helpers/query' -import { ActorFollowModel } from '@server/models/actor/actor-follow' -import { getServerActor } from '@server/models/application/application' -import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' -import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' -import { getFormattedObjects } from '../../helpers/utils' -import { JobQueue } from '../../lib/job-queue' -import { Hooks } from '../../lib/plugins/hooks' -import { - apiRateLimiter, - asyncMiddleware, - authenticate, - commonVideosFiltersValidator, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort, - setDefaultVideosSort, - videoPlaylistsSortValidator, - videoRatesSortValidator, - videoRatingValidator -} from '../../middlewares' -import { - accountNameWithHostGetValidator, - accountsFollowersSortValidator, - accountsSortValidator, - ensureAuthUserOwnsAccountValidator, - ensureCanManageChannelOrAccount, - videoChannelsSortValidator, - videoChannelStatsValidator, - videoChannelSyncsSortValidator, - videosSortValidator -} from '../../middlewares/validators' -import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists' -import { AccountModel } from '../../models/account/account' -import { AccountVideoRateModel } from '../../models/account/account-video-rate' -import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter' -import { VideoModel } from '../../models/video/video' -import { VideoChannelModel } from '../../models/video/video-channel' -import { VideoPlaylistModel } from '../../models/video/video-playlist' - -const accountsRouter = express.Router() - -accountsRouter.use(apiRateLimiter) - -accountsRouter.get('/', - paginationValidator, - accountsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listAccounts) -) - -accountsRouter.get('/:accountName', - asyncMiddleware(accountNameWithHostGetValidator), - getAccount -) - -accountsRouter.get('/:accountName/videos', - asyncMiddleware(accountNameWithHostGetValidator), - paginationValidator, - videosSortValidator, - setDefaultVideosSort, - setDefaultPagination, - optionalAuthenticate, - commonVideosFiltersValidator, - asyncMiddleware(listAccountVideos) -) - -accountsRouter.get('/:accountName/video-channels', - asyncMiddleware(accountNameWithHostGetValidator), - videoChannelStatsValidator, - paginationValidator, - videoChannelsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listAccountChannels) -) - -accountsRouter.get('/:accountName/video-channel-syncs', - authenticate, - asyncMiddleware(accountNameWithHostGetValidator), - ensureCanManageChannelOrAccount, - paginationValidator, - videoChannelSyncsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listAccountChannelsSync) -) - -accountsRouter.get('/:accountName/video-playlists', - optionalAuthenticate, - asyncMiddleware(accountNameWithHostGetValidator), - paginationValidator, - videoPlaylistsSortValidator, - setDefaultSort, - setDefaultPagination, - commonVideoPlaylistFiltersValidator, - videoPlaylistsSearchValidator, - asyncMiddleware(listAccountPlaylists) -) - -accountsRouter.get('/:accountName/ratings', - authenticate, - asyncMiddleware(accountNameWithHostGetValidator), - ensureAuthUserOwnsAccountValidator, - paginationValidator, - videoRatesSortValidator, - setDefaultSort, - setDefaultPagination, - videoRatingValidator, - asyncMiddleware(listAccountRatings) -) - -accountsRouter.get('/:accountName/followers', - authenticate, - asyncMiddleware(accountNameWithHostGetValidator), - ensureAuthUserOwnsAccountValidator, - paginationValidator, - accountsFollowersSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listAccountFollowers) -) - -// --------------------------------------------------------------------------- - -export { - accountsRouter -} - -// --------------------------------------------------------------------------- - -function getAccount (req: express.Request, res: express.Response) { - const account = res.locals.account - - if (account.isOutdated()) { - JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } }) - } - - return res.json(account.toFormattedJSON()) -} - -async function listAccounts (req: express.Request, res: express.Response) { - const resultList = await AccountModel.listForApi(req.query.start, req.query.count, req.query.sort) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listAccountChannels (req: express.Request, res: express.Response) { - const options = { - accountId: res.locals.account.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - withStats: req.query.withStats, - search: req.query.search - } - - const resultList = await VideoChannelModel.listByAccountForAPI(options) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listAccountChannelsSync (req: express.Request, res: express.Response) { - const options = { - accountId: res.locals.account.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search - } - - const resultList = await VideoChannelSyncModel.listByAccountForAPI(options) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listAccountPlaylists (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - // Allow users to see their private/unlisted video playlists - let listMyPlaylists = false - if (res.locals.oauth && res.locals.oauth.token.User.Account.id === res.locals.account.id) { - listMyPlaylists = true - } - - const resultList = await VideoPlaylistModel.listForApi({ - search: req.query.search, - followerActorId: serverActor.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - accountId: res.locals.account.id, - listMyPlaylists, - type: req.query.playlistType - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listAccountVideos (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - const account = res.locals.account - - const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res) - ? null - : { - actorId: serverActor.id, - orLocalVideos: true - } - - const countVideos = getCountVideos(req) - const query = pickCommonVideoQuery(req.query) - - const apiOptions = await Hooks.wrapObject({ - ...query, - - displayOnlyForFollower, - nsfw: buildNSFWFilter(res, query.nsfw), - accountId: account.id, - user: res.locals.oauth ? res.locals.oauth.token.User : undefined, - countVideos - }, 'filter:api.accounts.videos.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoModel.listForApi, - apiOptions, - 'filter:api.accounts.videos.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) -} - -async function listAccountRatings (req: express.Request, res: express.Response) { - const account = res.locals.account - - const resultList = await AccountVideoRateModel.listByAccountForApi({ - accountId: account.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - type: req.query.rating - }) - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listAccountFollowers (req: express.Request, res: express.Response) { - const account = res.locals.account - - const channels = await VideoChannelModel.listAllByAccount(account.id) - const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId)) - - const resultList = await ActorFollowModel.listFollowersForApi({ - actorIds, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - state: 'accepted' - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} diff --git a/server/controllers/api/blocklist.ts b/server/controllers/api/blocklist.ts deleted file mode 100644 index dee12b108..000000000 --- a/server/controllers/api/blocklist.ts +++ /dev/null @@ -1,110 +0,0 @@ -import express from 'express' -import { handleToNameAndHost } from '@server/helpers/actors' -import { logger } from '@server/helpers/logger' -import { AccountBlocklistModel } from '@server/models/account/account-blocklist' -import { getServerActor } from '@server/models/application/application' -import { ServerBlocklistModel } from '@server/models/server/server-blocklist' -import { MActorAccountId, MUserAccountId } from '@server/types/models' -import { BlockStatus } from '@shared/models' -import { apiRateLimiter, asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares' - -const blocklistRouter = express.Router() - -blocklistRouter.use(apiRateLimiter) - -blocklistRouter.get('/status', - optionalAuthenticate, - blocklistStatusValidator, - asyncMiddleware(getBlocklistStatus) -) - -// --------------------------------------------------------------------------- - -export { - blocklistRouter -} - -// --------------------------------------------------------------------------- - -async function getBlocklistStatus (req: express.Request, res: express.Response) { - const hosts = req.query.hosts as string[] - const accounts = req.query.accounts as string[] - const user = res.locals.oauth?.token.User - - const serverActor = await getServerActor() - - const byAccountIds = [ serverActor.Account.id ] - if (user) byAccountIds.push(user.Account.id) - - const status: BlockStatus = { - accounts: {}, - hosts: {} - } - - const baseOptions = { - byAccountIds, - user, - serverActor, - status - } - - await Promise.all([ - populateServerBlocklistStatus({ ...baseOptions, hosts }), - populateAccountBlocklistStatus({ ...baseOptions, accounts }) - ]) - - return res.json(status) -} - -async function populateServerBlocklistStatus (options: { - byAccountIds: number[] - user?: MUserAccountId - serverActor: MActorAccountId - hosts: string[] - status: BlockStatus -}) { - const { byAccountIds, user, serverActor, hosts, status } = options - - if (!hosts || hosts.length === 0) return - - const serverBlocklistStatus = await ServerBlocklistModel.getBlockStatus(byAccountIds, hosts) - - logger.debug('Got server blocklist status.', { serverBlocklistStatus, byAccountIds, hosts }) - - for (const host of hosts) { - const block = serverBlocklistStatus.find(b => b.host === host) - - status.hosts[host] = getStatus(block, serverActor, user) - } -} - -async function populateAccountBlocklistStatus (options: { - byAccountIds: number[] - user?: MUserAccountId - serverActor: MActorAccountId - accounts: string[] - status: BlockStatus -}) { - const { byAccountIds, user, serverActor, accounts, status } = options - - if (!accounts || accounts.length === 0) return - - const accountBlocklistStatus = await AccountBlocklistModel.getBlockStatus(byAccountIds, accounts) - - logger.debug('Got account blocklist status.', { accountBlocklistStatus, byAccountIds, accounts }) - - for (const account of accounts) { - const sanitizedHandle = handleToNameAndHost(account) - - const block = accountBlocklistStatus.find(b => b.name === sanitizedHandle.name && b.host === sanitizedHandle.host) - - status.accounts[sanitizedHandle.handle] = getStatus(block, serverActor, user) - } -} - -function getStatus (block: { accountId: number }, serverActor: MActorAccountId, user?: MUserAccountId) { - return { - blockedByServer: !!(block && block.accountId === serverActor.Account.id), - blockedByUser: !!(block && user && block.accountId === user.Account.id) - } -} diff --git a/server/controllers/api/bulk.ts b/server/controllers/api/bulk.ts deleted file mode 100644 index c41c7d378..000000000 --- a/server/controllers/api/bulk.ts +++ /dev/null @@ -1,44 +0,0 @@ -import express from 'express' -import { removeComment } from '@server/lib/video-comment' -import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bulk' -import { VideoCommentModel } from '@server/models/video/video-comment' -import { HttpStatusCode } from '@shared/models' -import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model' -import { apiRateLimiter, asyncMiddleware, authenticate } from '../../middlewares' - -const bulkRouter = express.Router() - -bulkRouter.use(apiRateLimiter) - -bulkRouter.post('/remove-comments-of', - authenticate, - asyncMiddleware(bulkRemoveCommentsOfValidator), - asyncMiddleware(bulkRemoveCommentsOf) -) - -// --------------------------------------------------------------------------- - -export { - bulkRouter -} - -// --------------------------------------------------------------------------- - -async function bulkRemoveCommentsOf (req: express.Request, res: express.Response) { - const account = res.locals.account - const body = req.body as BulkRemoveCommentsOfBody - const user = res.locals.oauth.token.User - - const filter = body.scope === 'my-videos' - ? { onVideosOfAccount: user.Account } - : {} - - const comments = await VideoCommentModel.listForBulkDelete(account, filter) - - // Don't wait result - res.status(HttpStatusCode.NO_CONTENT_204).end() - - for (const comment of comments) { - await removeComment(comment, req, res) - } -} diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts deleted file mode 100644 index c5c4c8a74..000000000 --- a/server/controllers/api/config.ts +++ /dev/null @@ -1,377 +0,0 @@ -import express from 'express' -import { remove, writeJSON } from 'fs-extra' -import { snakeCase } from 'lodash' -import validator from 'validator' -import { ServerConfigManager } from '@server/lib/server-config-manager' -import { About, CustomConfig, UserRight } from '@shared/models' -import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' -import { objectConverter } from '../../helpers/core-utils' -import { CONFIG, reloadConfig } from '../../initializers/config' -import { ClientHtml } from '../../lib/client-html' -import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight, openapiOperationDoc } from '../../middlewares' -import { customConfigUpdateValidator, ensureConfigIsEditable } from '../../middlewares/validators/config' - -const configRouter = express.Router() - -configRouter.use(apiRateLimiter) - -const auditLogger = auditLoggerFactory('config') - -configRouter.get('/', - openapiOperationDoc({ operationId: 'getConfig' }), - asyncMiddleware(getConfig) -) - -configRouter.get('/about', - openapiOperationDoc({ operationId: 'getAbout' }), - getAbout -) - -configRouter.get('/custom', - openapiOperationDoc({ operationId: 'getCustomConfig' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), - getCustomConfig -) - -configRouter.put('/custom', - openapiOperationDoc({ operationId: 'putCustomConfig' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), - ensureConfigIsEditable, - customConfigUpdateValidator, - asyncMiddleware(updateCustomConfig) -) - -configRouter.delete('/custom', - openapiOperationDoc({ operationId: 'delCustomConfig' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), - ensureConfigIsEditable, - asyncMiddleware(deleteCustomConfig) -) - -async function getConfig (req: express.Request, res: express.Response) { - const json = await ServerConfigManager.Instance.getServerConfig(req.ip) - - return res.json(json) -} - -function getAbout (req: express.Request, res: express.Response) { - const about: About = { - instance: { - name: CONFIG.INSTANCE.NAME, - shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, - description: CONFIG.INSTANCE.DESCRIPTION, - terms: CONFIG.INSTANCE.TERMS, - codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT, - - hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION, - - creationReason: CONFIG.INSTANCE.CREATION_REASON, - moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION, - administrator: CONFIG.INSTANCE.ADMINISTRATOR, - maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME, - businessModel: CONFIG.INSTANCE.BUSINESS_MODEL, - - languages: CONFIG.INSTANCE.LANGUAGES, - categories: CONFIG.INSTANCE.CATEGORIES - } - } - - return res.json(about) -} - -function getCustomConfig (req: express.Request, res: express.Response) { - const data = customConfig() - - return res.json(data) -} - -async function deleteCustomConfig (req: express.Request, res: express.Response) { - await remove(CONFIG.CUSTOM_FILE) - - auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig())) - - reloadConfig() - ClientHtml.invalidCache() - - const data = customConfig() - - return res.json(data) -} - -async function updateCustomConfig (req: express.Request, res: express.Response) { - const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) - - // camelCase to snake_case key + Force number conversion - const toUpdateJSON = convertCustomConfigBody(req.body) - - await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) - - reloadConfig() - ClientHtml.invalidCache() - - const data = customConfig() - - auditLogger.update( - getAuditIdFromRes(res), - new CustomConfigAuditView(data), - oldCustomConfigAuditKeys - ) - - return res.json(data) -} - -// --------------------------------------------------------------------------- - -export { - configRouter -} - -// --------------------------------------------------------------------------- - -function customConfig (): CustomConfig { - return { - instance: { - name: CONFIG.INSTANCE.NAME, - shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, - description: CONFIG.INSTANCE.DESCRIPTION, - terms: CONFIG.INSTANCE.TERMS, - codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT, - - creationReason: CONFIG.INSTANCE.CREATION_REASON, - moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION, - administrator: CONFIG.INSTANCE.ADMINISTRATOR, - maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME, - businessModel: CONFIG.INSTANCE.BUSINESS_MODEL, - hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION, - - languages: CONFIG.INSTANCE.LANGUAGES, - categories: CONFIG.INSTANCE.CATEGORIES, - - isNSFW: CONFIG.INSTANCE.IS_NSFW, - defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, - - defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, - - customizations: { - css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS, - javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT - } - }, - theme: { - default: CONFIG.THEME.DEFAULT - }, - services: { - twitter: { - username: CONFIG.SERVICES.TWITTER.USERNAME, - whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED - } - }, - client: { - videos: { - miniature: { - preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME - } - }, - menu: { - login: { - redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH - } - } - }, - cache: { - previews: { - size: CONFIG.CACHE.PREVIEWS.SIZE - }, - captions: { - size: CONFIG.CACHE.VIDEO_CAPTIONS.SIZE - }, - torrents: { - size: CONFIG.CACHE.TORRENTS.SIZE - }, - storyboards: { - size: CONFIG.CACHE.STORYBOARDS.SIZE - } - }, - signup: { - enabled: CONFIG.SIGNUP.ENABLED, - limit: CONFIG.SIGNUP.LIMIT, - requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL, - requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION, - minimumAge: CONFIG.SIGNUP.MINIMUM_AGE - }, - admin: { - email: CONFIG.ADMIN.EMAIL - }, - contactForm: { - enabled: CONFIG.CONTACT_FORM.ENABLED - }, - user: { - history: { - videos: { - enabled: CONFIG.USER.HISTORY.VIDEOS.ENABLED - } - }, - videoQuota: CONFIG.USER.VIDEO_QUOTA, - videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY - }, - videoChannels: { - maxPerUser: CONFIG.VIDEO_CHANNELS.MAX_PER_USER - }, - transcoding: { - enabled: CONFIG.TRANSCODING.ENABLED, - remoteRunners: { - enabled: CONFIG.TRANSCODING.REMOTE_RUNNERS.ENABLED - }, - allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS, - allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, - threads: CONFIG.TRANSCODING.THREADS, - concurrency: CONFIG.TRANSCODING.CONCURRENCY, - profile: CONFIG.TRANSCODING.PROFILE, - resolutions: { - '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'], - '144p': CONFIG.TRANSCODING.RESOLUTIONS['144p'], - '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'], - '360p': CONFIG.TRANSCODING.RESOLUTIONS['360p'], - '480p': CONFIG.TRANSCODING.RESOLUTIONS['480p'], - '720p': CONFIG.TRANSCODING.RESOLUTIONS['720p'], - '1080p': CONFIG.TRANSCODING.RESOLUTIONS['1080p'], - '1440p': CONFIG.TRANSCODING.RESOLUTIONS['1440p'], - '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p'] - }, - alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION, - webVideos: { - enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED - }, - hls: { - enabled: CONFIG.TRANSCODING.HLS.ENABLED - } - }, - live: { - enabled: CONFIG.LIVE.ENABLED, - allowReplay: CONFIG.LIVE.ALLOW_REPLAY, - latencySetting: { - enabled: CONFIG.LIVE.LATENCY_SETTING.ENABLED - }, - maxDuration: CONFIG.LIVE.MAX_DURATION, - maxInstanceLives: CONFIG.LIVE.MAX_INSTANCE_LIVES, - maxUserLives: CONFIG.LIVE.MAX_USER_LIVES, - transcoding: { - enabled: CONFIG.LIVE.TRANSCODING.ENABLED, - remoteRunners: { - enabled: CONFIG.LIVE.TRANSCODING.REMOTE_RUNNERS.ENABLED - }, - threads: CONFIG.LIVE.TRANSCODING.THREADS, - profile: CONFIG.LIVE.TRANSCODING.PROFILE, - resolutions: { - '144p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['144p'], - '240p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['240p'], - '360p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['360p'], - '480p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['480p'], - '720p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['720p'], - '1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'], - '1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'], - '2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p'] - }, - alwaysTranscodeOriginalResolution: CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION - } - }, - videoStudio: { - enabled: CONFIG.VIDEO_STUDIO.ENABLED, - remoteRunners: { - enabled: CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED - } - }, - videoFile: { - update: { - enabled: CONFIG.VIDEO_FILE.UPDATE.ENABLED - } - }, - import: { - videos: { - concurrency: CONFIG.IMPORT.VIDEOS.CONCURRENCY, - http: { - enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED - }, - torrent: { - enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED - } - }, - videoChannelSynchronization: { - enabled: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED, - maxPerUser: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER - } - }, - trending: { - videos: { - algorithms: { - enabled: CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED, - default: CONFIG.TRENDING.VIDEOS.ALGORITHMS.DEFAULT - } - } - }, - autoBlacklist: { - videos: { - ofUsers: { - enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED - } - } - }, - followers: { - instance: { - enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED, - manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL - } - }, - followings: { - instance: { - autoFollowBack: { - enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED - }, - - autoFollowIndex: { - enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED, - indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL - } - } - }, - broadcastMessage: { - enabled: CONFIG.BROADCAST_MESSAGE.ENABLED, - message: CONFIG.BROADCAST_MESSAGE.MESSAGE, - level: CONFIG.BROADCAST_MESSAGE.LEVEL, - dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE - }, - search: { - remoteUri: { - users: CONFIG.SEARCH.REMOTE_URI.USERS, - anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS - }, - searchIndex: { - enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED, - url: CONFIG.SEARCH.SEARCH_INDEX.URL, - disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH, - isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH - } - } - } -} - -function convertCustomConfigBody (body: CustomConfig) { - function keyConverter (k: string) { - // Transcoding resolutions exception - if (/^\d{3,4}p$/.exec(k)) return k - if (k === '0p') return k - - return snakeCase(k) - } - - function valueConverter (v: any) { - if (validator.isNumeric(v + '')) return parseInt('' + v, 10) - - return v - } - - return objectConverter(body, keyConverter, valueConverter) -} diff --git a/server/controllers/api/custom-page.ts b/server/controllers/api/custom-page.ts deleted file mode 100644 index f4e1a0e79..000000000 --- a/server/controllers/api/custom-page.ts +++ /dev/null @@ -1,48 +0,0 @@ -import express from 'express' -import { ServerConfigManager } from '@server/lib/server-config-manager' -import { ActorCustomPageModel } from '@server/models/account/actor-custom-page' -import { HttpStatusCode, UserRight } from '@shared/models' -import { apiRateLimiter, asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' - -const customPageRouter = express.Router() - -customPageRouter.use(apiRateLimiter) - -customPageRouter.get('/homepage/instance', - asyncMiddleware(getInstanceHomepage) -) - -customPageRouter.put('/homepage/instance', - authenticate, - ensureUserHasRight(UserRight.MANAGE_INSTANCE_CUSTOM_PAGE), - asyncMiddleware(updateInstanceHomepage) -) - -// --------------------------------------------------------------------------- - -export { - customPageRouter -} - -// --------------------------------------------------------------------------- - -async function getInstanceHomepage (req: express.Request, res: express.Response) { - const page = await ActorCustomPageModel.loadInstanceHomepage() - if (!page) { - return res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'Instance homepage could not be found' - }) - } - - return res.json(page.toFormattedJSON()) -} - -async function updateInstanceHomepage (req: express.Request, res: express.Response) { - const content = req.body.content - - await ActorCustomPageModel.updateInstanceHomepage(content) - ServerConfigManager.Instance.updateHomepageState(content) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts deleted file mode 100644 index 38bd135d0..000000000 --- a/server/controllers/api/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { logger } from '@server/helpers/logger' -import { HttpStatusCode } from '../../../shared/models' -import { abuseRouter } from './abuse' -import { accountsRouter } from './accounts' -import { blocklistRouter } from './blocklist' -import { bulkRouter } from './bulk' -import { configRouter } from './config' -import { customPageRouter } from './custom-page' -import { jobsRouter } from './jobs' -import { metricsRouter } from './metrics' -import { oauthClientsRouter } from './oauth-clients' -import { overviewsRouter } from './overviews' -import { pluginRouter } from './plugins' -import { runnersRouter } from './runners' -import { searchRouter } from './search' -import { serverRouter } from './server' -import { usersRouter } from './users' -import { videoChannelRouter } from './video-channel' -import { videoChannelSyncRouter } from './video-channel-sync' -import { videoPlaylistRouter } from './video-playlist' -import { videosRouter } from './videos' - -const apiRouter = express.Router() - -apiRouter.use(cors({ - origin: '*', - exposedHeaders: 'Retry-After', - credentials: true -})) - -apiRouter.use('/server', serverRouter) -apiRouter.use('/abuses', abuseRouter) -apiRouter.use('/bulk', bulkRouter) -apiRouter.use('/oauth-clients', oauthClientsRouter) -apiRouter.use('/config', configRouter) -apiRouter.use('/users', usersRouter) -apiRouter.use('/accounts', accountsRouter) -apiRouter.use('/video-channels', videoChannelRouter) -apiRouter.use('/video-channel-syncs', videoChannelSyncRouter) -apiRouter.use('/video-playlists', videoPlaylistRouter) -apiRouter.use('/videos', videosRouter) -apiRouter.use('/jobs', jobsRouter) -apiRouter.use('/metrics', metricsRouter) -apiRouter.use('/search', searchRouter) -apiRouter.use('/overviews', overviewsRouter) -apiRouter.use('/plugins', pluginRouter) -apiRouter.use('/custom-pages', customPageRouter) -apiRouter.use('/blocklist', blocklistRouter) -apiRouter.use('/runners', runnersRouter) - -// apiRouter.use(apiRateLimiter) -apiRouter.use('/ping', pong) -apiRouter.use('/*', badRequest) - -// --------------------------------------------------------------------------- - -export { apiRouter } - -// --------------------------------------------------------------------------- - -function pong (req: express.Request, res: express.Response) { - return res.send('pong').status(HttpStatusCode.OK_200).end() -} - -function badRequest (req: express.Request, res: express.Response) { - logger.debug(`API express handler not found: bad PeerTube request for ${req.method} - ${req.originalUrl}`) - - return res.type('json') - .status(HttpStatusCode.BAD_REQUEST_400) - .end() -} diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts deleted file mode 100644 index c701bc970..000000000 --- a/server/controllers/api/jobs.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Job as BullJob } from 'bullmq' -import express from 'express' -import { HttpStatusCode, Job, JobState, JobType, ResultList, UserRight } from '@shared/models' -import { isArray } from '../../helpers/custom-validators/misc' -import { JobQueue } from '../../lib/job-queue' -import { - apiRateLimiter, - asyncMiddleware, - authenticate, - ensureUserHasRight, - jobsSortValidator, - openapiOperationDoc, - paginationValidatorBuilder, - setDefaultPagination, - setDefaultSort -} from '../../middlewares' -import { listJobsValidator } from '../../middlewares/validators/jobs' - -const jobsRouter = express.Router() - -jobsRouter.use(apiRateLimiter) - -jobsRouter.post('/pause', - authenticate, - ensureUserHasRight(UserRight.MANAGE_JOBS), - asyncMiddleware(pauseJobQueue) -) - -jobsRouter.post('/resume', - authenticate, - ensureUserHasRight(UserRight.MANAGE_JOBS), - resumeJobQueue -) - -jobsRouter.get('/:state?', - openapiOperationDoc({ operationId: 'getJobs' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_JOBS), - paginationValidatorBuilder([ 'jobs' ]), - jobsSortValidator, - setDefaultSort, - setDefaultPagination, - listJobsValidator, - asyncMiddleware(listJobs) -) - -// --------------------------------------------------------------------------- - -export { - jobsRouter -} - -// --------------------------------------------------------------------------- - -async function pauseJobQueue (req: express.Request, res: express.Response) { - await JobQueue.Instance.pause() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -function resumeJobQueue (req: express.Request, res: express.Response) { - JobQueue.Instance.resume() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function listJobs (req: express.Request, res: express.Response) { - const state = req.params.state as JobState - const asc = req.query.sort === 'createdAt' - const jobType = req.query.jobType - - const jobs = await JobQueue.Instance.listForApi({ - state, - start: req.query.start, - count: req.query.count, - asc, - jobType - }) - const total = await JobQueue.Instance.count(state, jobType) - - const result: ResultList = { - total, - data: await Promise.all(jobs.map(j => formatJob(j, state))) - } - - return res.json(result) -} - -async function formatJob (job: BullJob, state?: JobState): Promise { - const error = isArray(job.stacktrace) && job.stacktrace.length !== 0 - ? job.stacktrace[0] - : null - - return { - id: job.id, - state: state || await job.getState(), - type: job.queueName as JobType, - data: job.data, - parent: job.parent - ? { id: job.parent.id } - : undefined, - progress: job.progress as number, - priority: job.opts.priority, - error, - createdAt: new Date(job.timestamp), - finishedOn: new Date(job.finishedOn), - processedOn: new Date(job.processedOn) - } -} diff --git a/server/controllers/api/metrics.ts b/server/controllers/api/metrics.ts deleted file mode 100644 index 909963fa7..000000000 --- a/server/controllers/api/metrics.ts +++ /dev/null @@ -1,34 +0,0 @@ -import express from 'express' -import { CONFIG } from '@server/initializers/config' -import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics' -import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models' -import { addPlaybackMetricValidator, apiRateLimiter, asyncMiddleware } from '../../middlewares' - -const metricsRouter = express.Router() - -metricsRouter.use(apiRateLimiter) - -metricsRouter.post('/playback', - asyncMiddleware(addPlaybackMetricValidator), - addPlaybackMetric -) - -// --------------------------------------------------------------------------- - -export { - metricsRouter -} - -// --------------------------------------------------------------------------- - -function addPlaybackMetric (req: express.Request, res: express.Response) { - if (!CONFIG.OPEN_TELEMETRY.METRICS.ENABLED) { - return res.sendStatus(HttpStatusCode.FORBIDDEN_403) - } - - const body: PlaybackMetricCreate = req.body - - OpenTelemetryMetrics.Instance.observePlaybackMetric(res.locals.onlyImmutableVideo, body) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} diff --git a/server/controllers/api/oauth-clients.ts b/server/controllers/api/oauth-clients.ts deleted file mode 100644 index 1899dbb02..000000000 --- a/server/controllers/api/oauth-clients.ts +++ /dev/null @@ -1,54 +0,0 @@ -import express from 'express' -import { isTestOrDevInstance } from '@server/helpers/core-utils' -import { OAuthClientModel } from '@server/models/oauth/oauth-client' -import { HttpStatusCode, OAuthClientLocal } from '@shared/models' -import { logger } from '../../helpers/logger' -import { CONFIG } from '../../initializers/config' -import { apiRateLimiter, asyncMiddleware, openapiOperationDoc } from '../../middlewares' - -const oauthClientsRouter = express.Router() - -oauthClientsRouter.use(apiRateLimiter) - -oauthClientsRouter.get('/local', - openapiOperationDoc({ operationId: 'getOAuthClient' }), - asyncMiddleware(getLocalClient) -) - -// Get the client credentials for the PeerTube front end -async function getLocalClient (req: express.Request, res: express.Response, next: express.NextFunction) { - const serverHostname = CONFIG.WEBSERVER.HOSTNAME - const serverPort = CONFIG.WEBSERVER.PORT - let headerHostShouldBe = serverHostname - if (serverPort !== 80 && serverPort !== 443) { - headerHostShouldBe += ':' + serverPort - } - - // Don't make this check if this is a test instance - if (!isTestOrDevInstance() && req.get('host') !== headerHostShouldBe) { - logger.info( - 'Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe, - { webserverConfig: CONFIG.WEBSERVER } - ) - - return res.fail({ - status: HttpStatusCode.FORBIDDEN_403, - message: `Getting client tokens for host ${req.get('host')} is forbidden` - }) - } - - const client = await OAuthClientModel.loadFirstClient() - if (!client) throw new Error('No client available.') - - const json: OAuthClientLocal = { - client_id: client.clientId, - client_secret: client.clientSecret - } - return res.json(json) -} - -// --------------------------------------------------------------------------- - -export { - oauthClientsRouter -} diff --git a/server/controllers/api/overviews.ts b/server/controllers/api/overviews.ts deleted file mode 100644 index fc616281e..000000000 --- a/server/controllers/api/overviews.ts +++ /dev/null @@ -1,139 +0,0 @@ -import express from 'express' -import memoizee from 'memoizee' -import { logger } from '@server/helpers/logger' -import { Hooks } from '@server/lib/plugins/hooks' -import { getServerActor } from '@server/models/application/application' -import { VideoModel } from '@server/models/video/video' -import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '../../../shared/models/overviews' -import { buildNSFWFilter } from '../../helpers/express-utils' -import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants' -import { apiRateLimiter, asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares' -import { TagModel } from '../../models/video/tag' - -const overviewsRouter = express.Router() - -overviewsRouter.use(apiRateLimiter) - -overviewsRouter.get('/videos', - videosOverviewValidator, - optionalAuthenticate, - asyncMiddleware(getVideosOverview) -) - -// --------------------------------------------------------------------------- - -export { overviewsRouter } - -// --------------------------------------------------------------------------- - -const buildSamples = memoizee(async function () { - const [ categories, channels, tags ] = await Promise.all([ - VideoModel.getRandomFieldSamples('category', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT), - VideoModel.getRandomFieldSamples('channelId', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT), - TagModel.getRandomSamples(OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT) - ]) - - const result = { categories, channels, tags } - - logger.debug('Building samples for overview endpoint.', { result }) - - return result -}, { maxAge: MEMOIZE_TTL.OVERVIEWS_SAMPLE }) - -// This endpoint could be quite long, but we cache it -async function getVideosOverview (req: express.Request, res: express.Response) { - const attributes = await buildSamples() - - const page = req.query.page || 1 - const index = page - 1 - - const categories: CategoryOverview[] = [] - const channels: ChannelOverview[] = [] - const tags: TagOverview[] = [] - - await Promise.all([ - getVideosByCategory(attributes.categories, index, res, categories), - getVideosByChannel(attributes.channels, index, res, channels), - getVideosByTag(attributes.tags, index, res, tags) - ]) - - const result: VideosOverview = { - categories, - channels, - tags - } - - return res.json(result) -} - -async function getVideosByTag (tagsSample: string[], index: number, res: express.Response, acc: TagOverview[]) { - if (tagsSample.length <= index) return - - const tag = tagsSample[index] - const videos = await getVideos(res, { tagsOneOf: [ tag ] }) - - if (videos.length === 0) return - - acc.push({ - tag, - videos - }) -} - -async function getVideosByCategory (categoriesSample: number[], index: number, res: express.Response, acc: CategoryOverview[]) { - if (categoriesSample.length <= index) return - - const category = categoriesSample[index] - const videos = await getVideos(res, { categoryOneOf: [ category ] }) - - if (videos.length === 0) return - - acc.push({ - category: videos[0].category, - videos - }) -} - -async function getVideosByChannel (channelsSample: number[], index: number, res: express.Response, acc: ChannelOverview[]) { - if (channelsSample.length <= index) return - - const channelId = channelsSample[index] - const videos = await getVideos(res, { videoChannelId: channelId }) - - if (videos.length === 0) return - - acc.push({ - channel: videos[0].channel, - videos - }) -} - -async function getVideos ( - res: express.Response, - where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] } -) { - const serverActor = await getServerActor() - - const query = await Hooks.wrapObject({ - start: 0, - count: 12, - sort: '-createdAt', - displayOnlyForFollower: { - actorId: serverActor.id, - orLocalVideos: true - }, - nsfw: buildNSFWFilter(res), - user: res.locals.oauth ? res.locals.oauth.token.User : undefined, - countVideos: false, - - ...where - }, 'filter:api.overviews.videos.list.params') - - const { data } = await Hooks.wrapPromiseFun( - VideoModel.listForApi, - query, - 'filter:api.overviews.videos.list.result' - ) - - return data.map(d => d.toFormattedJSON()) -} diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts deleted file mode 100644 index 337b72b2f..000000000 --- a/server/controllers/api/plugins.ts +++ /dev/null @@ -1,230 +0,0 @@ -import express from 'express' -import { logger } from '@server/helpers/logger' -import { getFormattedObjects } from '@server/helpers/utils' -import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index' -import { PluginManager } from '@server/lib/plugins/plugin-manager' -import { - apiRateLimiter, - asyncMiddleware, - authenticate, - availablePluginsSortValidator, - ensureUserHasRight, - openapiOperationDoc, - paginationValidator, - pluginsSortValidator, - setDefaultPagination, - setDefaultSort -} from '@server/middlewares' -import { - existingPluginValidator, - installOrUpdatePluginValidator, - listAvailablePluginsValidator, - listPluginsValidator, - uninstallPluginValidator, - updatePluginSettingsValidator -} from '@server/middlewares/validators/plugins' -import { PluginModel } from '@server/models/server/plugin' -import { - HttpStatusCode, - InstallOrUpdatePlugin, - ManagePlugin, - PeertubePluginIndexList, - PublicServerSetting, - RegisteredServerSettings, - UserRight -} from '@shared/models' - -const pluginRouter = express.Router() - -pluginRouter.use(apiRateLimiter) - -pluginRouter.get('/available', - openapiOperationDoc({ operationId: 'getAvailablePlugins' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - listAvailablePluginsValidator, - paginationValidator, - availablePluginsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listAvailablePlugins) -) - -pluginRouter.get('/', - openapiOperationDoc({ operationId: 'getPlugins' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - listPluginsValidator, - paginationValidator, - pluginsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listPlugins) -) - -pluginRouter.get('/:npmName/registered-settings', - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - asyncMiddleware(existingPluginValidator), - getPluginRegisteredSettings -) - -pluginRouter.get('/:npmName/public-settings', - asyncMiddleware(existingPluginValidator), - getPublicPluginSettings -) - -pluginRouter.put('/:npmName/settings', - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - updatePluginSettingsValidator, - asyncMiddleware(existingPluginValidator), - asyncMiddleware(updatePluginSettings) -) - -pluginRouter.get('/:npmName', - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - asyncMiddleware(existingPluginValidator), - getPlugin -) - -pluginRouter.post('/install', - openapiOperationDoc({ operationId: 'addPlugin' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - installOrUpdatePluginValidator, - asyncMiddleware(installPlugin) -) - -pluginRouter.post('/update', - openapiOperationDoc({ operationId: 'updatePlugin' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - installOrUpdatePluginValidator, - asyncMiddleware(updatePlugin) -) - -pluginRouter.post('/uninstall', - openapiOperationDoc({ operationId: 'uninstallPlugin' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_PLUGINS), - uninstallPluginValidator, - asyncMiddleware(uninstallPlugin) -) - -// --------------------------------------------------------------------------- - -export { - pluginRouter -} - -// --------------------------------------------------------------------------- - -async function listPlugins (req: express.Request, res: express.Response) { - const pluginType = req.query.pluginType - const uninstalled = req.query.uninstalled - - const resultList = await PluginModel.listForApi({ - pluginType, - uninstalled, - start: req.query.start, - count: req.query.count, - sort: req.query.sort - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -function getPlugin (req: express.Request, res: express.Response) { - const plugin = res.locals.plugin - - return res.json(plugin.toFormattedJSON()) -} - -async function installPlugin (req: express.Request, res: express.Response) { - const body: InstallOrUpdatePlugin = req.body - - const fromDisk = !!body.path - const toInstall = body.npmName || body.path - - const pluginVersion = body.pluginVersion && body.npmName - ? body.pluginVersion - : undefined - - try { - const plugin = await PluginManager.Instance.install({ toInstall, version: pluginVersion, fromDisk }) - - return res.json(plugin.toFormattedJSON()) - } catch (err) { - logger.warn('Cannot install plugin %s.', toInstall, { err }) - return res.fail({ message: 'Cannot install plugin ' + toInstall }) - } -} - -async function updatePlugin (req: express.Request, res: express.Response) { - const body: InstallOrUpdatePlugin = req.body - - const fromDisk = !!body.path - const toUpdate = body.npmName || body.path - try { - const plugin = await PluginManager.Instance.update(toUpdate, fromDisk) - - return res.json(plugin.toFormattedJSON()) - } catch (err) { - logger.warn('Cannot update plugin %s.', toUpdate, { err }) - return res.fail({ message: 'Cannot update plugin ' + toUpdate }) - } -} - -async function uninstallPlugin (req: express.Request, res: express.Response) { - const body: ManagePlugin = req.body - - await PluginManager.Instance.uninstall({ npmName: body.npmName }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -function getPublicPluginSettings (req: express.Request, res: express.Response) { - const plugin = res.locals.plugin - const registeredSettings = PluginManager.Instance.getRegisteredSettings(req.params.npmName) - const publicSettings = plugin.getPublicSettings(registeredSettings) - - const json: PublicServerSetting = { publicSettings } - - return res.json(json) -} - -function getPluginRegisteredSettings (req: express.Request, res: express.Response) { - const registeredSettings = PluginManager.Instance.getRegisteredSettings(req.params.npmName) - - const json: RegisteredServerSettings = { registeredSettings } - - return res.json(json) -} - -async function updatePluginSettings (req: express.Request, res: express.Response) { - const plugin = res.locals.plugin - - plugin.settings = req.body.settings - await plugin.save() - - await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function listAvailablePlugins (req: express.Request, res: express.Response) { - const query: PeertubePluginIndexList = req.query - - const resultList = await listAvailablePluginsFromIndex(query) - - if (!resultList) { - return res.fail({ - status: HttpStatusCode.SERVICE_UNAVAILABLE_503, - message: 'Plugin index unavailable. Please retry later' - }) - } - - return res.json(resultList) -} diff --git a/server/controllers/api/runners/index.ts b/server/controllers/api/runners/index.ts deleted file mode 100644 index 9998fe4cc..000000000 --- a/server/controllers/api/runners/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import express from 'express' -import { runnerJobsRouter } from './jobs' -import { runnerJobFilesRouter } from './jobs-files' -import { manageRunnersRouter } from './manage-runners' -import { runnerRegistrationTokensRouter } from './registration-tokens' - -const runnersRouter = express.Router() - -// No api route limiter here, they are defined in child routers - -runnersRouter.use('/', manageRunnersRouter) -runnersRouter.use('/', runnerJobsRouter) -runnersRouter.use('/', runnerJobFilesRouter) -runnersRouter.use('/', runnerRegistrationTokensRouter) - -// --------------------------------------------------------------------------- - -export { - runnersRouter -} diff --git a/server/controllers/api/runners/jobs-files.ts b/server/controllers/api/runners/jobs-files.ts deleted file mode 100644 index d28f43701..000000000 --- a/server/controllers/api/runners/jobs-files.ts +++ /dev/null @@ -1,112 +0,0 @@ -import express from 'express' -import { logger, loggerTagsFactory } from '@server/helpers/logger' -import { proxifyHLS, proxifyWebVideoFile } from '@server/lib/object-storage' -import { VideoPathManager } from '@server/lib/video-path-manager' -import { getStudioTaskFilePath } from '@server/lib/video-studio' -import { apiRateLimiter, asyncMiddleware } from '@server/middlewares' -import { jobOfRunnerGetValidatorFactory } from '@server/middlewares/validators/runners' -import { - runnerJobGetVideoStudioTaskFileValidator, - runnerJobGetVideoTranscodingFileValidator -} from '@server/middlewares/validators/runners/job-files' -import { RunnerJobState, VideoStorage } from '@shared/models' - -const lTags = loggerTagsFactory('api', 'runner') - -const runnerJobFilesRouter = express.Router() - -runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/max-quality', - apiRateLimiter, - asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), - asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), - asyncMiddleware(getMaxQualityVideoFile) -) - -runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/previews/max-quality', - apiRateLimiter, - asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), - asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), - getMaxQualityVideoPreview -) - -runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/studio/task-files/:filename', - apiRateLimiter, - asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), - asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), - runnerJobGetVideoStudioTaskFileValidator, - getVideoStudioTaskFile -) - -// --------------------------------------------------------------------------- - -export { - runnerJobFilesRouter -} - -// --------------------------------------------------------------------------- - -async function getMaxQualityVideoFile (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - const runner = runnerJob.Runner - const video = res.locals.videoAll - - logger.info( - 'Get max quality file of video %s of job %s for runner %s', video.uuid, runnerJob.uuid, runner.name, - lTags(runner.name, runnerJob.id, runnerJob.type) - ) - - const file = video.getMaxQualityFile() - - if (file.storage === VideoStorage.OBJECT_STORAGE) { - if (file.isHLS()) { - return proxifyHLS({ - req, - res, - filename: file.filename, - playlist: video.getHLSPlaylist(), - reinjectVideoFileToken: false, - video - }) - } - - // Web video - return proxifyWebVideoFile({ - req, - res, - filename: file.filename - }) - } - - return VideoPathManager.Instance.makeAvailableVideoFile(file, videoPath => { - return res.sendFile(videoPath) - }) -} - -function getMaxQualityVideoPreview (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - const runner = runnerJob.Runner - const video = res.locals.videoAll - - logger.info( - 'Get max quality preview file of video %s of job %s for runner %s', video.uuid, runnerJob.uuid, runner.name, - lTags(runner.name, runnerJob.id, runnerJob.type) - ) - - const file = video.getPreview() - - return res.sendFile(file.getPath()) -} - -function getVideoStudioTaskFile (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - const runner = runnerJob.Runner - const video = res.locals.videoAll - const filename = req.params.filename - - logger.info( - 'Get video studio task file %s of video %s of job %s for runner %s', filename, video.uuid, runnerJob.uuid, runner.name, - lTags(runner.name, runnerJob.id, runnerJob.type) - ) - - return res.sendFile(getStudioTaskFilePath(filename)) -} diff --git a/server/controllers/api/runners/jobs.ts b/server/controllers/api/runners/jobs.ts deleted file mode 100644 index e9e2ddf49..000000000 --- a/server/controllers/api/runners/jobs.ts +++ /dev/null @@ -1,416 +0,0 @@ -import express, { UploadFiles } from 'express' -import { retryTransactionWrapper } from '@server/helpers/database-utils' -import { createReqFiles } from '@server/helpers/express-utils' -import { logger, loggerTagsFactory } from '@server/helpers/logger' -import { generateRunnerJobToken } from '@server/helpers/token-generator' -import { MIMETYPES } from '@server/initializers/constants' -import { sequelizeTypescript } from '@server/initializers/database' -import { getRunnerJobHandlerClass, runnerJobCanBeCancelled, updateLastRunnerContact } from '@server/lib/runners' -import { - apiRateLimiter, - asyncMiddleware, - authenticate, - ensureUserHasRight, - paginationValidator, - runnerJobsSortValidator, - setDefaultPagination, - setDefaultSort -} from '@server/middlewares' -import { - abortRunnerJobValidator, - acceptRunnerJobValidator, - cancelRunnerJobValidator, - errorRunnerJobValidator, - getRunnerFromTokenValidator, - jobOfRunnerGetValidatorFactory, - listRunnerJobsValidator, - runnerJobGetValidator, - successRunnerJobValidator, - updateRunnerJobValidator -} from '@server/middlewares/validators/runners' -import { RunnerModel } from '@server/models/runner/runner' -import { RunnerJobModel } from '@server/models/runner/runner-job' -import { - AbortRunnerJobBody, - AcceptRunnerJobResult, - ErrorRunnerJobBody, - HttpStatusCode, - ListRunnerJobsQuery, - LiveRTMPHLSTranscodingUpdatePayload, - RequestRunnerJobResult, - RunnerJobState, - RunnerJobSuccessBody, - RunnerJobSuccessPayload, - RunnerJobType, - RunnerJobUpdateBody, - RunnerJobUpdatePayload, - ServerErrorCode, - UserRight, - VideoStudioTranscodingSuccess, - VODAudioMergeTranscodingSuccess, - VODHLSTranscodingSuccess, - VODWebVideoTranscodingSuccess -} from '@shared/models' - -const postRunnerJobSuccessVideoFiles = createReqFiles( - [ 'payload[videoFile]', 'payload[resolutionPlaylistFile]' ], - { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT } -) - -const runnerJobUpdateVideoFiles = createReqFiles( - [ 'payload[videoChunkFile]', 'payload[resolutionPlaylistFile]', 'payload[masterPlaylistFile]' ], - { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.M3U8.MIMETYPE_EXT } -) - -const lTags = loggerTagsFactory('api', 'runner') - -const runnerJobsRouter = express.Router() - -// --------------------------------------------------------------------------- -// Controllers for runners -// --------------------------------------------------------------------------- - -runnerJobsRouter.post('/jobs/request', - apiRateLimiter, - asyncMiddleware(getRunnerFromTokenValidator), - asyncMiddleware(requestRunnerJob) -) - -runnerJobsRouter.post('/jobs/:jobUUID/accept', - apiRateLimiter, - asyncMiddleware(runnerJobGetValidator), - acceptRunnerJobValidator, - asyncMiddleware(getRunnerFromTokenValidator), - asyncMiddleware(acceptRunnerJob) -) - -runnerJobsRouter.post('/jobs/:jobUUID/abort', - apiRateLimiter, - asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), - abortRunnerJobValidator, - asyncMiddleware(abortRunnerJob) -) - -runnerJobsRouter.post('/jobs/:jobUUID/update', - runnerJobUpdateVideoFiles, - apiRateLimiter, // Has to be after multer middleware to parse runner token - asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING, RunnerJobState.COMPLETING, RunnerJobState.COMPLETED ])), - updateRunnerJobValidator, - asyncMiddleware(updateRunnerJobController) -) - -runnerJobsRouter.post('/jobs/:jobUUID/error', - asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), - errorRunnerJobValidator, - asyncMiddleware(errorRunnerJob) -) - -runnerJobsRouter.post('/jobs/:jobUUID/success', - postRunnerJobSuccessVideoFiles, - apiRateLimiter, // Has to be after multer middleware to parse runner token - asyncMiddleware(jobOfRunnerGetValidatorFactory([ RunnerJobState.PROCESSING ])), - successRunnerJobValidator, - asyncMiddleware(postRunnerJobSuccess) -) - -// --------------------------------------------------------------------------- -// Controllers for admins -// --------------------------------------------------------------------------- - -runnerJobsRouter.post('/jobs/:jobUUID/cancel', - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - asyncMiddleware(runnerJobGetValidator), - cancelRunnerJobValidator, - asyncMiddleware(cancelRunnerJob) -) - -runnerJobsRouter.get('/jobs', - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - paginationValidator, - runnerJobsSortValidator, - setDefaultSort, - setDefaultPagination, - listRunnerJobsValidator, - asyncMiddleware(listRunnerJobs) -) - -runnerJobsRouter.delete('/jobs/:jobUUID', - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - asyncMiddleware(runnerJobGetValidator), - asyncMiddleware(deleteRunnerJob) -) - -// --------------------------------------------------------------------------- - -export { - runnerJobsRouter -} - -// --------------------------------------------------------------------------- - -// --------------------------------------------------------------------------- -// Controllers for runners -// --------------------------------------------------------------------------- - -async function requestRunnerJob (req: express.Request, res: express.Response) { - const runner = res.locals.runner - const availableJobs = await RunnerJobModel.listAvailableJobs() - - logger.debug('Runner %s requests for a job.', runner.name, { availableJobs, ...lTags(runner.name) }) - - const result: RequestRunnerJobResult = { - availableJobs: availableJobs.map(j => ({ - uuid: j.uuid, - type: j.type, - payload: j.payload - })) - } - - updateLastRunnerContact(req, runner) - - return res.json(result) -} - -async function acceptRunnerJob (req: express.Request, res: express.Response) { - const runner = res.locals.runner - const runnerJob = res.locals.runnerJob - - const newRunnerJob = await retryTransactionWrapper(() => { - return sequelizeTypescript.transaction(async transaction => { - await runnerJob.reload({ transaction }) - - if (runnerJob.state !== RunnerJobState.PENDING) { - res.fail({ - type: ServerErrorCode.RUNNER_JOB_NOT_IN_PENDING_STATE, - message: 'This job is not in pending state anymore', - status: HttpStatusCode.CONFLICT_409 - }) - - return undefined - } - - runnerJob.state = RunnerJobState.PROCESSING - runnerJob.processingJobToken = generateRunnerJobToken() - runnerJob.startedAt = new Date() - runnerJob.runnerId = runner.id - - return runnerJob.save({ transaction }) - }) - }) - if (!newRunnerJob) return - - newRunnerJob.Runner = runner as RunnerModel - - const result: AcceptRunnerJobResult = { - job: { - ...newRunnerJob.toFormattedJSON(), - - jobToken: newRunnerJob.processingJobToken - } - } - - updateLastRunnerContact(req, runner) - - logger.info( - 'Remote runner %s has accepted job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type, - lTags(runner.name, runnerJob.uuid, runnerJob.type) - ) - - return res.json(result) -} - -async function abortRunnerJob (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - const runner = runnerJob.Runner - const body: AbortRunnerJobBody = req.body - - logger.info( - 'Remote runner %s is aborting job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type, - { reason: body.reason, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } - ) - - const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob) - await new RunnerJobHandler().abort({ runnerJob }) - - updateLastRunnerContact(req, runnerJob.Runner) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function errorRunnerJob (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - const runner = runnerJob.Runner - const body: ErrorRunnerJobBody = req.body - - runnerJob.failures += 1 - - logger.error( - 'Remote runner %s had an error with job %s (%s)', runner.name, runnerJob.uuid, runnerJob.type, - { errorMessage: body.message, totalFailures: runnerJob.failures, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } - ) - - const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob) - await new RunnerJobHandler().error({ runnerJob, message: body.message }) - - updateLastRunnerContact(req, runnerJob.Runner) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -// --------------------------------------------------------------------------- - -const jobUpdateBuilders: { - [id in RunnerJobType]?: (payload: RunnerJobUpdatePayload, files?: UploadFiles) => RunnerJobUpdatePayload -} = { - 'live-rtmp-hls-transcoding': (payload: LiveRTMPHLSTranscodingUpdatePayload, files) => { - return { - ...payload, - - masterPlaylistFile: files['payload[masterPlaylistFile]']?.[0].path, - resolutionPlaylistFile: files['payload[resolutionPlaylistFile]']?.[0].path, - videoChunkFile: files['payload[videoChunkFile]']?.[0].path - } - } -} - -async function updateRunnerJobController (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - const runner = runnerJob.Runner - const body: RunnerJobUpdateBody = req.body - - if (runnerJob.state === RunnerJobState.COMPLETING || runnerJob.state === RunnerJobState.COMPLETED) { - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) - } - - const payloadBuilder = jobUpdateBuilders[runnerJob.type] - const updatePayload = payloadBuilder - ? payloadBuilder(body.payload, req.files as UploadFiles) - : undefined - - logger.debug( - 'Remote runner %s is updating job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type, - { body, updatePayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } - ) - - const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob) - await new RunnerJobHandler().update({ - runnerJob, - progress: req.body.progress, - updatePayload - }) - - updateLastRunnerContact(req, runnerJob.Runner) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -// --------------------------------------------------------------------------- - -const jobSuccessPayloadBuilders: { - [id in RunnerJobType]: (payload: RunnerJobSuccessPayload, files?: UploadFiles) => RunnerJobSuccessPayload -} = { - 'vod-web-video-transcoding': (payload: VODWebVideoTranscodingSuccess, files) => { - return { - ...payload, - - videoFile: files['payload[videoFile]'][0].path - } - }, - - 'vod-hls-transcoding': (payload: VODHLSTranscodingSuccess, files) => { - return { - ...payload, - - videoFile: files['payload[videoFile]'][0].path, - resolutionPlaylistFile: files['payload[resolutionPlaylistFile]'][0].path - } - }, - - 'vod-audio-merge-transcoding': (payload: VODAudioMergeTranscodingSuccess, files) => { - return { - ...payload, - - videoFile: files['payload[videoFile]'][0].path - } - }, - - 'video-studio-transcoding': (payload: VideoStudioTranscodingSuccess, files) => { - return { - ...payload, - - videoFile: files['payload[videoFile]'][0].path - } - }, - - 'live-rtmp-hls-transcoding': () => ({}) -} - -async function postRunnerJobSuccess (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - const runner = runnerJob.Runner - const body: RunnerJobSuccessBody = req.body - - const resultPayload = jobSuccessPayloadBuilders[runnerJob.type](body.payload, req.files as UploadFiles) - - logger.info( - 'Remote runner %s is sending success result for job %s (%s)', runnerJob.Runner.name, runnerJob.uuid, runnerJob.type, - { resultPayload, ...lTags(runner.name, runnerJob.uuid, runnerJob.type) } - ) - - const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob) - await new RunnerJobHandler().complete({ runnerJob, resultPayload }) - - updateLastRunnerContact(req, runnerJob.Runner) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -// --------------------------------------------------------------------------- -// Controllers for admins -// --------------------------------------------------------------------------- - -async function cancelRunnerJob (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - - logger.info('Cancelling job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type)) - - const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob) - await new RunnerJobHandler().cancel({ runnerJob }) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function deleteRunnerJob (req: express.Request, res: express.Response) { - const runnerJob = res.locals.runnerJob - - logger.info('Deleting job %s (%s)', runnerJob.uuid, runnerJob.type, lTags(runnerJob.uuid, runnerJob.type)) - - if (runnerJobCanBeCancelled(runnerJob)) { - const RunnerJobHandler = getRunnerJobHandlerClass(runnerJob) - await new RunnerJobHandler().cancel({ runnerJob }) - } - - await runnerJob.destroy() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function listRunnerJobs (req: express.Request, res: express.Response) { - const query: ListRunnerJobsQuery = req.query - - const resultList = await RunnerJobModel.listForApi({ - start: query.start, - count: query.count, - sort: query.sort, - search: query.search, - stateOneOf: query.stateOneOf - }) - - return res.json({ - total: resultList.total, - data: resultList.data.map(d => d.toFormattedAdminJSON()) - }) -} diff --git a/server/controllers/api/runners/manage-runners.ts b/server/controllers/api/runners/manage-runners.ts deleted file mode 100644 index be7ebc0b3..000000000 --- a/server/controllers/api/runners/manage-runners.ts +++ /dev/null @@ -1,112 +0,0 @@ -import express from 'express' -import { logger, loggerTagsFactory } from '@server/helpers/logger' -import { generateRunnerToken } from '@server/helpers/token-generator' -import { - apiRateLimiter, - asyncMiddleware, - authenticate, - ensureUserHasRight, - paginationValidator, - runnersSortValidator, - setDefaultPagination, - setDefaultSort -} from '@server/middlewares' -import { deleteRunnerValidator, getRunnerFromTokenValidator, registerRunnerValidator } from '@server/middlewares/validators/runners' -import { RunnerModel } from '@server/models/runner/runner' -import { HttpStatusCode, ListRunnersQuery, RegisterRunnerBody, UserRight } from '@shared/models' - -const lTags = loggerTagsFactory('api', 'runner') - -const manageRunnersRouter = express.Router() - -manageRunnersRouter.post('/register', - apiRateLimiter, - asyncMiddleware(registerRunnerValidator), - asyncMiddleware(registerRunner) -) -manageRunnersRouter.post('/unregister', - apiRateLimiter, - asyncMiddleware(getRunnerFromTokenValidator), - asyncMiddleware(unregisterRunner) -) - -manageRunnersRouter.delete('/:runnerId', - apiRateLimiter, - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - asyncMiddleware(deleteRunnerValidator), - asyncMiddleware(deleteRunner) -) - -manageRunnersRouter.get('/', - apiRateLimiter, - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - paginationValidator, - runnersSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listRunners) -) - -// --------------------------------------------------------------------------- - -export { - manageRunnersRouter -} - -// --------------------------------------------------------------------------- - -async function registerRunner (req: express.Request, res: express.Response) { - const body: RegisterRunnerBody = req.body - - const runnerToken = generateRunnerToken() - - const runner = new RunnerModel({ - runnerToken, - name: body.name, - description: body.description, - lastContact: new Date(), - ip: req.ip, - runnerRegistrationTokenId: res.locals.runnerRegistrationToken.id - }) - - await runner.save() - - logger.info('Registered new runner %s', runner.name, { ...lTags(runner.name) }) - - return res.json({ id: runner.id, runnerToken }) -} -async function unregisterRunner (req: express.Request, res: express.Response) { - const runner = res.locals.runner - await runner.destroy() - - logger.info('Unregistered runner %s', runner.name, { ...lTags(runner.name) }) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function deleteRunner (req: express.Request, res: express.Response) { - const runner = res.locals.runner - - await runner.destroy() - - logger.info('Deleted runner %s', runner.name, { ...lTags(runner.name) }) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function listRunners (req: express.Request, res: express.Response) { - const query: ListRunnersQuery = req.query - - const resultList = await RunnerModel.listForApi({ - start: query.start, - count: query.count, - sort: query.sort - }) - - return res.json({ - total: resultList.total, - data: resultList.data.map(d => d.toFormattedJSON()) - }) -} diff --git a/server/controllers/api/runners/registration-tokens.ts b/server/controllers/api/runners/registration-tokens.ts deleted file mode 100644 index 117ff271b..000000000 --- a/server/controllers/api/runners/registration-tokens.ts +++ /dev/null @@ -1,91 +0,0 @@ -import express from 'express' -import { logger, loggerTagsFactory } from '@server/helpers/logger' -import { generateRunnerRegistrationToken } from '@server/helpers/token-generator' -import { - apiRateLimiter, - asyncMiddleware, - authenticate, - ensureUserHasRight, - paginationValidator, - runnerRegistrationTokensSortValidator, - setDefaultPagination, - setDefaultSort -} from '@server/middlewares' -import { deleteRegistrationTokenValidator } from '@server/middlewares/validators/runners' -import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token' -import { HttpStatusCode, ListRunnerRegistrationTokensQuery, UserRight } from '@shared/models' - -const lTags = loggerTagsFactory('api', 'runner') - -const runnerRegistrationTokensRouter = express.Router() - -runnerRegistrationTokensRouter.post('/registration-tokens/generate', - apiRateLimiter, - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - asyncMiddleware(generateRegistrationToken) -) - -runnerRegistrationTokensRouter.delete('/registration-tokens/:id', - apiRateLimiter, - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - asyncMiddleware(deleteRegistrationTokenValidator), - asyncMiddleware(deleteRegistrationToken) -) - -runnerRegistrationTokensRouter.get('/registration-tokens', - apiRateLimiter, - authenticate, - ensureUserHasRight(UserRight.MANAGE_RUNNERS), - paginationValidator, - runnerRegistrationTokensSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listRegistrationTokens) -) - -// --------------------------------------------------------------------------- - -export { - runnerRegistrationTokensRouter -} - -// --------------------------------------------------------------------------- - -async function generateRegistrationToken (req: express.Request, res: express.Response) { - logger.info('Generating new runner registration token.', lTags()) - - const registrationToken = new RunnerRegistrationTokenModel({ - registrationToken: generateRunnerRegistrationToken() - }) - - await registrationToken.save() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function deleteRegistrationToken (req: express.Request, res: express.Response) { - logger.info('Removing runner registration token.', lTags()) - - const runnerRegistrationToken = res.locals.runnerRegistrationToken - - await runnerRegistrationToken.destroy() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function listRegistrationTokens (req: express.Request, res: express.Response) { - const query: ListRunnerRegistrationTokensQuery = req.query - - const resultList = await RunnerRegistrationTokenModel.listForApi({ - start: query.start, - count: query.count, - sort: query.sort - }) - - return res.json({ - total: resultList.total, - data: resultList.data.map(d => d.toFormattedJSON()) - }) -} diff --git a/server/controllers/api/search/index.ts b/server/controllers/api/search/index.ts deleted file mode 100644 index 4d395161c..000000000 --- a/server/controllers/api/search/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import express from 'express' -import { apiRateLimiter } from '@server/middlewares' -import { searchChannelsRouter } from './search-video-channels' -import { searchPlaylistsRouter } from './search-video-playlists' -import { searchVideosRouter } from './search-videos' - -const searchRouter = express.Router() - -searchRouter.use(apiRateLimiter) - -searchRouter.use('/', searchVideosRouter) -searchRouter.use('/', searchChannelsRouter) -searchRouter.use('/', searchPlaylistsRouter) - -// --------------------------------------------------------------------------- - -export { - searchRouter -} diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts deleted file mode 100644 index 1d2a9d235..000000000 --- a/server/controllers/api/search/search-video-channels.ts +++ /dev/null @@ -1,152 +0,0 @@ -import express from 'express' -import { sanitizeUrl } from '@server/helpers/core-utils' -import { pickSearchChannelQuery } from '@server/helpers/query' -import { doJSONRequest } from '@server/helpers/requests' -import { CONFIG } from '@server/initializers/config' -import { WEBSERVER } from '@server/initializers/constants' -import { findLatestAPRedirection } from '@server/lib/activitypub/activity' -import { Hooks } from '@server/lib/plugins/hooks' -import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' -import { getServerActor } from '@server/models/application/application' -import { HttpStatusCode, ResultList, VideoChannel } from '@shared/models' -import { VideoChannelsSearchQueryAfterSanitize } from '../../../../shared/models/search' -import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../../lib/activitypub/actors' -import { - asyncMiddleware, - openapiOperationDoc, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultSearchSort, - videoChannelsListSearchValidator, - videoChannelsSearchSortValidator -} from '../../../middlewares' -import { VideoChannelModel } from '../../../models/video/video-channel' -import { MChannelAccountDefault } from '../../../types/models' -import { searchLocalUrl } from './shared' - -const searchChannelsRouter = express.Router() - -searchChannelsRouter.get('/video-channels', - openapiOperationDoc({ operationId: 'searchChannels' }), - paginationValidator, - setDefaultPagination, - videoChannelsSearchSortValidator, - setDefaultSearchSort, - optionalAuthenticate, - videoChannelsListSearchValidator, - asyncMiddleware(searchVideoChannels) -) - -// --------------------------------------------------------------------------- - -export { searchChannelsRouter } - -// --------------------------------------------------------------------------- - -function searchVideoChannels (req: express.Request, res: express.Response) { - const query = pickSearchChannelQuery(req.query) - const search = query.search || '' - - const parts = search.split('@') - - // Handle strings like @toto@example.com - if (parts.length === 3 && parts[0].length === 0) parts.shift() - const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' ')) - - if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, res) - - // @username -> username to search in DB - if (search.startsWith('@')) query.search = search.replace(/^@/, '') - - if (isSearchIndexSearch(query)) { - return searchVideoChannelsIndex(query, res) - } - - return searchVideoChannelsDB(query, res) -} - -async function searchVideoChannelsIndex (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) { - const result = await buildMutedForSearchIndex(res) - - const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') - - const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' - - try { - logger.debug('Doing video channels search index request on %s.', url, { body }) - - const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) - const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') - - return res.json(jsonResult) - } catch (err) { - logger.warn('Cannot use search index to make video channels search.', { err }) - - return res.fail({ - status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, - message: 'Cannot use search index to make video channels search' - }) - } -} - -async function searchVideoChannelsDB (query: VideoChannelsSearchQueryAfterSanitize, res: express.Response) { - const serverActor = await getServerActor() - - const apiOptions = await Hooks.wrapObject({ - ...query, - - actorId: serverActor.id - }, 'filter:api.search.video-channels.local.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoChannelModel.searchForApi, - apiOptions, - 'filter:api.search.video-channels.local.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function searchVideoChannelURI (search: string, res: express.Response) { - let videoChannel: MChannelAccountDefault - let uri = search - - if (!isURISearch(search)) { - try { - uri = await loadActorUrlOrGetFromWebfinger(search) - } catch (err) { - logger.warn('Cannot load actor URL or get from webfinger.', { search, err }) - - return res.json({ total: 0, data: [] }) - } - } - - if (isUserAbleToSearchRemoteURI(res)) { - try { - const latestUri = await findLatestAPRedirection(uri) - - const actor = await getOrCreateAPActor(latestUri, 'all', true, true) - videoChannel = actor.VideoChannel - } catch (err) { - logger.info('Cannot search remote video channel %s.', uri, { err }) - } - } else { - videoChannel = await searchLocalUrl(sanitizeLocalUrl(uri), url => VideoChannelModel.loadByUrlAndPopulateAccount(url)) - } - - return res.json({ - total: videoChannel ? 1 : 0, - data: videoChannel ? [ videoChannel.toFormattedJSON() ] : [] - }) -} - -function sanitizeLocalUrl (url: string) { - if (!url) return '' - - // Handle alternative channel URLs - return url.replace(new RegExp('^' + WEBSERVER.URL + '/c/'), WEBSERVER.URL + '/video-channels/') -} diff --git a/server/controllers/api/search/search-video-playlists.ts b/server/controllers/api/search/search-video-playlists.ts deleted file mode 100644 index 97aeeaba9..000000000 --- a/server/controllers/api/search/search-video-playlists.ts +++ /dev/null @@ -1,131 +0,0 @@ -import express from 'express' -import { sanitizeUrl } from '@server/helpers/core-utils' -import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils' -import { logger } from '@server/helpers/logger' -import { pickSearchPlaylistQuery } from '@server/helpers/query' -import { doJSONRequest } from '@server/helpers/requests' -import { getFormattedObjects } from '@server/helpers/utils' -import { CONFIG } from '@server/initializers/config' -import { WEBSERVER } from '@server/initializers/constants' -import { findLatestAPRedirection } from '@server/lib/activitypub/activity' -import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get' -import { Hooks } from '@server/lib/plugins/hooks' -import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' -import { getServerActor } from '@server/models/application/application' -import { VideoPlaylistModel } from '@server/models/video/video-playlist' -import { MVideoPlaylistFullSummary } from '@server/types/models' -import { HttpStatusCode, ResultList, VideoPlaylist, VideoPlaylistsSearchQueryAfterSanitize } from '@shared/models' -import { - asyncMiddleware, - openapiOperationDoc, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultSearchSort, - videoPlaylistsListSearchValidator, - videoPlaylistsSearchSortValidator -} from '../../../middlewares' -import { searchLocalUrl } from './shared' - -const searchPlaylistsRouter = express.Router() - -searchPlaylistsRouter.get('/video-playlists', - openapiOperationDoc({ operationId: 'searchPlaylists' }), - paginationValidator, - setDefaultPagination, - videoPlaylistsSearchSortValidator, - setDefaultSearchSort, - optionalAuthenticate, - videoPlaylistsListSearchValidator, - asyncMiddleware(searchVideoPlaylists) -) - -// --------------------------------------------------------------------------- - -export { searchPlaylistsRouter } - -// --------------------------------------------------------------------------- - -function searchVideoPlaylists (req: express.Request, res: express.Response) { - const query = pickSearchPlaylistQuery(req.query) - const search = query.search - - if (isURISearch(search)) return searchVideoPlaylistsURI(search, res) - - if (isSearchIndexSearch(query)) { - return searchVideoPlaylistsIndex(query, res) - } - - return searchVideoPlaylistsDB(query, res) -} - -async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) { - const result = await buildMutedForSearchIndex(res) - - const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params') - - const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-playlists' - - try { - logger.debug('Doing video playlists search index request on %s.', url, { body }) - - const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) - const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-playlists.index.list.result') - - return res.json(jsonResult) - } catch (err) { - logger.warn('Cannot use search index to make video playlists search.', { err }) - - return res.fail({ - status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, - message: 'Cannot use search index to make video playlists search' - }) - } -} - -async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQueryAfterSanitize, res: express.Response) { - const serverActor = await getServerActor() - - const apiOptions = await Hooks.wrapObject({ - ...query, - - followerActorId: serverActor.id - }, 'filter:api.search.video-playlists.local.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoPlaylistModel.searchForApi, - apiOptions, - 'filter:api.search.video-playlists.local.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function searchVideoPlaylistsURI (search: string, res: express.Response) { - let videoPlaylist: MVideoPlaylistFullSummary - - if (isUserAbleToSearchRemoteURI(res)) { - try { - const url = await findLatestAPRedirection(search) - - videoPlaylist = await getOrCreateAPVideoPlaylist(url) - } catch (err) { - logger.info('Cannot search remote video playlist %s.', search, { err }) - } - } else { - videoPlaylist = await searchLocalUrl(sanitizeLocalUrl(search), url => VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(url)) - } - - return res.json({ - total: videoPlaylist ? 1 : 0, - data: videoPlaylist ? [ videoPlaylist.toFormattedJSON() ] : [] - }) -} - -function sanitizeLocalUrl (url: string) { - if (!url) return '' - - // Handle alternative channel URLs - return url.replace(new RegExp('^' + WEBSERVER.URL + '/videos/watch/playlist/'), WEBSERVER.URL + '/video-playlists/') - .replace(new RegExp('^' + WEBSERVER.URL + '/w/p/'), WEBSERVER.URL + '/video-playlists/') -} diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts deleted file mode 100644 index b33064335..000000000 --- a/server/controllers/api/search/search-videos.ts +++ /dev/null @@ -1,167 +0,0 @@ -import express from 'express' -import { sanitizeUrl } from '@server/helpers/core-utils' -import { pickSearchVideoQuery } from '@server/helpers/query' -import { doJSONRequest } from '@server/helpers/requests' -import { CONFIG } from '@server/initializers/config' -import { WEBSERVER } from '@server/initializers/constants' -import { findLatestAPRedirection } from '@server/lib/activitypub/activity' -import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' -import { Hooks } from '@server/lib/plugins/hooks' -import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' -import { getServerActor } from '@server/models/application/application' -import { HttpStatusCode, ResultList, Video } from '@shared/models' -import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' -import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { - asyncMiddleware, - commonVideosFiltersValidator, - openapiOperationDoc, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultSearchSort, - videosSearchSortValidator, - videosSearchValidator -} from '../../../middlewares' -import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' -import { VideoModel } from '../../../models/video/video' -import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' -import { searchLocalUrl } from './shared' - -const searchVideosRouter = express.Router() - -searchVideosRouter.get('/videos', - openapiOperationDoc({ operationId: 'searchVideos' }), - paginationValidator, - setDefaultPagination, - videosSearchSortValidator, - setDefaultSearchSort, - optionalAuthenticate, - commonVideosFiltersValidator, - videosSearchValidator, - asyncMiddleware(searchVideos) -) - -// --------------------------------------------------------------------------- - -export { searchVideosRouter } - -// --------------------------------------------------------------------------- - -function searchVideos (req: express.Request, res: express.Response) { - const query = pickSearchVideoQuery(req.query) - const search = query.search - - if (isURISearch(search)) { - return searchVideoURI(search, res) - } - - if (isSearchIndexSearch(query)) { - return searchVideosIndex(query, res) - } - - return searchVideosDB(query, res) -} - -async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: express.Response) { - const result = await buildMutedForSearchIndex(res) - - let body = { ...query, ...result } - - // Use the default instance NSFW policy if not specified - if (!body.nsfw) { - const nsfwPolicy = res.locals.oauth - ? res.locals.oauth.token.User.nsfwPolicy - : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY - - body.nsfw = nsfwPolicy === 'do_not_list' - ? 'false' - : 'both' - } - - body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') - - const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' - - try { - logger.debug('Doing videos search index request on %s.', url, { body }) - - const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) - const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') - - return res.json(jsonResult) - } catch (err) { - logger.warn('Cannot use search index to make video search.', { err }) - - return res.fail({ - status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, - message: 'Cannot use search index to make video search' - }) - } -} - -async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) { - const serverActor = await getServerActor() - - const apiOptions = await Hooks.wrapObject({ - ...query, - - displayOnlyForFollower: { - actorId: serverActor.id, - orLocalVideos: true - }, - - nsfw: buildNSFWFilter(res, query.nsfw), - user: res.locals.oauth - ? res.locals.oauth.token.User - : undefined - }, 'filter:api.search.videos.local.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoModel.searchAndPopulateAccountAndServer, - apiOptions, - 'filter:api.search.videos.local.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) -} - -async function searchVideoURI (url: string, res: express.Response) { - let video: MVideoAccountLightBlacklistAllFiles - - // Check if we can fetch a remote video with the URL - if (isUserAbleToSearchRemoteURI(res)) { - try { - const syncParam = { - rates: false, - shares: false, - comments: false, - refreshVideo: false - } - - const result = await getOrCreateAPVideo({ - videoObject: await findLatestAPRedirection(url), - syncParam - }) - video = result ? result.video : undefined - } catch (err) { - logger.info('Cannot search remote video %s.', url, { err }) - } - } else { - video = await searchLocalUrl(sanitizeLocalUrl(url), url => VideoModel.loadByUrlAndPopulateAccount(url)) - } - - return res.json({ - total: video ? 1 : 0, - data: video ? [ video.toFormattedJSON() ] : [] - }) -} - -function sanitizeLocalUrl (url: string) { - if (!url) return '' - - // Handle alternative video URLs - return url.replace(new RegExp('^' + WEBSERVER.URL + '/w/'), WEBSERVER.URL + '/videos/watch/') -} diff --git a/server/controllers/api/search/shared/index.ts b/server/controllers/api/search/shared/index.ts deleted file mode 100644 index 9c56149ef..000000000 --- a/server/controllers/api/search/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils' diff --git a/server/controllers/api/search/shared/utils.ts b/server/controllers/api/search/shared/utils.ts deleted file mode 100644 index e02e84f31..000000000 --- a/server/controllers/api/search/shared/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -async function searchLocalUrl (url: string, finder: (url: string) => Promise) { - const data = await finder(url) - if (data) return data - - return finder(removeQueryParams(url)) -} - -export { - searchLocalUrl -} - -// --------------------------------------------------------------------------- - -function removeQueryParams (url: string) { - return url.split('?').shift() -} diff --git a/server/controllers/api/server/contact.ts b/server/controllers/api/server/contact.ts deleted file mode 100644 index 56596bea5..000000000 --- a/server/controllers/api/server/contact.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { logger } from '@server/helpers/logger' -import express from 'express' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { ContactForm } from '../../../../shared/models/server' -import { Emailer } from '../../../lib/emailer' -import { Redis } from '../../../lib/redis' -import { asyncMiddleware, contactAdministratorValidator } from '../../../middlewares' - -const contactRouter = express.Router() - -contactRouter.post('/contact', - asyncMiddleware(contactAdministratorValidator), - asyncMiddleware(contactAdministrator) -) - -async function contactAdministrator (req: express.Request, res: express.Response) { - const data = req.body as ContactForm - - Emailer.Instance.addContactFormJob(data.fromEmail, data.fromName, data.subject, data.body) - - try { - await Redis.Instance.setContactFormIp(req.ip) - } catch (err) { - logger.error(err) - } - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -// --------------------------------------------------------------------------- - -export { - contactRouter -} diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts deleted file mode 100644 index f3792bfc8..000000000 --- a/server/controllers/api/server/debug.ts +++ /dev/null @@ -1,56 +0,0 @@ -import express from 'express' -import { InboxManager } from '@server/lib/activitypub/inbox-manager' -import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' -import { VideoViewsBufferScheduler } from '@server/lib/schedulers/video-views-buffer-scheduler' -import { VideoViewsManager } from '@server/lib/views/video-views-manager' -import { Debug, SendDebugCommand } from '@shared/models' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { UserRight } from '../../../../shared/models/users' -import { authenticate, ensureUserHasRight } from '../../../middlewares' -import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-channel-sync-latest-scheduler' -import { UpdateVideosScheduler } from '@server/lib/schedulers/update-videos-scheduler' - -const debugRouter = express.Router() - -debugRouter.get('/debug', - authenticate, - ensureUserHasRight(UserRight.MANAGE_DEBUG), - getDebug -) - -debugRouter.post('/debug/run-command', - authenticate, - ensureUserHasRight(UserRight.MANAGE_DEBUG), - runCommand -) - -// --------------------------------------------------------------------------- - -export { - debugRouter -} - -// --------------------------------------------------------------------------- - -function getDebug (req: express.Request, res: express.Response) { - return res.json({ - ip: req.ip, - activityPubMessagesWaiting: InboxManager.Instance.getActivityPubMessagesWaiting() - } as Debug) -} - -async function runCommand (req: express.Request, res: express.Response) { - const body: SendDebugCommand = req.body - - const processors: { [id in SendDebugCommand['command']]: () => Promise } = { - 'remove-dandling-resumable-uploads': () => RemoveDanglingResumableUploadsScheduler.Instance.execute(), - 'process-video-views-buffer': () => VideoViewsBufferScheduler.Instance.execute(), - 'process-video-viewers': () => VideoViewsManager.Instance.processViewerStats(), - 'process-update-videos-scheduler': () => UpdateVideosScheduler.Instance.execute(), - 'process-video-channel-sync-latest': () => VideoChannelSyncLatestScheduler.Instance.execute() - } - - await processors[body.command]() - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts deleted file mode 100644 index 87828813a..000000000 --- a/server/controllers/api/server/follows.ts +++ /dev/null @@ -1,214 +0,0 @@ -import express from 'express' -import { getServerActor } from '@server/models/application/application' -import { ServerFollowCreate } from '@shared/models' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { UserRight } from '../../../../shared/models/users' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { SERVER_ACTOR_NAME } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' -import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' -import { JobQueue } from '../../../lib/job-queue' -import { removeRedundanciesOfServer } from '../../../lib/redundancy' -import { - asyncMiddleware, - authenticate, - ensureUserHasRight, - paginationValidator, - setBodyHostsPort, - setDefaultPagination, - setDefaultSort -} from '../../../middlewares' -import { - acceptFollowerValidator, - followValidator, - getFollowerValidator, - instanceFollowersSortValidator, - instanceFollowingSortValidator, - listFollowsValidator, - rejectFollowerValidator, - removeFollowingValidator -} from '../../../middlewares/validators' -import { ActorFollowModel } from '../../../models/actor/actor-follow' - -const serverFollowsRouter = express.Router() -serverFollowsRouter.get('/following', - listFollowsValidator, - paginationValidator, - instanceFollowingSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listFollowing) -) - -serverFollowsRouter.post('/following', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), - followValidator, - setBodyHostsPort, - asyncMiddleware(addFollow) -) - -serverFollowsRouter.delete('/following/:hostOrHandle', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), - asyncMiddleware(removeFollowingValidator), - asyncMiddleware(removeFollowing) -) - -serverFollowsRouter.get('/followers', - listFollowsValidator, - paginationValidator, - instanceFollowersSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listFollowers) -) - -serverFollowsRouter.delete('/followers/:nameWithHost', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), - asyncMiddleware(getFollowerValidator), - asyncMiddleware(removeFollower) -) - -serverFollowsRouter.post('/followers/:nameWithHost/reject', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), - asyncMiddleware(getFollowerValidator), - rejectFollowerValidator, - asyncMiddleware(rejectFollower) -) - -serverFollowsRouter.post('/followers/:nameWithHost/accept', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), - asyncMiddleware(getFollowerValidator), - acceptFollowerValidator, - asyncMiddleware(acceptFollower) -) - -// --------------------------------------------------------------------------- - -export { - serverFollowsRouter -} - -// --------------------------------------------------------------------------- - -async function listFollowing (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - const resultList = await ActorFollowModel.listInstanceFollowingForApi({ - followerId: serverActor.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - actorType: req.query.actorType, - state: req.query.state - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listFollowers (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - const resultList = await ActorFollowModel.listFollowersForApi({ - actorIds: [ serverActor.id ], - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - actorType: req.query.actorType, - state: req.query.state - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function addFollow (req: express.Request, res: express.Response) { - const { hosts, handles } = req.body as ServerFollowCreate - const follower = await getServerActor() - - for (const host of hosts) { - const payload = { - host, - name: SERVER_ACTOR_NAME, - followerActorId: follower.id - } - - JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload }) - } - - for (const handle of handles) { - const [ name, host ] = handle.split('@') - - const payload = { - host, - name, - followerActorId: follower.id - } - - JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload }) - } - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function removeFollowing (req: express.Request, res: express.Response) { - const follow = res.locals.follow - - await sequelizeTypescript.transaction(async t => { - if (follow.state === 'accepted') sendUndoFollow(follow, t) - - // Disable redundancy on unfollowed instances - const server = follow.ActorFollowing.Server - server.redundancyAllowed = false - await server.save({ transaction: t }) - - // Async, could be long - removeRedundanciesOfServer(server.id) - .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) - - await follow.destroy({ transaction: t }) - }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function rejectFollower (req: express.Request, res: express.Response) { - const follow = res.locals.follow - - follow.state = 'rejected' - await follow.save() - - sendReject(follow.url, follow.ActorFollower, follow.ActorFollowing) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function removeFollower (req: express.Request, res: express.Response) { - const follow = res.locals.follow - - if (follow.state === 'accepted' || follow.state === 'pending') { - sendReject(follow.url, follow.ActorFollower, follow.ActorFollowing) - } - - await follow.destroy() - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function acceptFollower (req: express.Request, res: express.Response) { - const follow = res.locals.follow - - sendAccept(follow) - - follow.state = 'accepted' - await follow.save() - - await autoFollowBackIfNeeded(follow) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts deleted file mode 100644 index 57f7d601c..000000000 --- a/server/controllers/api/server/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import express from 'express' -import { apiRateLimiter } from '@server/middlewares' -import { contactRouter } from './contact' -import { debugRouter } from './debug' -import { serverFollowsRouter } from './follows' -import { logsRouter } from './logs' -import { serverRedundancyRouter } from './redundancy' -import { serverBlocklistRouter } from './server-blocklist' -import { statsRouter } from './stats' - -const serverRouter = express.Router() - -serverRouter.use(apiRateLimiter) - -serverRouter.use('/', serverFollowsRouter) -serverRouter.use('/', serverRedundancyRouter) -serverRouter.use('/', statsRouter) -serverRouter.use('/', serverBlocklistRouter) -serverRouter.use('/', contactRouter) -serverRouter.use('/', logsRouter) -serverRouter.use('/', debugRouter) - -// --------------------------------------------------------------------------- - -export { - serverRouter -} diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts deleted file mode 100644 index ed0aa6e8e..000000000 --- a/server/controllers/api/server/logs.ts +++ /dev/null @@ -1,203 +0,0 @@ -import express from 'express' -import { readdir, readFile } from 'fs-extra' -import { join } from 'path' -import { isArray } from '@server/helpers/custom-validators/misc' -import { logger, mtimeSortFilesDesc } from '@server/helpers/logger' -import { pick } from '@shared/core-utils' -import { ClientLogCreate, HttpStatusCode } from '@shared/models' -import { ServerLogLevel } from '../../../../shared/models/server/server-log-level.type' -import { UserRight } from '../../../../shared/models/users' -import { CONFIG } from '../../../initializers/config' -import { AUDIT_LOG_FILENAME, LOG_FILENAME, MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants' -import { asyncMiddleware, authenticate, buildRateLimiter, ensureUserHasRight, optionalAuthenticate } from '../../../middlewares' -import { createClientLogValidator, getAuditLogsValidator, getLogsValidator } from '../../../middlewares/validators/logs' - -const createClientLogRateLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.WINDOW_MS, - max: CONFIG.RATES_LIMIT.RECEIVE_CLIENT_LOG.MAX -}) - -const logsRouter = express.Router() - -logsRouter.post('/logs/client', - createClientLogRateLimiter, - optionalAuthenticate, - createClientLogValidator, - createClientLog -) - -logsRouter.get('/logs', - authenticate, - ensureUserHasRight(UserRight.MANAGE_LOGS), - getLogsValidator, - asyncMiddleware(getLogs) -) - -logsRouter.get('/audit-logs', - authenticate, - ensureUserHasRight(UserRight.MANAGE_LOGS), - getAuditLogsValidator, - asyncMiddleware(getAuditLogs) -) - -// --------------------------------------------------------------------------- - -export { - logsRouter -} - -// --------------------------------------------------------------------------- - -function createClientLog (req: express.Request, res: express.Response) { - const logInfo = req.body as ClientLogCreate - - const meta = { - tags: [ 'client' ], - username: res.locals.oauth?.token?.User?.username, - - ...pick(logInfo, [ 'userAgent', 'stackTrace', 'meta', 'url' ]) - } - - logger.log(logInfo.level, `Client log: ${logInfo.message}`, meta) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -const auditLogNameFilter = generateLogNameFilter(AUDIT_LOG_FILENAME) -async function getAuditLogs (req: express.Request, res: express.Response) { - const output = await generateOutput({ - startDateQuery: req.query.startDate, - endDateQuery: req.query.endDate, - level: 'audit', - nameFilter: auditLogNameFilter - }) - - return res.json(output).end() -} - -const logNameFilter = generateLogNameFilter(LOG_FILENAME) -async function getLogs (req: express.Request, res: express.Response) { - const output = await generateOutput({ - startDateQuery: req.query.startDate, - endDateQuery: req.query.endDate, - level: req.query.level || 'info', - tagsOneOf: req.query.tagsOneOf, - nameFilter: logNameFilter - }) - - return res.json(output) -} - -async function generateOutput (options: { - startDateQuery: string - endDateQuery?: string - - level: ServerLogLevel - nameFilter: RegExp - tagsOneOf?: string[] -}) { - const { startDateQuery, level, nameFilter } = options - - const tagsOneOf = Array.isArray(options.tagsOneOf) && options.tagsOneOf.length !== 0 - ? new Set(options.tagsOneOf) - : undefined - - const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR) - const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR) - let currentSize = 0 - - const startDate = new Date(startDateQuery) - const endDate = options.endDateQuery ? new Date(options.endDateQuery) : new Date() - - let output: string[] = [] - - for (const meta of sortedLogFiles) { - if (nameFilter.exec(meta.file) === null) continue - - const path = join(CONFIG.STORAGE.LOG_DIR, meta.file) - logger.debug('Opening %s to fetch logs.', path) - - const result = await getOutputFromFile({ path, startDate, endDate, level, currentSize, tagsOneOf }) - if (!result.output) break - - output = result.output.concat(output) - currentSize = result.currentSize - - if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break - } - - return output -} - -async function getOutputFromFile (options: { - path: string - startDate: Date - endDate: Date - level: ServerLogLevel - currentSize: number - tagsOneOf: Set -}) { - const { path, startDate, endDate, level, tagsOneOf } = options - - const startTime = startDate.getTime() - const endTime = endDate.getTime() - let currentSize = options.currentSize - - let logTime: number - - const logsLevel: { [ id in ServerLogLevel ]: number } = { - audit: -1, - debug: 0, - info: 1, - warn: 2, - error: 3 - } - - const content = await readFile(path) - const lines = content.toString().split('\n') - const output: any[] = [] - - for (let i = lines.length - 1; i >= 0; i--) { - const line = lines[i] - let log: any - - try { - log = JSON.parse(line) - } catch { - // Maybe there a multiple \n at the end of the file - continue - } - - logTime = new Date(log.timestamp).getTime() - if ( - logTime >= startTime && - logTime <= endTime && - logsLevel[log.level] >= logsLevel[level] && - (!tagsOneOf || lineHasTag(log, tagsOneOf)) - ) { - output.push(log) - - currentSize += line.length - - if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break - } else if (logTime < startTime) { - break - } - } - - return { currentSize, output: output.reverse(), logTime } -} - -function lineHasTag (line: { tags?: string }, tagsOneOf: Set) { - if (!isArray(line.tags)) return false - - for (const lineTag of line.tags) { - if (tagsOneOf.has(lineTag)) return true - } - - return false -} - -function generateLogNameFilter (baseName: string) { - return new RegExp('^' + baseName.replace(/\.log$/, '') + '\\d*.log$') -} diff --git a/server/controllers/api/server/redundancy.ts b/server/controllers/api/server/redundancy.ts deleted file mode 100644 index 94e187cd4..000000000 --- a/server/controllers/api/server/redundancy.ts +++ /dev/null @@ -1,116 +0,0 @@ -import express from 'express' -import { JobQueue } from '@server/lib/job-queue' -import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { UserRight } from '../../../../shared/models/users' -import { logger } from '../../../helpers/logger' -import { removeRedundanciesOfServer, removeVideoRedundancy } from '../../../lib/redundancy' -import { - asyncMiddleware, - authenticate, - ensureUserHasRight, - paginationValidator, - setDefaultPagination, - setDefaultVideoRedundanciesSort, - videoRedundanciesSortValidator -} from '../../../middlewares' -import { - addVideoRedundancyValidator, - listVideoRedundanciesValidator, - removeVideoRedundancyValidator, - updateServerRedundancyValidator -} from '../../../middlewares/validators/redundancy' - -const serverRedundancyRouter = express.Router() - -serverRedundancyRouter.put('/redundancy/:host', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), - asyncMiddleware(updateServerRedundancyValidator), - asyncMiddleware(updateRedundancy) -) - -serverRedundancyRouter.get('/redundancy/videos', - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES), - listVideoRedundanciesValidator, - paginationValidator, - videoRedundanciesSortValidator, - setDefaultVideoRedundanciesSort, - setDefaultPagination, - asyncMiddleware(listVideoRedundancies) -) - -serverRedundancyRouter.post('/redundancy/videos', - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES), - addVideoRedundancyValidator, - asyncMiddleware(addVideoRedundancy) -) - -serverRedundancyRouter.delete('/redundancy/videos/:redundancyId', - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES), - removeVideoRedundancyValidator, - asyncMiddleware(removeVideoRedundancyController) -) - -// --------------------------------------------------------------------------- - -export { - serverRedundancyRouter -} - -// --------------------------------------------------------------------------- - -async function listVideoRedundancies (req: express.Request, res: express.Response) { - const resultList = await VideoRedundancyModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - target: req.query.target, - strategy: req.query.strategy - }) - - const result = { - total: resultList.total, - data: resultList.data.map(r => VideoRedundancyModel.toFormattedJSONStatic(r)) - } - - return res.json(result) -} - -async function addVideoRedundancy (req: express.Request, res: express.Response) { - const payload = { - videoId: res.locals.onlyVideo.id - } - - await JobQueue.Instance.createJob({ - type: 'video-redundancy', - payload - }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function removeVideoRedundancyController (req: express.Request, res: express.Response) { - await removeVideoRedundancy(res.locals.videoRedundancy) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function updateRedundancy (req: express.Request, res: express.Response) { - const server = res.locals.server - - server.redundancyAllowed = req.body.redundancyAllowed - - await server.save() - - if (server.redundancyAllowed !== true) { - // Async, could be long - removeRedundanciesOfServer(server.id) - .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) - } - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts deleted file mode 100644 index 740f95da3..000000000 --- a/server/controllers/api/server/server-blocklist.ts +++ /dev/null @@ -1,158 +0,0 @@ -import 'multer' -import express from 'express' -import { logger } from '@server/helpers/logger' -import { getServerActor } from '@server/models/application/application' -import { UserNotificationModel } from '@server/models/user/user-notification' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { UserRight } from '../../../../shared/models/users' -import { getFormattedObjects } from '../../../helpers/utils' -import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - ensureUserHasRight, - paginationValidator, - setDefaultPagination, - setDefaultSort -} from '../../../middlewares' -import { - accountsBlocklistSortValidator, - blockAccountValidator, - blockServerValidator, - serversBlocklistSortValidator, - unblockAccountByServerValidator, - unblockServerByServerValidator -} from '../../../middlewares/validators' -import { AccountBlocklistModel } from '../../../models/account/account-blocklist' -import { ServerBlocklistModel } from '../../../models/server/server-blocklist' - -const serverBlocklistRouter = express.Router() - -serverBlocklistRouter.get('/blocklist/accounts', - authenticate, - ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), - paginationValidator, - accountsBlocklistSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listBlockedAccounts) -) - -serverBlocklistRouter.post('/blocklist/accounts', - authenticate, - ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), - asyncMiddleware(blockAccountValidator), - asyncRetryTransactionMiddleware(blockAccount) -) - -serverBlocklistRouter.delete('/blocklist/accounts/:accountName', - authenticate, - ensureUserHasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST), - asyncMiddleware(unblockAccountByServerValidator), - asyncRetryTransactionMiddleware(unblockAccount) -) - -serverBlocklistRouter.get('/blocklist/servers', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), - paginationValidator, - serversBlocklistSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listBlockedServers) -) - -serverBlocklistRouter.post('/blocklist/servers', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), - asyncMiddleware(blockServerValidator), - asyncRetryTransactionMiddleware(blockServer) -) - -serverBlocklistRouter.delete('/blocklist/servers/:host', - authenticate, - ensureUserHasRight(UserRight.MANAGE_SERVERS_BLOCKLIST), - asyncMiddleware(unblockServerByServerValidator), - asyncRetryTransactionMiddleware(unblockServer) -) - -export { - serverBlocklistRouter -} - -// --------------------------------------------------------------------------- - -async function listBlockedAccounts (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - const resultList = await AccountBlocklistModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - accountId: serverActor.Account.id - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function blockAccount (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - const accountToBlock = res.locals.account - - await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id) - - UserNotificationModel.removeNotificationsOf({ - id: accountToBlock.id, - type: 'account', - forUserId: null // For all users - }).catch(err => logger.error('Cannot remove notifications after an account mute.', { err })) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function unblockAccount (req: express.Request, res: express.Response) { - const accountBlock = res.locals.accountBlock - - await removeAccountFromBlocklist(accountBlock) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function listBlockedServers (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - const resultList = await ServerBlocklistModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - accountId: serverActor.Account.id - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function blockServer (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - const serverToBlock = res.locals.server - - await addServerInBlocklist(serverActor.Account.id, serverToBlock.id) - - UserNotificationModel.removeNotificationsOf({ - id: serverToBlock.id, - type: 'server', - forUserId: null // For all users - }).catch(err => logger.error('Cannot remove notifications after a server mute.', { err })) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function unblockServer (req: express.Request, res: express.Response) { - const serverBlock = res.locals.serverBlock - - await removeServerFromBlocklist(serverBlock) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts deleted file mode 100644 index 2ab398f4d..000000000 --- a/server/controllers/api/server/stats.ts +++ /dev/null @@ -1,26 +0,0 @@ -import express from 'express' -import { StatsManager } from '@server/lib/stat-manager' -import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' -import { asyncMiddleware } from '../../../middlewares' -import { cacheRoute } from '../../../middlewares/cache/cache' -import { Hooks } from '@server/lib/plugins/hooks' - -const statsRouter = express.Router() - -statsRouter.get('/stats', - cacheRoute(ROUTE_CACHE_LIFETIME.STATS), - asyncMiddleware(getStats) -) - -async function getStats (_req: express.Request, res: express.Response) { - let data = await StatsManager.Instance.getStats() - data = await Hooks.wrapObject(data, 'filter:api.server.stats.get.result') - - return res.json(data) -} - -// --------------------------------------------------------------------------- - -export { - statsRouter -} diff --git a/server/controllers/api/users/email-verification.ts b/server/controllers/api/users/email-verification.ts deleted file mode 100644 index 230aaa9af..000000000 --- a/server/controllers/api/users/email-verification.ts +++ /dev/null @@ -1,72 +0,0 @@ -import express from 'express' -import { HttpStatusCode } from '@shared/models' -import { CONFIG } from '../../../initializers/config' -import { sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user' -import { asyncMiddleware, buildRateLimiter } from '../../../middlewares' -import { - registrationVerifyEmailValidator, - usersAskSendVerifyEmailValidator, - usersVerifyEmailValidator -} from '../../../middlewares/validators' - -const askSendEmailLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, - max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX -}) - -const emailVerificationRouter = express.Router() - -emailVerificationRouter.post([ '/ask-send-verify-email', '/registrations/ask-send-verify-email' ], - askSendEmailLimiter, - asyncMiddleware(usersAskSendVerifyEmailValidator), - asyncMiddleware(reSendVerifyUserEmail) -) - -emailVerificationRouter.post('/:id/verify-email', - asyncMiddleware(usersVerifyEmailValidator), - asyncMiddleware(verifyUserEmail) -) - -emailVerificationRouter.post('/registrations/:registrationId/verify-email', - asyncMiddleware(registrationVerifyEmailValidator), - asyncMiddleware(verifyRegistrationEmail) -) - -// --------------------------------------------------------------------------- - -export { - emailVerificationRouter -} - -async function reSendVerifyUserEmail (req: express.Request, res: express.Response) { - const user = res.locals.user - const registration = res.locals.userRegistration - - if (user) await sendVerifyUserEmail(user) - else if (registration) await sendVerifyRegistrationEmail(registration) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function verifyUserEmail (req: express.Request, res: express.Response) { - const user = res.locals.user - user.emailVerified = true - - if (req.body.isPendingEmail === true) { - user.email = user.pendingEmail - user.pendingEmail = null - } - - await user.save() - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function verifyRegistrationEmail (req: express.Request, res: express.Response) { - const registration = res.locals.userRegistration - registration.emailVerified = true - - await registration.save() - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts deleted file mode 100644 index 5eac6fd0f..000000000 --- a/server/controllers/api/users/index.ts +++ /dev/null @@ -1,319 +0,0 @@ -import express from 'express' -import { tokensRouter } from '@server/controllers/api/users/token' -import { Hooks } from '@server/lib/plugins/hooks' -import { OAuthTokenModel } from '@server/models/oauth/oauth-token' -import { MUserAccountDefault } from '@server/types/models' -import { pick } from '@shared/core-utils' -import { HttpStatusCode, UserCreate, UserCreateResult, UserRight, UserUpdate } from '@shared/models' -import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' -import { logger } from '../../../helpers/logger' -import { generateRandomString, getFormattedObjects } from '../../../helpers/utils' -import { WEBSERVER } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { Emailer } from '../../../lib/emailer' -import { Redis } from '../../../lib/redis' -import { buildUser, createUserAccountAndChannelAndPlaylist } from '../../../lib/user' -import { - adminUsersSortValidator, - apiRateLimiter, - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - ensureUserHasRight, - paginationValidator, - setDefaultPagination, - setDefaultSort, - userAutocompleteValidator, - usersAddValidator, - usersGetValidator, - usersListValidator, - usersRemoveValidator, - usersUpdateValidator -} from '../../../middlewares' -import { - ensureCanModerateUser, - usersAskResetPasswordValidator, - usersBlockingValidator, - usersResetPasswordValidator -} from '../../../middlewares/validators' -import { UserModel } from '../../../models/user/user' -import { emailVerificationRouter } from './email-verification' -import { meRouter } from './me' -import { myAbusesRouter } from './my-abuses' -import { myBlocklistRouter } from './my-blocklist' -import { myVideosHistoryRouter } from './my-history' -import { myNotificationsRouter } from './my-notifications' -import { mySubscriptionsRouter } from './my-subscriptions' -import { myVideoPlaylistsRouter } from './my-video-playlists' -import { registrationsRouter } from './registrations' -import { twoFactorRouter } from './two-factor' - -const auditLogger = auditLoggerFactory('users') - -const usersRouter = express.Router() - -usersRouter.use(apiRateLimiter) - -usersRouter.use('/', emailVerificationRouter) -usersRouter.use('/', registrationsRouter) -usersRouter.use('/', twoFactorRouter) -usersRouter.use('/', tokensRouter) -usersRouter.use('/', myNotificationsRouter) -usersRouter.use('/', mySubscriptionsRouter) -usersRouter.use('/', myBlocklistRouter) -usersRouter.use('/', myVideosHistoryRouter) -usersRouter.use('/', myVideoPlaylistsRouter) -usersRouter.use('/', myAbusesRouter) -usersRouter.use('/', meRouter) - -usersRouter.get('/autocomplete', - userAutocompleteValidator, - asyncMiddleware(autocompleteUsers) -) - -usersRouter.get('/', - authenticate, - ensureUserHasRight(UserRight.MANAGE_USERS), - paginationValidator, - adminUsersSortValidator, - setDefaultSort, - setDefaultPagination, - usersListValidator, - asyncMiddleware(listUsers) -) - -usersRouter.post('/:id/block', - authenticate, - ensureUserHasRight(UserRight.MANAGE_USERS), - asyncMiddleware(usersBlockingValidator), - ensureCanModerateUser, - asyncMiddleware(blockUser) -) -usersRouter.post('/:id/unblock', - authenticate, - ensureUserHasRight(UserRight.MANAGE_USERS), - asyncMiddleware(usersBlockingValidator), - ensureCanModerateUser, - asyncMiddleware(unblockUser) -) - -usersRouter.get('/:id', - authenticate, - ensureUserHasRight(UserRight.MANAGE_USERS), - asyncMiddleware(usersGetValidator), - getUser -) - -usersRouter.post('/', - authenticate, - ensureUserHasRight(UserRight.MANAGE_USERS), - asyncMiddleware(usersAddValidator), - asyncRetryTransactionMiddleware(createUser) -) - -usersRouter.put('/:id', - authenticate, - ensureUserHasRight(UserRight.MANAGE_USERS), - asyncMiddleware(usersUpdateValidator), - ensureCanModerateUser, - asyncMiddleware(updateUser) -) - -usersRouter.delete('/:id', - authenticate, - ensureUserHasRight(UserRight.MANAGE_USERS), - asyncMiddleware(usersRemoveValidator), - ensureCanModerateUser, - asyncMiddleware(removeUser) -) - -usersRouter.post('/ask-reset-password', - asyncMiddleware(usersAskResetPasswordValidator), - asyncMiddleware(askResetUserPassword) -) - -usersRouter.post('/:id/reset-password', - asyncMiddleware(usersResetPasswordValidator), - asyncMiddleware(resetUserPassword) -) - -// --------------------------------------------------------------------------- - -export { - usersRouter -} - -// --------------------------------------------------------------------------- - -async function createUser (req: express.Request, res: express.Response) { - const body: UserCreate = req.body - - const userToCreate = buildUser({ - ...pick(body, [ 'username', 'password', 'email', 'role', 'videoQuota', 'videoQuotaDaily', 'adminFlags' ]), - - emailVerified: null - }) - - // NB: due to the validator usersAddValidator, password==='' can only be true if we can send the mail. - const createPassword = userToCreate.password === '' - if (createPassword) { - userToCreate.password = await generateRandomString(20) - } - - const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ - userToCreate, - channelNames: body.channelName && { name: body.channelName, displayName: body.channelName } - }) - - auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) - logger.info('User %s with its channel and account created.', body.username) - - if (createPassword) { - // this will send an email for newly created users, so then can set their first password. - logger.info('Sending to user %s a create password email', body.username) - const verificationString = await Redis.Instance.setCreatePasswordVerificationString(user.id) - const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString - Emailer.Instance.addPasswordCreateEmailJob(userToCreate.username, user.email, url) - } - - Hooks.runAction('action:api.user.created', { body, user, account, videoChannel, req, res }) - - return res.json({ - user: { - id: user.id, - account: { - id: account.id - } - } as UserCreateResult - }) -} - -async function unblockUser (req: express.Request, res: express.Response) { - const user = res.locals.user - - await changeUserBlock(res, user, false) - - Hooks.runAction('action:api.user.unblocked', { user, req, res }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function blockUser (req: express.Request, res: express.Response) { - const user = res.locals.user - const reason = req.body.reason - - await changeUserBlock(res, user, true, reason) - - Hooks.runAction('action:api.user.blocked', { user, req, res }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -function getUser (req: express.Request, res: express.Response) { - return res.json(res.locals.user.toFormattedJSON({ withAdminFlags: true })) -} - -async function autocompleteUsers (req: express.Request, res: express.Response) { - const resultList = await UserModel.autoComplete(req.query.search as string) - - return res.json(resultList) -} - -async function listUsers (req: express.Request, res: express.Response) { - const resultList = await UserModel.listForAdminApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - blocked: req.query.blocked - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total, { withAdminFlags: true })) -} - -async function removeUser (req: express.Request, res: express.Response) { - const user = res.locals.user - - auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) - - await sequelizeTypescript.transaction(async t => { - // Use a transaction to avoid inconsistencies with hooks (account/channel deletion & federation) - await user.destroy({ transaction: t }) - }) - - Hooks.runAction('action:api.user.deleted', { user, req, res }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function updateUser (req: express.Request, res: express.Response) { - const body: UserUpdate = req.body - const userToUpdate = res.locals.user - const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) - const roleChanged = body.role !== undefined && body.role !== userToUpdate.role - - const keysToUpdate: (keyof UserUpdate)[] = [ - 'password', - 'email', - 'emailVerified', - 'videoQuota', - 'videoQuotaDaily', - 'role', - 'adminFlags', - 'pluginAuth' - ] - - for (const key of keysToUpdate) { - if (body[key] !== undefined) userToUpdate.set(key, body[key]) - } - - const user = await userToUpdate.save() - - // Destroy user token to refresh rights - if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id) - - auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) - - Hooks.runAction('action:api.user.updated', { user, req, res }) - - // Don't need to send this update to followers, these attributes are not federated - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function askResetUserPassword (req: express.Request, res: express.Response) { - const user = res.locals.user - - const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) - const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString - Emailer.Instance.addPasswordResetEmailJob(user.username, user.email, url) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function resetUserPassword (req: express.Request, res: express.Response) { - const user = res.locals.user - user.password = req.body.password - - await user.save() - await Redis.Instance.removePasswordVerificationString(user.id) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) { - const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) - - user.blocked = block - user.blockedReason = reason || null - - await sequelizeTypescript.transaction(async t => { - await OAuthTokenModel.deleteUserToken(user.id, t) - - await user.save({ transaction: t }) - }) - - Emailer.Instance.addUserBlockJob(user, block, reason) - - auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) -} diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts deleted file mode 100644 index 26811136e..000000000 --- a/server/controllers/api/users/me.ts +++ /dev/null @@ -1,277 +0,0 @@ -import 'multer' -import express from 'express' -import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' -import { Hooks } from '@server/lib/plugins/hooks' -import { pick } from '@shared/core-utils' -import { ActorImageType, HttpStatusCode, UserUpdateMe, UserVideoQuota, UserVideoRate as FormattedUserVideoRate } from '@shared/models' -import { AttributesOnly } from '@shared/typescript-utils' -import { createReqFiles } from '../../../helpers/express-utils' -import { getFormattedObjects } from '../../../helpers/utils' -import { CONFIG } from '../../../initializers/config' -import { MIMETYPES } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { sendUpdateActor } from '../../../lib/activitypub/send' -import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor' -import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort, - setDefaultVideosSort, - usersUpdateMeValidator, - usersVideoRatingValidator -} from '../../../middlewares' -import { - deleteMeValidator, - getMyVideoImportsValidator, - usersVideosValidator, - videoImportsSortValidator, - videosSortValidator -} from '../../../middlewares/validators' -import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' -import { AccountModel } from '../../../models/account/account' -import { AccountVideoRateModel } from '../../../models/account/account-video-rate' -import { UserModel } from '../../../models/user/user' -import { VideoModel } from '../../../models/video/video' -import { VideoImportModel } from '../../../models/video/video-import' - -const auditLogger = auditLoggerFactory('users') - -const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) - -const meRouter = express.Router() - -meRouter.get('/me', - authenticate, - asyncMiddleware(getUserInformation) -) -meRouter.delete('/me', - authenticate, - deleteMeValidator, - asyncMiddleware(deleteMe) -) - -meRouter.get('/me/video-quota-used', - authenticate, - asyncMiddleware(getUserVideoQuotaUsed) -) - -meRouter.get('/me/videos/imports', - authenticate, - paginationValidator, - videoImportsSortValidator, - setDefaultSort, - setDefaultPagination, - getMyVideoImportsValidator, - asyncMiddleware(getUserVideoImports) -) - -meRouter.get('/me/videos', - authenticate, - paginationValidator, - videosSortValidator, - setDefaultVideosSort, - setDefaultPagination, - asyncMiddleware(usersVideosValidator), - asyncMiddleware(getUserVideos) -) - -meRouter.get('/me/videos/:videoId/rating', - authenticate, - asyncMiddleware(usersVideoRatingValidator), - asyncMiddleware(getUserVideoRating) -) - -meRouter.put('/me', - authenticate, - asyncMiddleware(usersUpdateMeValidator), - asyncRetryTransactionMiddleware(updateMe) -) - -meRouter.post('/me/avatar/pick', - authenticate, - reqAvatarFile, - updateAvatarValidator, - asyncRetryTransactionMiddleware(updateMyAvatar) -) - -meRouter.delete('/me/avatar', - authenticate, - asyncRetryTransactionMiddleware(deleteMyAvatar) -) - -// --------------------------------------------------------------------------- - -export { - meRouter -} - -// --------------------------------------------------------------------------- - -async function getUserVideos (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - const apiOptions = await Hooks.wrapObject({ - accountId: user.Account.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - channelId: res.locals.videoChannel?.id, - isLive: req.query.isLive - }, 'filter:api.user.me.videos.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoModel.listUserVideosForApi, - apiOptions, - 'filter:api.user.me.videos.list.result' - ) - - const additionalAttributes = { - waitTranscoding: true, - state: true, - scheduledUpdate: true, - blacklistInfo: true - } - return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) -} - -async function getUserVideoImports (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const resultList = await VideoImportModel.listUserVideoImportsForApi({ - userId: user.id, - - ...pick(req.query, [ 'targetUrl', 'start', 'count', 'sort', 'search', 'videoChannelSyncId' ]) - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function getUserInformation (req: express.Request, res: express.Response) { - // We did not load channels in res.locals.user - const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id) - - return res.json(user.toMeFormattedJSON()) -} - -async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.user - const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user) - const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user) - - const data: UserVideoQuota = { - videoQuotaUsed, - videoQuotaUsedDaily - } - return res.json(data) -} - -async function getUserVideoRating (req: express.Request, res: express.Response) { - const videoId = res.locals.videoId.id - const accountId = +res.locals.oauth.token.User.Account.id - - const ratingObj = await AccountVideoRateModel.load(accountId, videoId, null) - const rating = ratingObj ? ratingObj.type : 'none' - - const json: FormattedUserVideoRate = { - videoId, - rating - } - return res.json(json) -} - -async function deleteMe (req: express.Request, res: express.Response) { - const user = await UserModel.loadByIdWithChannels(res.locals.oauth.token.User.id) - - auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) - - await user.destroy() - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function updateMe (req: express.Request, res: express.Response) { - const body: UserUpdateMe = req.body - let sendVerificationEmail = false - - const user = res.locals.oauth.token.user - - const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly)[] = [ - 'password', - 'nsfwPolicy', - 'p2pEnabled', - 'autoPlayVideo', - 'autoPlayNextVideo', - 'autoPlayNextVideoPlaylist', - 'videosHistoryEnabled', - 'videoLanguages', - 'theme', - 'noInstanceConfigWarningModal', - 'noAccountSetupWarningModal', - 'noWelcomeModal', - 'emailPublic', - 'p2pEnabled' - ] - - for (const key of keysToUpdate) { - if (body[key] !== undefined) user.set(key, body[key]) - } - - if (body.email !== undefined) { - if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { - user.pendingEmail = body.email - sendVerificationEmail = true - } else { - user.email = body.email - } - } - - await sequelizeTypescript.transaction(async t => { - await user.save({ transaction: t }) - - if (body.displayName === undefined && body.description === undefined) return - - const userAccount = await AccountModel.load(user.Account.id, t) - - if (body.displayName !== undefined) userAccount.name = body.displayName - if (body.description !== undefined) userAccount.description = body.description - await userAccount.save({ transaction: t }) - - await sendUpdateActor(userAccount, t) - }) - - if (sendVerificationEmail === true) { - await sendVerifyUserEmail(user, true) - } - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function updateMyAvatar (req: express.Request, res: express.Response) { - const avatarPhysicalFile = req.files['avatarfile'][0] - const user = res.locals.oauth.token.user - - const userAccount = await AccountModel.load(user.Account.id) - - const avatars = await updateLocalActorImageFiles( - userAccount, - avatarPhysicalFile, - ActorImageType.AVATAR - ) - - return res.json({ - avatars: avatars.map(avatar => avatar.toFormattedJSON()) - }) -} - -async function deleteMyAvatar (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.user - - const userAccount = await AccountModel.load(user.Account.id) - await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) - - return res.json({ avatars: [] }) -} diff --git a/server/controllers/api/users/my-abuses.ts b/server/controllers/api/users/my-abuses.ts deleted file mode 100644 index 103c3d332..000000000 --- a/server/controllers/api/users/my-abuses.ts +++ /dev/null @@ -1,48 +0,0 @@ -import express from 'express' -import { AbuseModel } from '@server/models/abuse/abuse' -import { - abuseListForUserValidator, - abusesSortValidator, - asyncMiddleware, - authenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort -} from '../../../middlewares' - -const myAbusesRouter = express.Router() - -myAbusesRouter.get('/me/abuses', - authenticate, - paginationValidator, - abusesSortValidator, - setDefaultSort, - setDefaultPagination, - abuseListForUserValidator, - asyncMiddleware(listMyAbuses) -) - -// --------------------------------------------------------------------------- - -export { - myAbusesRouter -} - -// --------------------------------------------------------------------------- - -async function listMyAbuses (req: express.Request, res: express.Response) { - const resultList = await AbuseModel.listForUserApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - id: req.query.id, - search: req.query.search, - state: req.query.state, - user: res.locals.oauth.token.User - }) - - return res.json({ - total: resultList.total, - data: resultList.data.map(d => d.toFormattedUserJSON()) - }) -} diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts deleted file mode 100644 index 0b56645cf..000000000 --- a/server/controllers/api/users/my-blocklist.ts +++ /dev/null @@ -1,149 +0,0 @@ -import 'multer' -import express from 'express' -import { logger } from '@server/helpers/logger' -import { UserNotificationModel } from '@server/models/user/user-notification' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { getFormattedObjects } from '../../../helpers/utils' -import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort, - unblockAccountByAccountValidator -} from '../../../middlewares' -import { - accountsBlocklistSortValidator, - blockAccountValidator, - blockServerValidator, - serversBlocklistSortValidator, - unblockServerByAccountValidator -} from '../../../middlewares/validators' -import { AccountBlocklistModel } from '../../../models/account/account-blocklist' -import { ServerBlocklistModel } from '../../../models/server/server-blocklist' - -const myBlocklistRouter = express.Router() - -myBlocklistRouter.get('/me/blocklist/accounts', - authenticate, - paginationValidator, - accountsBlocklistSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listBlockedAccounts) -) - -myBlocklistRouter.post('/me/blocklist/accounts', - authenticate, - asyncMiddleware(blockAccountValidator), - asyncRetryTransactionMiddleware(blockAccount) -) - -myBlocklistRouter.delete('/me/blocklist/accounts/:accountName', - authenticate, - asyncMiddleware(unblockAccountByAccountValidator), - asyncRetryTransactionMiddleware(unblockAccount) -) - -myBlocklistRouter.get('/me/blocklist/servers', - authenticate, - paginationValidator, - serversBlocklistSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listBlockedServers) -) - -myBlocklistRouter.post('/me/blocklist/servers', - authenticate, - asyncMiddleware(blockServerValidator), - asyncRetryTransactionMiddleware(blockServer) -) - -myBlocklistRouter.delete('/me/blocklist/servers/:host', - authenticate, - asyncMiddleware(unblockServerByAccountValidator), - asyncRetryTransactionMiddleware(unblockServer) -) - -export { - myBlocklistRouter -} - -// --------------------------------------------------------------------------- - -async function listBlockedAccounts (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - const resultList = await AccountBlocklistModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - accountId: user.Account.id - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function blockAccount (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const accountToBlock = res.locals.account - - await addAccountInBlocklist(user.Account.id, accountToBlock.id) - - UserNotificationModel.removeNotificationsOf({ - id: accountToBlock.id, - type: 'account', - forUserId: user.id - }).catch(err => logger.error('Cannot remove notifications after an account mute.', { err })) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function unblockAccount (req: express.Request, res: express.Response) { - const accountBlock = res.locals.accountBlock - - await removeAccountFromBlocklist(accountBlock) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function listBlockedServers (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - const resultList = await ServerBlocklistModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - accountId: user.Account.id - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function blockServer (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const serverToBlock = res.locals.server - - await addServerInBlocklist(user.Account.id, serverToBlock.id) - - UserNotificationModel.removeNotificationsOf({ - id: serverToBlock.id, - type: 'server', - forUserId: user.id - }).catch(err => logger.error('Cannot remove notifications after a server mute.', { err })) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function unblockServer (req: express.Request, res: express.Response) { - const serverBlock = res.locals.serverBlock - - await removeServerFromBlocklist(serverBlock) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts deleted file mode 100644 index e6d3e86ac..000000000 --- a/server/controllers/api/users/my-history.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { forceNumber } from '@shared/core-utils' -import express from 'express' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { getFormattedObjects } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers/database' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - paginationValidator, - setDefaultPagination, - userHistoryListValidator, - userHistoryRemoveAllValidator, - userHistoryRemoveElementValidator -} from '../../../middlewares' -import { UserVideoHistoryModel } from '../../../models/user/user-video-history' - -const myVideosHistoryRouter = express.Router() - -myVideosHistoryRouter.get('/me/history/videos', - authenticate, - paginationValidator, - setDefaultPagination, - userHistoryListValidator, - asyncMiddleware(listMyVideosHistory) -) - -myVideosHistoryRouter.delete('/me/history/videos/:videoId', - authenticate, - userHistoryRemoveElementValidator, - asyncMiddleware(removeUserHistoryElement) -) - -myVideosHistoryRouter.post('/me/history/videos/remove', - authenticate, - userHistoryRemoveAllValidator, - asyncRetryTransactionMiddleware(removeAllUserHistory) -) - -// --------------------------------------------------------------------------- - -export { - myVideosHistoryRouter -} - -// --------------------------------------------------------------------------- - -async function listMyVideosHistory (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count, req.query.search) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function removeUserHistoryElement (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - await UserVideoHistoryModel.removeUserHistoryElement(user, forceNumber(req.params.videoId)) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function removeAllUserHistory (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const beforeDate = req.body.beforeDate || null - - await sequelizeTypescript.transaction(t => { - return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t) - }) - - return res.type('json') - .status(HttpStatusCode.NO_CONTENT_204) - .end() -} diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts deleted file mode 100644 index 6014cdbbf..000000000 --- a/server/controllers/api/users/my-notifications.ts +++ /dev/null @@ -1,116 +0,0 @@ -import 'multer' -import express from 'express' -import { UserNotificationModel } from '@server/models/user/user-notification' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { UserNotificationSetting } from '../../../../shared/models/users' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort, - userNotificationsSortValidator -} from '../../../middlewares' -import { - listUserNotificationsValidator, - markAsReadUserNotificationsValidator, - updateNotificationSettingsValidator -} from '../../../middlewares/validators/user-notifications' -import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting' -import { meRouter } from './me' -import { getFormattedObjects } from '@server/helpers/utils' - -const myNotificationsRouter = express.Router() - -meRouter.put('/me/notification-settings', - authenticate, - updateNotificationSettingsValidator, - asyncRetryTransactionMiddleware(updateNotificationSettings) -) - -myNotificationsRouter.get('/me/notifications', - authenticate, - paginationValidator, - userNotificationsSortValidator, - setDefaultSort, - setDefaultPagination, - listUserNotificationsValidator, - asyncMiddleware(listUserNotifications) -) - -myNotificationsRouter.post('/me/notifications/read', - authenticate, - markAsReadUserNotificationsValidator, - asyncMiddleware(markAsReadUserNotifications) -) - -myNotificationsRouter.post('/me/notifications/read-all', - authenticate, - asyncMiddleware(markAsReadAllUserNotifications) -) - -export { - myNotificationsRouter -} - -// --------------------------------------------------------------------------- - -async function updateNotificationSettings (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const body = req.body as UserNotificationSetting - - const query = { - where: { - userId: user.id - } - } - - const values: UserNotificationSetting = { - newVideoFromSubscription: body.newVideoFromSubscription, - newCommentOnMyVideo: body.newCommentOnMyVideo, - abuseAsModerator: body.abuseAsModerator, - videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator, - blacklistOnMyVideo: body.blacklistOnMyVideo, - myVideoPublished: body.myVideoPublished, - myVideoImportFinished: body.myVideoImportFinished, - newFollow: body.newFollow, - newUserRegistration: body.newUserRegistration, - commentMention: body.commentMention, - newInstanceFollower: body.newInstanceFollower, - autoInstanceFollowing: body.autoInstanceFollowing, - abuseNewMessage: body.abuseNewMessage, - abuseStateChange: body.abuseStateChange, - newPeerTubeVersion: body.newPeerTubeVersion, - newPluginVersion: body.newPluginVersion, - myVideoStudioEditionFinished: body.myVideoStudioEditionFinished - } - - await UserNotificationSettingModel.update(values, query) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function listUserNotifications (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function markAsReadUserNotifications (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - await UserNotificationModel.markAsRead(user.id, req.body.ids) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - - await UserNotificationModel.markAllAsRead(user.id) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts deleted file mode 100644 index c4360f59d..000000000 --- a/server/controllers/api/users/my-subscriptions.ts +++ /dev/null @@ -1,193 +0,0 @@ -import 'multer' -import express from 'express' -import { handlesToNameAndHost } from '@server/helpers/actors' -import { pickCommonVideoQuery } from '@server/helpers/query' -import { sendUndoFollow } from '@server/lib/activitypub/send' -import { Hooks } from '@server/lib/plugins/hooks' -import { VideoChannelModel } from '@server/models/video/video-channel' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' -import { getFormattedObjects } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers/database' -import { JobQueue } from '../../../lib/job-queue' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - commonVideosFiltersValidator, - paginationValidator, - setDefaultPagination, - setDefaultSort, - setDefaultVideosSort, - userSubscriptionAddValidator, - userSubscriptionGetValidator -} from '../../../middlewares' -import { - areSubscriptionsExistValidator, - userSubscriptionListValidator, - userSubscriptionsSortValidator, - videosSortValidator -} from '../../../middlewares/validators' -import { ActorFollowModel } from '../../../models/actor/actor-follow' -import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' -import { VideoModel } from '../../../models/video/video' - -const mySubscriptionsRouter = express.Router() - -mySubscriptionsRouter.get('/me/subscriptions/videos', - authenticate, - paginationValidator, - videosSortValidator, - setDefaultVideosSort, - setDefaultPagination, - commonVideosFiltersValidator, - asyncMiddleware(getUserSubscriptionVideos) -) - -mySubscriptionsRouter.get('/me/subscriptions/exist', - authenticate, - areSubscriptionsExistValidator, - asyncMiddleware(areSubscriptionsExist) -) - -mySubscriptionsRouter.get('/me/subscriptions', - authenticate, - paginationValidator, - userSubscriptionsSortValidator, - setDefaultSort, - setDefaultPagination, - userSubscriptionListValidator, - asyncMiddleware(getUserSubscriptions) -) - -mySubscriptionsRouter.post('/me/subscriptions', - authenticate, - userSubscriptionAddValidator, - addUserSubscription -) - -mySubscriptionsRouter.get('/me/subscriptions/:uri', - authenticate, - userSubscriptionGetValidator, - asyncMiddleware(getUserSubscription) -) - -mySubscriptionsRouter.delete('/me/subscriptions/:uri', - authenticate, - userSubscriptionGetValidator, - asyncRetryTransactionMiddleware(deleteUserSubscription) -) - -// --------------------------------------------------------------------------- - -export { - mySubscriptionsRouter -} - -// --------------------------------------------------------------------------- - -async function areSubscriptionsExist (req: express.Request, res: express.Response) { - const uris = req.query.uris as string[] - const user = res.locals.oauth.token.User - - const sanitizedHandles = handlesToNameAndHost(uris) - - const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles) - - const existObject: { [id: string ]: boolean } = {} - for (const sanitizedHandle of sanitizedHandles) { - const obj = results.find(r => { - const server = r.ActorFollowing.Server - - return r.ActorFollowing.preferredUsername.toLowerCase() === sanitizedHandle.name.toLowerCase() && - ( - (!server && !sanitizedHandle.host) || - (server.host === sanitizedHandle.host) - ) - }) - - existObject[sanitizedHandle.handle] = obj !== undefined - } - - return res.json(existObject) -} - -function addUserSubscription (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const [ name, host ] = req.body.uri.split('@') - - const payload = { - name, - host, - assertIsChannel: true, - followerActorId: user.Account.Actor.id - } - - JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function getUserSubscription (req: express.Request, res: express.Response) { - const subscription = res.locals.subscription - const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id) - - return res.json(videoChannel.toFormattedJSON()) -} - -async function deleteUserSubscription (req: express.Request, res: express.Response) { - const subscription = res.locals.subscription - - await sequelizeTypescript.transaction(async t => { - if (subscription.state === 'accepted') { - sendUndoFollow(subscription, t) - } - - return subscription.destroy({ transaction: t }) - }) - - return res.type('json') - .status(HttpStatusCode.NO_CONTENT_204) - .end() -} - -async function getUserSubscriptions (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const actorId = user.Account.Actor.id - - const resultList = await ActorFollowModel.listSubscriptionsForApi({ - actorId, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function getUserSubscriptionVideos (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.User - const countVideos = getCountVideos(req) - const query = pickCommonVideoQuery(req.query) - - const apiOptions = await Hooks.wrapObject({ - ...query, - - displayOnlyForFollower: { - actorId: user.Account.Actor.id, - orLocalVideos: false - }, - nsfw: buildNSFWFilter(res, query.nsfw), - user, - countVideos - }, 'filter:api.user.me.subscription-videos.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoModel.listForApi, - apiOptions, - 'filter:api.user.me.subscription-videos.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) -} diff --git a/server/controllers/api/users/my-video-playlists.ts b/server/controllers/api/users/my-video-playlists.ts deleted file mode 100644 index fbdbb7e50..000000000 --- a/server/controllers/api/users/my-video-playlists.ts +++ /dev/null @@ -1,51 +0,0 @@ -import express from 'express' -import { forceNumber } from '@shared/core-utils' -import { uuidToShort } from '@shared/extra-utils' -import { VideosExistInPlaylists } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' -import { asyncMiddleware, authenticate } from '../../../middlewares' -import { doVideosInPlaylistExistValidator } from '../../../middlewares/validators/videos/video-playlists' -import { VideoPlaylistModel } from '../../../models/video/video-playlist' - -const myVideoPlaylistsRouter = express.Router() - -myVideoPlaylistsRouter.get('/me/video-playlists/videos-exist', - authenticate, - doVideosInPlaylistExistValidator, - asyncMiddleware(doVideosInPlaylistExist) -) - -// --------------------------------------------------------------------------- - -export { - myVideoPlaylistsRouter -} - -// --------------------------------------------------------------------------- - -async function doVideosInPlaylistExist (req: express.Request, res: express.Response) { - const videoIds = req.query.videoIds.map(i => forceNumber(i)) - const user = res.locals.oauth.token.User - - const results = await VideoPlaylistModel.listPlaylistSummariesOf(user.Account.id, videoIds) - - const existObject: VideosExistInPlaylists = {} - - for (const videoId of videoIds) { - existObject[videoId] = [] - } - - for (const result of results) { - for (const element of result.VideoPlaylistElements) { - existObject[element.videoId].push({ - playlistElementId: element.id, - playlistId: result.id, - playlistDisplayName: result.name, - playlistShortUUID: uuidToShort(result.uuid), - startTimestamp: element.startTimestamp, - stopTimestamp: element.stopTimestamp - }) - } - } - - return res.json(existObject) -} diff --git a/server/controllers/api/users/registrations.ts b/server/controllers/api/users/registrations.ts deleted file mode 100644 index 5e213d6cc..000000000 --- a/server/controllers/api/users/registrations.ts +++ /dev/null @@ -1,249 +0,0 @@ -import express from 'express' -import { Emailer } from '@server/lib/emailer' -import { Hooks } from '@server/lib/plugins/hooks' -import { UserRegistrationModel } from '@server/models/user/user-registration' -import { pick } from '@shared/core-utils' -import { - HttpStatusCode, - UserRegister, - UserRegistrationRequest, - UserRegistrationState, - UserRegistrationUpdateState, - UserRight -} from '@shared/models' -import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' -import { logger } from '../../../helpers/logger' -import { CONFIG } from '../../../initializers/config' -import { Notifier } from '../../../lib/notifier' -import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyRegistrationEmail, sendVerifyUserEmail } from '../../../lib/user' -import { - acceptOrRejectRegistrationValidator, - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - buildRateLimiter, - ensureUserHasRight, - ensureUserRegistrationAllowedFactory, - ensureUserRegistrationAllowedForIP, - getRegistrationValidator, - listRegistrationsValidator, - paginationValidator, - setDefaultPagination, - setDefaultSort, - userRegistrationsSortValidator, - usersDirectRegistrationValidator, - usersRequestRegistrationValidator -} from '../../../middlewares' - -const auditLogger = auditLoggerFactory('users') - -const registrationRateLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, - max: CONFIG.RATES_LIMIT.SIGNUP.MAX, - skipFailedRequests: true -}) - -const registrationsRouter = express.Router() - -registrationsRouter.post('/registrations/request', - registrationRateLimiter, - asyncMiddleware(ensureUserRegistrationAllowedFactory('request-registration')), - ensureUserRegistrationAllowedForIP, - asyncMiddleware(usersRequestRegistrationValidator), - asyncRetryTransactionMiddleware(requestRegistration) -) - -registrationsRouter.post('/registrations/:registrationId/accept', - authenticate, - ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), - asyncMiddleware(acceptOrRejectRegistrationValidator), - asyncRetryTransactionMiddleware(acceptRegistration) -) -registrationsRouter.post('/registrations/:registrationId/reject', - authenticate, - ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), - asyncMiddleware(acceptOrRejectRegistrationValidator), - asyncRetryTransactionMiddleware(rejectRegistration) -) - -registrationsRouter.delete('/registrations/:registrationId', - authenticate, - ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), - asyncMiddleware(getRegistrationValidator), - asyncRetryTransactionMiddleware(deleteRegistration) -) - -registrationsRouter.get('/registrations', - authenticate, - ensureUserHasRight(UserRight.MANAGE_REGISTRATIONS), - paginationValidator, - userRegistrationsSortValidator, - setDefaultSort, - setDefaultPagination, - listRegistrationsValidator, - asyncMiddleware(listRegistrations) -) - -registrationsRouter.post('/register', - registrationRateLimiter, - asyncMiddleware(ensureUserRegistrationAllowedFactory('direct-registration')), - ensureUserRegistrationAllowedForIP, - asyncMiddleware(usersDirectRegistrationValidator), - asyncRetryTransactionMiddleware(registerUser) -) - -// --------------------------------------------------------------------------- - -export { - registrationsRouter -} - -// --------------------------------------------------------------------------- - -async function requestRegistration (req: express.Request, res: express.Response) { - const body: UserRegistrationRequest = req.body - - const registration = new UserRegistrationModel({ - ...pick(body, [ 'username', 'password', 'email', 'registrationReason' ]), - - accountDisplayName: body.displayName, - channelDisplayName: body.channel?.displayName, - channelHandle: body.channel?.name, - - state: UserRegistrationState.PENDING, - - emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null - }) - - await registration.save() - - if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { - await sendVerifyRegistrationEmail(registration) - } - - Notifier.Instance.notifyOnNewRegistrationRequest(registration) - - Hooks.runAction('action:api.user.requested-registration', { body, registration, req, res }) - - return res.json(registration.toFormattedJSON()) -} - -// --------------------------------------------------------------------------- - -async function acceptRegistration (req: express.Request, res: express.Response) { - const registration = res.locals.userRegistration - const body: UserRegistrationUpdateState = req.body - - const userToCreate = buildUser({ - username: registration.username, - password: registration.password, - email: registration.email, - emailVerified: registration.emailVerified - }) - // We already encrypted password in registration model - userToCreate.skipPasswordEncryption = true - - // TODO: handle conflicts if someone else created a channel handle/user handle/user email between registration and approval - - const { user } = await createUserAccountAndChannelAndPlaylist({ - userToCreate, - userDisplayName: registration.accountDisplayName, - channelNames: registration.channelHandle && registration.channelDisplayName - ? { - name: registration.channelHandle, - displayName: registration.channelDisplayName - } - : undefined - }) - - registration.userId = user.id - registration.state = UserRegistrationState.ACCEPTED - registration.moderationResponse = body.moderationResponse - - await registration.save() - - logger.info('Registration of %s accepted', registration.username) - - if (body.preventEmailDelivery !== true) { - Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) - } - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function rejectRegistration (req: express.Request, res: express.Response) { - const registration = res.locals.userRegistration - const body: UserRegistrationUpdateState = req.body - - registration.state = UserRegistrationState.REJECTED - registration.moderationResponse = body.moderationResponse - - await registration.save() - - if (body.preventEmailDelivery !== true) { - Emailer.Instance.addUserRegistrationRequestProcessedJob(registration) - } - - logger.info('Registration of %s rejected', registration.username) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -// --------------------------------------------------------------------------- - -async function deleteRegistration (req: express.Request, res: express.Response) { - const registration = res.locals.userRegistration - - await registration.destroy() - - logger.info('Registration of %s deleted', registration.username) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -// --------------------------------------------------------------------------- - -async function listRegistrations (req: express.Request, res: express.Response) { - const resultList = await UserRegistrationModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search - }) - - return res.json({ - total: resultList.total, - data: resultList.data.map(d => d.toFormattedJSON()) - }) -} - -// --------------------------------------------------------------------------- - -async function registerUser (req: express.Request, res: express.Response) { - const body: UserRegister = req.body - - const userToCreate = buildUser({ - ...pick(body, [ 'username', 'password', 'email' ]), - - emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null - }) - - const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ - userToCreate, - userDisplayName: body.displayName || undefined, - channelNames: body.channel - }) - - auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) - logger.info('User %s with its channel and account registered.', body.username) - - if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { - await sendVerifyUserEmail(user) - } - - Notifier.Instance.notifyOnNewDirectRegistration(user) - - Hooks.runAction('action:api.user.registered', { body, user, account, videoChannel, req, res }) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts deleted file mode 100644 index c6afea67c..000000000 --- a/server/controllers/api/users/token.ts +++ /dev/null @@ -1,131 +0,0 @@ -import express from 'express' -import { logger } from '@server/helpers/logger' -import { CONFIG } from '@server/initializers/config' -import { OTP } from '@server/initializers/constants' -import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' -import { handleOAuthToken, MissingTwoFactorError } from '@server/lib/auth/oauth' -import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' -import { Hooks } from '@server/lib/plugins/hooks' -import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares' -import { buildUUID } from '@shared/extra-utils' -import { ScopedToken } from '@shared/models/users/user-scoped-token' - -const tokensRouter = express.Router() - -const loginRateLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, - max: CONFIG.RATES_LIMIT.LOGIN.MAX -}) - -tokensRouter.post('/token', - loginRateLimiter, - openapiOperationDoc({ operationId: 'getOAuthToken' }), - asyncMiddleware(handleToken) -) - -tokensRouter.post('/revoke-token', - openapiOperationDoc({ operationId: 'revokeOAuthToken' }), - authenticate, - asyncMiddleware(handleTokenRevocation) -) - -tokensRouter.get('/scoped-tokens', - authenticate, - getScopedTokens -) - -tokensRouter.post('/scoped-tokens', - authenticate, - asyncMiddleware(renewScopedTokens) -) - -// --------------------------------------------------------------------------- - -export { - tokensRouter -} -// --------------------------------------------------------------------------- - -async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) { - const grantType = req.body.grant_type - - try { - const bypassLogin = await buildByPassLogin(req, grantType) - - const refreshTokenAuthName = grantType === 'refresh_token' - ? await getAuthNameFromRefreshGrant(req.body.refresh_token) - : undefined - - const options = { - refreshTokenAuthName, - bypassLogin - } - - const token = await handleOAuthToken(req, options) - - res.set('Cache-Control', 'no-store') - res.set('Pragma', 'no-cache') - - Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip, req, res }) - - return res.json({ - token_type: 'Bearer', - - access_token: token.accessToken, - refresh_token: token.refreshToken, - - expires_in: token.accessTokenExpiresIn, - refresh_token_expires_in: token.refreshTokenExpiresIn - }) - } catch (err) { - logger.warn('Login error', { err }) - - if (err instanceof MissingTwoFactorError) { - res.set(OTP.HEADER_NAME, OTP.HEADER_REQUIRED_VALUE) - } - - return res.fail({ - status: err.code, - message: err.message, - type: err.name - }) - } -} - -async function handleTokenRevocation (req: express.Request, res: express.Response) { - const token = res.locals.oauth.token - - const result = await revokeToken(token, { req, explicitLogout: true }) - - return res.json(result) -} - -function getScopedTokens (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.user - - return res.json({ - feedToken: user.feedToken - } as ScopedToken) -} - -async function renewScopedTokens (req: express.Request, res: express.Response) { - const user = res.locals.oauth.token.user - - user.feedToken = buildUUID() - await user.save() - - return res.json({ - feedToken: user.feedToken - } as ScopedToken) -} - -async function buildByPassLogin (req: express.Request, grantType: string): Promise { - if (grantType !== 'password') return undefined - - if (req.body.externalAuthToken) { - // Consistency with the getBypassFromPasswordGrant promise - return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken) - } - - return getBypassFromPasswordGrant(req.body.username, req.body.password) -} diff --git a/server/controllers/api/users/two-factor.ts b/server/controllers/api/users/two-factor.ts deleted file mode 100644 index e6ae9e4dd..000000000 --- a/server/controllers/api/users/two-factor.ts +++ /dev/null @@ -1,95 +0,0 @@ -import express from 'express' -import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' -import { encrypt } from '@server/helpers/peertube-crypto' -import { CONFIG } from '@server/initializers/config' -import { Redis } from '@server/lib/redis' -import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares' -import { - confirmTwoFactorValidator, - disableTwoFactorValidator, - requestOrConfirmTwoFactorValidator -} from '@server/middlewares/validators/two-factor' -import { HttpStatusCode, TwoFactorEnableResult } from '@shared/models' - -const twoFactorRouter = express.Router() - -twoFactorRouter.post('/:id/two-factor/request', - authenticate, - asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)), - asyncMiddleware(requestOrConfirmTwoFactorValidator), - asyncMiddleware(requestTwoFactor) -) - -twoFactorRouter.post('/:id/two-factor/confirm-request', - authenticate, - asyncMiddleware(requestOrConfirmTwoFactorValidator), - confirmTwoFactorValidator, - asyncMiddleware(confirmRequestTwoFactor) -) - -twoFactorRouter.post('/:id/two-factor/disable', - authenticate, - asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)), - asyncMiddleware(disableTwoFactorValidator), - asyncMiddleware(disableTwoFactor) -) - -// --------------------------------------------------------------------------- - -export { - twoFactorRouter -} - -// --------------------------------------------------------------------------- - -async function requestTwoFactor (req: express.Request, res: express.Response) { - const user = res.locals.user - - const { secret, uri } = generateOTPSecret(user.email) - - const encryptedSecret = await encrypt(secret, CONFIG.SECRETS.PEERTUBE) - const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, encryptedSecret) - - return res.json({ - otpRequest: { - requestToken, - secret, - uri - } - } as TwoFactorEnableResult) -} - -async function confirmRequestTwoFactor (req: express.Request, res: express.Response) { - const requestToken = req.body.requestToken - const otpToken = req.body.otpToken - const user = res.locals.user - - const encryptedSecret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken) - if (!encryptedSecret) { - return res.fail({ - message: 'Invalid request token', - status: HttpStatusCode.FORBIDDEN_403 - }) - } - - if (await isOTPValid({ encryptedSecret, token: otpToken }) !== true) { - return res.fail({ - message: 'Invalid OTP token', - status: HttpStatusCode.FORBIDDEN_403 - }) - } - - user.otpSecret = encryptedSecret - await user.save() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function disableTwoFactor (req: express.Request, res: express.Response) { - const user = res.locals.user - - user.otpSecret = null - await user.save() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} diff --git a/server/controllers/api/video-channel-sync.ts b/server/controllers/api/video-channel-sync.ts deleted file mode 100644 index 6b52ac7dd..000000000 --- a/server/controllers/api/video-channel-sync.ts +++ /dev/null @@ -1,79 +0,0 @@ -import express from 'express' -import { auditLoggerFactory, getAuditIdFromRes, VideoChannelSyncAuditView } from '@server/helpers/audit-logger' -import { logger } from '@server/helpers/logger' -import { - apiRateLimiter, - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - ensureCanManageChannelOrAccount, - ensureSyncExists, - ensureSyncIsEnabled, - videoChannelSyncValidator -} from '@server/middlewares' -import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' -import { MChannelSyncFormattable } from '@server/types/models' -import { HttpStatusCode, VideoChannelSyncState } from '@shared/models' - -const videoChannelSyncRouter = express.Router() -const auditLogger = auditLoggerFactory('channel-syncs') - -videoChannelSyncRouter.use(apiRateLimiter) - -videoChannelSyncRouter.post('/', - authenticate, - ensureSyncIsEnabled, - asyncMiddleware(videoChannelSyncValidator), - ensureCanManageChannelOrAccount, - asyncRetryTransactionMiddleware(createVideoChannelSync) -) - -videoChannelSyncRouter.delete('/:id', - authenticate, - asyncMiddleware(ensureSyncExists), - ensureCanManageChannelOrAccount, - asyncRetryTransactionMiddleware(removeVideoChannelSync) -) - -export { videoChannelSyncRouter } - -// --------------------------------------------------------------------------- - -async function createVideoChannelSync (req: express.Request, res: express.Response) { - const syncCreated: MChannelSyncFormattable = new VideoChannelSyncModel({ - externalChannelUrl: req.body.externalChannelUrl, - videoChannelId: req.body.videoChannelId, - state: VideoChannelSyncState.WAITING_FIRST_RUN - }) - - await syncCreated.save() - syncCreated.VideoChannel = res.locals.videoChannel - - auditLogger.create(getAuditIdFromRes(res), new VideoChannelSyncAuditView(syncCreated.toFormattedJSON())) - - logger.info( - 'Video synchronization for channel "%s" with external channel "%s" created.', - syncCreated.VideoChannel.name, - syncCreated.externalChannelUrl - ) - - return res.json({ - videoChannelSync: syncCreated.toFormattedJSON() - }) -} - -async function removeVideoChannelSync (req: express.Request, res: express.Response) { - const syncInstance = res.locals.videoChannelSync - - await syncInstance.destroy() - - auditLogger.delete(getAuditIdFromRes(res), new VideoChannelSyncAuditView(syncInstance.toFormattedJSON())) - - logger.info( - 'Video synchronization for channel "%s" with external channel "%s" deleted.', - syncInstance.VideoChannel.name, - syncInstance.externalChannelUrl - ) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts deleted file mode 100644 index 18de5bf6a..000000000 --- a/server/controllers/api/video-channel.ts +++ /dev/null @@ -1,431 +0,0 @@ -import express from 'express' -import { pickCommonVideoQuery } from '@server/helpers/query' -import { Hooks } from '@server/lib/plugins/hooks' -import { ActorFollowModel } from '@server/models/actor/actor-follow' -import { getServerActor } from '@server/models/application/application' -import { MChannelBannerAccountDefault } from '@server/types/models' -import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models' -import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' -import { resetSequelizeInstance } from '../../helpers/database-utils' -import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' -import { logger } from '../../helpers/logger' -import { getFormattedObjects } from '../../helpers/utils' -import { MIMETYPES } from '../../initializers/constants' -import { sequelizeTypescript } from '../../initializers/database' -import { sendUpdateActor } from '../../lib/activitypub/send' -import { JobQueue } from '../../lib/job-queue' -import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor' -import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' -import { - apiRateLimiter, - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - commonVideosFiltersValidator, - ensureCanManageChannelOrAccount, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort, - setDefaultVideosSort, - videoChannelsAddValidator, - videoChannelsRemoveValidator, - videoChannelsSortValidator, - videoChannelsUpdateValidator, - videoPlaylistsSortValidator -} from '../../middlewares' -import { - ensureChannelOwnerCanUpload, - ensureIsLocalChannel, - videoChannelImportVideosValidator, - videoChannelsFollowersSortValidator, - videoChannelsListValidator, - videoChannelsNameWithHostValidator, - videosSortValidator -} from '../../middlewares/validators' -import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' -import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' -import { AccountModel } from '../../models/account/account' -import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter' -import { VideoModel } from '../../models/video/video' -import { VideoChannelModel } from '../../models/video/video-channel' -import { VideoPlaylistModel } from '../../models/video/video-playlist' - -const auditLogger = auditLoggerFactory('channels') -const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) -const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) - -const videoChannelRouter = express.Router() - -videoChannelRouter.use(apiRateLimiter) - -videoChannelRouter.get('/', - paginationValidator, - videoChannelsSortValidator, - setDefaultSort, - setDefaultPagination, - videoChannelsListValidator, - asyncMiddleware(listVideoChannels) -) - -videoChannelRouter.post('/', - authenticate, - asyncMiddleware(videoChannelsAddValidator), - asyncRetryTransactionMiddleware(addVideoChannel) -) - -videoChannelRouter.post('/:nameWithHost/avatar/pick', - authenticate, - reqAvatarFile, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - ensureCanManageChannelOrAccount, - updateAvatarValidator, - asyncMiddleware(updateVideoChannelAvatar) -) - -videoChannelRouter.post('/:nameWithHost/banner/pick', - authenticate, - reqBannerFile, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - ensureCanManageChannelOrAccount, - updateBannerValidator, - asyncMiddleware(updateVideoChannelBanner) -) - -videoChannelRouter.delete('/:nameWithHost/avatar', - authenticate, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - ensureCanManageChannelOrAccount, - asyncMiddleware(deleteVideoChannelAvatar) -) - -videoChannelRouter.delete('/:nameWithHost/banner', - authenticate, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - ensureCanManageChannelOrAccount, - asyncMiddleware(deleteVideoChannelBanner) -) - -videoChannelRouter.put('/:nameWithHost', - authenticate, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - ensureCanManageChannelOrAccount, - videoChannelsUpdateValidator, - asyncRetryTransactionMiddleware(updateVideoChannel) -) - -videoChannelRouter.delete('/:nameWithHost', - authenticate, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - ensureCanManageChannelOrAccount, - asyncMiddleware(videoChannelsRemoveValidator), - asyncRetryTransactionMiddleware(removeVideoChannel) -) - -videoChannelRouter.get('/:nameWithHost', - asyncMiddleware(videoChannelsNameWithHostValidator), - asyncMiddleware(getVideoChannel) -) - -videoChannelRouter.get('/:nameWithHost/video-playlists', - asyncMiddleware(videoChannelsNameWithHostValidator), - paginationValidator, - videoPlaylistsSortValidator, - setDefaultSort, - setDefaultPagination, - commonVideoPlaylistFiltersValidator, - asyncMiddleware(listVideoChannelPlaylists) -) - -videoChannelRouter.get('/:nameWithHost/videos', - asyncMiddleware(videoChannelsNameWithHostValidator), - paginationValidator, - videosSortValidator, - setDefaultVideosSort, - setDefaultPagination, - optionalAuthenticate, - commonVideosFiltersValidator, - asyncMiddleware(listVideoChannelVideos) -) - -videoChannelRouter.get('/:nameWithHost/followers', - authenticate, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureCanManageChannelOrAccount, - paginationValidator, - videoChannelsFollowersSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listVideoChannelFollowers) -) - -videoChannelRouter.post('/:nameWithHost/import-videos', - authenticate, - asyncMiddleware(videoChannelsNameWithHostValidator), - asyncMiddleware(videoChannelImportVideosValidator), - ensureIsLocalChannel, - ensureCanManageChannelOrAccount, - asyncMiddleware(ensureChannelOwnerCanUpload), - asyncMiddleware(importVideosInChannel) -) - -// --------------------------------------------------------------------------- - -export { - videoChannelRouter -} - -// --------------------------------------------------------------------------- - -async function listVideoChannels (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - const apiOptions = await Hooks.wrapObject({ - actorId: serverActor.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort - }, 'filter:api.video-channels.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoChannelModel.listForApi, - apiOptions, - 'filter:api.video-channels.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function updateVideoChannelBanner (req: express.Request, res: express.Response) { - const bannerPhysicalFile = req.files['bannerfile'][0] - const videoChannel = res.locals.videoChannel - const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) - - const banners = await updateLocalActorImageFiles(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) - - auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) - - return res.json({ - banners: banners.map(b => b.toFormattedJSON()) - }) -} - -async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { - const avatarPhysicalFile = req.files['avatarfile'][0] - const videoChannel = res.locals.videoChannel - const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) - - const avatars = await updateLocalActorImageFiles(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) - auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) - - return res.json({ - avatars: avatars.map(a => a.toFormattedJSON()) - }) -} - -async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { - const videoChannel = res.locals.videoChannel - - await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { - const videoChannel = res.locals.videoChannel - - await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function addVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInfo: VideoChannelCreate = req.body - - const videoChannelCreated = await sequelizeTypescript.transaction(async t => { - const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) - - return createLocalVideoChannel(videoChannelInfo, account, t) - }) - - const payload = { actorId: videoChannelCreated.actorId } - await JobQueue.Instance.createJob({ type: 'actor-keys', payload }) - - auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON())) - logger.info('Video channel %s created.', videoChannelCreated.Actor.url) - - Hooks.runAction('action:api.video-channel.created', { videoChannel: videoChannelCreated, req, res }) - - return res.json({ - videoChannel: { - id: videoChannelCreated.id - } - }) -} - -async function updateVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInstance = res.locals.videoChannel - const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()) - const videoChannelInfoToUpdate = req.body as VideoChannelUpdate - let doBulkVideoUpdate = false - - try { - await sequelizeTypescript.transaction(async t => { - if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName - if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description - - if (videoChannelInfoToUpdate.support !== undefined) { - const oldSupportField = videoChannelInstance.support - videoChannelInstance.support = videoChannelInfoToUpdate.support - - if (videoChannelInfoToUpdate.bulkVideosSupportUpdate === true && oldSupportField !== videoChannelInfoToUpdate.support) { - doBulkVideoUpdate = true - await VideoModel.bulkUpdateSupportField(videoChannelInstance, t) - } - } - - const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault - await sendUpdateActor(videoChannelInstanceUpdated, t) - - auditLogger.update( - getAuditIdFromRes(res), - new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()), - oldVideoChannelAuditKeys - ) - - Hooks.runAction('action:api.video-channel.updated', { videoChannel: videoChannelInstanceUpdated, req, res }) - - logger.info('Video channel %s updated.', videoChannelInstance.Actor.url) - }) - } catch (err) { - logger.debug('Cannot update the video channel.', { err }) - - // If the transaction is retried, sequelize will think the object has not changed - // So we need to restore the previous fields - await resetSequelizeInstance(videoChannelInstance) - - throw err - } - - res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() - - // Don't process in a transaction, and after the response because it could be long - if (doBulkVideoUpdate) { - await federateAllVideosOfChannel(videoChannelInstance) - } -} - -async function removeVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInstance = res.locals.videoChannel - - await sequelizeTypescript.transaction(async t => { - await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t) - - await videoChannelInstance.destroy({ transaction: t }) - - Hooks.runAction('action:api.video-channel.deleted', { videoChannel: videoChannelInstance, req, res }) - - auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())) - logger.info('Video channel %s deleted.', videoChannelInstance.Actor.url) - }) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function getVideoChannel (req: express.Request, res: express.Response) { - const id = res.locals.videoChannel.id - const videoChannel = await Hooks.wrapObject(res.locals.videoChannel, 'filter:api.video-channel.get.result', { id }) - - if (videoChannel.isOutdated()) { - JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } }) - } - - return res.json(videoChannel.toFormattedJSON()) -} - -async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - const resultList = await VideoPlaylistModel.listForApi({ - followerActorId: serverActor.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - videoChannelId: res.locals.videoChannel.id, - type: req.query.playlistType - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listVideoChannelVideos (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - const videoChannelInstance = res.locals.videoChannel - - const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res) - ? null - : { - actorId: serverActor.id, - orLocalVideos: true - } - - const countVideos = getCountVideos(req) - const query = pickCommonVideoQuery(req.query) - - const apiOptions = await Hooks.wrapObject({ - ...query, - - displayOnlyForFollower, - nsfw: buildNSFWFilter(res, query.nsfw), - videoChannelId: videoChannelInstance.id, - user: res.locals.oauth ? res.locals.oauth.token.User : undefined, - countVideos - }, 'filter:api.video-channels.videos.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoModel.listForApi, - apiOptions, - 'filter:api.video-channels.videos.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) -} - -async function listVideoChannelFollowers (req: express.Request, res: express.Response) { - const channel = res.locals.videoChannel - - const resultList = await ActorFollowModel.listFollowersForApi({ - actorIds: [ channel.actorId ], - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - state: 'accepted' - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function importVideosInChannel (req: express.Request, res: express.Response) { - const { externalChannelUrl } = req.body as VideosImportInChannelCreate - - await JobQueue.Instance.createJob({ - type: 'video-channel-import', - payload: { - externalChannelUrl, - videoChannelId: res.locals.videoChannel.id, - partOfChannelSyncId: res.locals.videoChannelSync?.id - } - }) - - logger.info('Video import job for channel "%s" with url "%s" created.', res.locals.videoChannel.name, externalChannelUrl) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts deleted file mode 100644 index 73362e1e3..000000000 --- a/server/controllers/api/video-playlist.ts +++ /dev/null @@ -1,514 +0,0 @@ -import express from 'express' -import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' -import { VideoMiniaturePermanentFileCache } from '@server/lib/files-cache' -import { Hooks } from '@server/lib/plugins/hooks' -import { getServerActor } from '@server/models/application/application' -import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' -import { forceNumber } from '@shared/core-utils' -import { uuidToShort } from '@shared/extra-utils' -import { VideoPlaylistCreateResult, VideoPlaylistElementCreateResult } from '@shared/models' -import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' -import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' -import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' -import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' -import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' -import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model' -import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' -import { resetSequelizeInstance } from '../../helpers/database-utils' -import { createReqFiles } from '../../helpers/express-utils' -import { logger } from '../../helpers/logger' -import { getFormattedObjects } from '../../helpers/utils' -import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' -import { sequelizeTypescript } from '../../initializers/database' -import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' -import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' -import { updateLocalPlaylistMiniatureFromExisting } from '../../lib/thumbnail' -import { - apiRateLimiter, - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort -} from '../../middlewares' -import { videoPlaylistsSortValidator } from '../../middlewares/validators' -import { - commonVideoPlaylistFiltersValidator, - videoPlaylistsAddValidator, - videoPlaylistsAddVideoValidator, - videoPlaylistsDeleteValidator, - videoPlaylistsGetValidator, - videoPlaylistsReorderVideosValidator, - videoPlaylistsUpdateOrRemoveVideoValidator, - videoPlaylistsUpdateValidator -} from '../../middlewares/validators/videos/video-playlists' -import { AccountModel } from '../../models/account/account' -import { VideoPlaylistModel } from '../../models/video/video-playlist' -import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' - -const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) - -const videoPlaylistRouter = express.Router() - -videoPlaylistRouter.use(apiRateLimiter) - -videoPlaylistRouter.get('/privacies', listVideoPlaylistPrivacies) - -videoPlaylistRouter.get('/', - paginationValidator, - videoPlaylistsSortValidator, - setDefaultSort, - setDefaultPagination, - commonVideoPlaylistFiltersValidator, - asyncMiddleware(listVideoPlaylists) -) - -videoPlaylistRouter.get('/:playlistId', - asyncMiddleware(videoPlaylistsGetValidator('summary')), - getVideoPlaylist -) - -videoPlaylistRouter.post('/', - authenticate, - reqThumbnailFile, - asyncMiddleware(videoPlaylistsAddValidator), - asyncRetryTransactionMiddleware(addVideoPlaylist) -) - -videoPlaylistRouter.put('/:playlistId', - authenticate, - reqThumbnailFile, - asyncMiddleware(videoPlaylistsUpdateValidator), - asyncRetryTransactionMiddleware(updateVideoPlaylist) -) - -videoPlaylistRouter.delete('/:playlistId', - authenticate, - asyncMiddleware(videoPlaylistsDeleteValidator), - asyncRetryTransactionMiddleware(removeVideoPlaylist) -) - -videoPlaylistRouter.get('/:playlistId/videos', - asyncMiddleware(videoPlaylistsGetValidator('summary')), - paginationValidator, - setDefaultPagination, - optionalAuthenticate, - asyncMiddleware(getVideoPlaylistVideos) -) - -videoPlaylistRouter.post('/:playlistId/videos', - authenticate, - asyncMiddleware(videoPlaylistsAddVideoValidator), - asyncRetryTransactionMiddleware(addVideoInPlaylist) -) - -videoPlaylistRouter.post('/:playlistId/videos/reorder', - authenticate, - asyncMiddleware(videoPlaylistsReorderVideosValidator), - asyncRetryTransactionMiddleware(reorderVideosPlaylist) -) - -videoPlaylistRouter.put('/:playlistId/videos/:playlistElementId', - authenticate, - asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), - asyncRetryTransactionMiddleware(updateVideoPlaylistElement) -) - -videoPlaylistRouter.delete('/:playlistId/videos/:playlistElementId', - authenticate, - asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), - asyncRetryTransactionMiddleware(removeVideoFromPlaylist) -) - -// --------------------------------------------------------------------------- - -export { - videoPlaylistRouter -} - -// --------------------------------------------------------------------------- - -function listVideoPlaylistPrivacies (req: express.Request, res: express.Response) { - res.json(VIDEO_PLAYLIST_PRIVACIES) -} - -async function listVideoPlaylists (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - const resultList = await VideoPlaylistModel.listForApi({ - followerActorId: serverActor.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - type: req.query.playlistType - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -function getVideoPlaylist (req: express.Request, res: express.Response) { - const videoPlaylist = res.locals.videoPlaylistSummary - - scheduleRefreshIfNeeded(videoPlaylist) - - return res.json(videoPlaylist.toFormattedJSON()) -} - -async function addVideoPlaylist (req: express.Request, res: express.Response) { - const videoPlaylistInfo: VideoPlaylistCreate = req.body - const user = res.locals.oauth.token.User - - const videoPlaylist = new VideoPlaylistModel({ - name: videoPlaylistInfo.displayName, - description: videoPlaylistInfo.description, - privacy: videoPlaylistInfo.privacy || VideoPlaylistPrivacy.PRIVATE, - ownerAccountId: user.Account.id - }) as MVideoPlaylistFull - - videoPlaylist.url = getLocalVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object - - if (videoPlaylistInfo.videoChannelId) { - const videoChannel = res.locals.videoChannel - - videoPlaylist.videoChannelId = videoChannel.id - videoPlaylist.VideoChannel = videoChannel - } - - const thumbnailField = req.files['thumbnailfile'] - const thumbnailModel = thumbnailField - ? await updateLocalPlaylistMiniatureFromExisting({ - inputPath: thumbnailField[0].path, - playlist: videoPlaylist, - automaticallyGenerated: false - }) - : undefined - - const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => { - const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) as MVideoPlaylistFull - - if (thumbnailModel) { - thumbnailModel.automaticallyGenerated = false - await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t) - } - - // We need more attributes for the federation - videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) - await sendCreateVideoPlaylist(videoPlaylistCreated, t) - - return videoPlaylistCreated - }) - - logger.info('Video playlist with uuid %s created.', videoPlaylist.uuid) - - return res.json({ - videoPlaylist: { - id: videoPlaylistCreated.id, - shortUUID: uuidToShort(videoPlaylistCreated.uuid), - uuid: videoPlaylistCreated.uuid - } as VideoPlaylistCreateResult - }) -} - -async function updateVideoPlaylist (req: express.Request, res: express.Response) { - const videoPlaylistInstance = res.locals.videoPlaylistFull - const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate - - const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE - const wasNotPrivatePlaylist = videoPlaylistInstance.privacy !== VideoPlaylistPrivacy.PRIVATE - - const thumbnailField = req.files['thumbnailfile'] - const thumbnailModel = thumbnailField - ? await updateLocalPlaylistMiniatureFromExisting({ - inputPath: thumbnailField[0].path, - playlist: videoPlaylistInstance, - automaticallyGenerated: false - }) - : undefined - - try { - await sequelizeTypescript.transaction(async t => { - const sequelizeOptions = { - transaction: t - } - - if (videoPlaylistInfoToUpdate.videoChannelId !== undefined) { - if (videoPlaylistInfoToUpdate.videoChannelId === null) { - videoPlaylistInstance.videoChannelId = null - } else { - const videoChannel = res.locals.videoChannel - - videoPlaylistInstance.videoChannelId = videoChannel.id - videoPlaylistInstance.VideoChannel = videoChannel - } - } - - if (videoPlaylistInfoToUpdate.displayName !== undefined) videoPlaylistInstance.name = videoPlaylistInfoToUpdate.displayName - if (videoPlaylistInfoToUpdate.description !== undefined) videoPlaylistInstance.description = videoPlaylistInfoToUpdate.description - - if (videoPlaylistInfoToUpdate.privacy !== undefined) { - videoPlaylistInstance.privacy = forceNumber(videoPlaylistInfoToUpdate.privacy) - - if (wasNotPrivatePlaylist === true && videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE) { - await sendDeleteVideoPlaylist(videoPlaylistInstance, t) - } - } - - const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) - - if (thumbnailModel) { - thumbnailModel.automaticallyGenerated = false - await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t) - } - - const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE - - if (isNewPlaylist) { - await sendCreateVideoPlaylist(playlistUpdated, t) - } else { - await sendUpdateVideoPlaylist(playlistUpdated, t) - } - - logger.info('Video playlist %s updated.', videoPlaylistInstance.uuid) - - return playlistUpdated - }) - } catch (err) { - logger.debug('Cannot update the video playlist.', { err }) - - // If the transaction is retried, sequelize will think the object has not changed - // So we need to restore the previous fields - await resetSequelizeInstance(videoPlaylistInstance) - - throw err - } - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function removeVideoPlaylist (req: express.Request, res: express.Response) { - const videoPlaylistInstance = res.locals.videoPlaylistSummary - - await sequelizeTypescript.transaction(async t => { - await videoPlaylistInstance.destroy({ transaction: t }) - - await sendDeleteVideoPlaylist(videoPlaylistInstance, t) - - logger.info('Video playlist %s deleted.', videoPlaylistInstance.uuid) - }) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function addVideoInPlaylist (req: express.Request, res: express.Response) { - const body: VideoPlaylistElementCreate = req.body - const videoPlaylist = res.locals.videoPlaylistFull - const video = res.locals.onlyVideo - - const playlistElement = await sequelizeTypescript.transaction(async t => { - const position = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id, t) - - const playlistElement = await VideoPlaylistElementModel.create({ - position, - startTimestamp: body.startTimestamp || null, - stopTimestamp: body.stopTimestamp || null, - videoPlaylistId: videoPlaylist.id, - videoId: video.id - }, { transaction: t }) - - playlistElement.url = getLocalVideoPlaylistElementActivityPubUrl(videoPlaylist, playlistElement) - await playlistElement.save({ transaction: t }) - - videoPlaylist.changed('updatedAt', true) - await videoPlaylist.save({ transaction: t }) - - return playlistElement - }) - - // If the user did not set a thumbnail, automatically take the video thumbnail - if (videoPlaylist.hasThumbnail() === false || (videoPlaylist.hasGeneratedThumbnail() && playlistElement.position === 1)) { - await generateThumbnailForPlaylist(videoPlaylist, video) - } - - sendUpdateVideoPlaylist(videoPlaylist, undefined) - .catch(err => logger.error('Cannot send video playlist update.', { err })) - - logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) - - Hooks.runAction('action:api.video-playlist-element.created', { playlistElement, req, res }) - - return res.json({ - videoPlaylistElement: { - id: playlistElement.id - } as VideoPlaylistElementCreateResult - }) -} - -async function updateVideoPlaylistElement (req: express.Request, res: express.Response) { - const body: VideoPlaylistElementUpdate = req.body - const videoPlaylist = res.locals.videoPlaylistFull - const videoPlaylistElement = res.locals.videoPlaylistElement - - const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => { - if (body.startTimestamp !== undefined) videoPlaylistElement.startTimestamp = body.startTimestamp - if (body.stopTimestamp !== undefined) videoPlaylistElement.stopTimestamp = body.stopTimestamp - - const element = await videoPlaylistElement.save({ transaction: t }) - - videoPlaylist.changed('updatedAt', true) - await videoPlaylist.save({ transaction: t }) - - await sendUpdateVideoPlaylist(videoPlaylist, t) - - return element - }) - - logger.info('Element of position %d of playlist %s updated.', playlistElement.position, videoPlaylist.uuid) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function removeVideoFromPlaylist (req: express.Request, res: express.Response) { - const videoPlaylistElement = res.locals.videoPlaylistElement - const videoPlaylist = res.locals.videoPlaylistFull - const positionToDelete = videoPlaylistElement.position - - await sequelizeTypescript.transaction(async t => { - await videoPlaylistElement.destroy({ transaction: t }) - - // Decrease position of the next elements - await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, -1, t) - - videoPlaylist.changed('updatedAt', true) - await videoPlaylist.save({ transaction: t }) - - logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid) - }) - - // Do we need to regenerate the default thumbnail? - if (positionToDelete === 1 && videoPlaylist.hasGeneratedThumbnail()) { - await regeneratePlaylistThumbnail(videoPlaylist) - } - - sendUpdateVideoPlaylist(videoPlaylist, undefined) - .catch(err => logger.error('Cannot send video playlist update.', { err })) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function reorderVideosPlaylist (req: express.Request, res: express.Response) { - const videoPlaylist = res.locals.videoPlaylistFull - const body: VideoPlaylistReorder = req.body - - const start: number = body.startPosition - const insertAfter: number = body.insertAfterPosition - const reorderLength: number = body.reorderLength || 1 - - if (start === insertAfter) { - return res.status(HttpStatusCode.NO_CONTENT_204).end() - } - - // Example: if we reorder position 2 and insert after position 5 (so at position 6): # 1 2 3 4 5 6 7 8 9 - // * increase position when position > 5 # 1 2 3 4 5 7 8 9 10 - // * update position 2 -> position 6 # 1 3 4 5 6 7 8 9 10 - // * decrease position when position position > 2 # 1 2 3 4 5 6 7 8 9 - await sequelizeTypescript.transaction(async t => { - const newPosition = insertAfter + 1 - - // Add space after the position when we want to insert our reordered elements (increase) - await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, newPosition, reorderLength, t) - - let oldPosition = start - - // We incremented the position of the elements we want to reorder - if (start >= newPosition) oldPosition += reorderLength - - const endOldPosition = oldPosition + reorderLength - 1 - // Insert our reordered elements in their place (update) - await VideoPlaylistElementModel.reassignPositionOf({ - videoPlaylistId: videoPlaylist.id, - firstPosition: oldPosition, - endPosition: endOldPosition, - newPosition, - transaction: t - }) - - // Decrease positions of elements after the old position of our ordered elements (decrease) - await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, -reorderLength, t) - - videoPlaylist.changed('updatedAt', true) - await videoPlaylist.save({ transaction: t }) - - await sendUpdateVideoPlaylist(videoPlaylist, t) - }) - - // The first element changed - if ((start === 1 || insertAfter === 0) && videoPlaylist.hasGeneratedThumbnail()) { - await regeneratePlaylistThumbnail(videoPlaylist) - } - - logger.info( - 'Reordered playlist %s (inserted after position %d elements %d - %d).', - videoPlaylist.uuid, insertAfter, start, start + reorderLength - 1 - ) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { - const videoPlaylistInstance = res.locals.videoPlaylistSummary - const user = res.locals.oauth ? res.locals.oauth.token.User : undefined - const server = await getServerActor() - - const apiOptions = await Hooks.wrapObject({ - start: req.query.start, - count: req.query.count, - videoPlaylistId: videoPlaylistInstance.id, - serverAccount: server.Account, - user - }, 'filter:api.video-playlist.videos.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoPlaylistElementModel.listForApi, - apiOptions, - 'filter:api.video-playlist.videos.list.result' - ) - - const options = { accountId: user?.Account?.id } - return res.json(getFormattedObjects(resultList.data, resultList.total, options)) -} - -async function regeneratePlaylistThumbnail (videoPlaylist: MVideoPlaylistThumbnail) { - await videoPlaylist.Thumbnail.destroy() - videoPlaylist.Thumbnail = null - - const firstElement = await VideoPlaylistElementModel.loadFirstElementWithVideoThumbnail(videoPlaylist.id) - if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video) -} - -async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbnail, video: MVideoThumbnail) { - logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) - - const videoMiniature = video.getMiniature() - if (!videoMiniature) { - logger.info('Cannot generate thumbnail for playlist %s because video %s does not have any.', videoPlaylist.url, video.url) - return - } - - // Ensure the file is on disk - const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache() - const inputPath = videoMiniature.isOwned() - ? videoMiniature.getPath() - : await videoMiniaturePermanentFileCache.downloadRemoteFile(videoMiniature) - - const thumbnailModel = await updateLocalPlaylistMiniatureFromExisting({ - inputPath, - playlist: videoPlaylist, - automaticallyGenerated: true, - keepOriginal: true - }) - - thumbnailModel.videoPlaylistId = videoPlaylist.id - - videoPlaylist.Thumbnail = await thumbnailModel.save() -} diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts deleted file mode 100644 index 4103bb063..000000000 --- a/server/controllers/api/videos/blacklist.ts +++ /dev/null @@ -1,112 +0,0 @@ -import express from 'express' -import { blacklistVideo, unblacklistVideo } from '@server/lib/video-blacklist' -import { HttpStatusCode, UserRight, VideoBlacklistCreate } from '@shared/models' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers/database' -import { - asyncMiddleware, - authenticate, - blacklistSortValidator, - ensureUserHasRight, - openapiOperationDoc, - paginationValidator, - setBlacklistSort, - setDefaultPagination, - videosBlacklistAddValidator, - videosBlacklistFiltersValidator, - videosBlacklistRemoveValidator, - videosBlacklistUpdateValidator -} from '../../../middlewares' -import { VideoBlacklistModel } from '../../../models/video/video-blacklist' - -const blacklistRouter = express.Router() - -blacklistRouter.post('/:videoId/blacklist', - openapiOperationDoc({ operationId: 'addVideoBlock' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), - asyncMiddleware(videosBlacklistAddValidator), - asyncMiddleware(addVideoToBlacklistController) -) - -blacklistRouter.get('/blacklist', - openapiOperationDoc({ operationId: 'getVideoBlocks' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), - paginationValidator, - blacklistSortValidator, - setBlacklistSort, - setDefaultPagination, - videosBlacklistFiltersValidator, - asyncMiddleware(listBlacklist) -) - -blacklistRouter.put('/:videoId/blacklist', - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), - asyncMiddleware(videosBlacklistUpdateValidator), - asyncMiddleware(updateVideoBlacklistController) -) - -blacklistRouter.delete('/:videoId/blacklist', - openapiOperationDoc({ operationId: 'delVideoBlock' }), - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), - asyncMiddleware(videosBlacklistRemoveValidator), - asyncMiddleware(removeVideoFromBlacklistController) -) - -// --------------------------------------------------------------------------- - -export { - blacklistRouter -} - -// --------------------------------------------------------------------------- - -async function addVideoToBlacklistController (req: express.Request, res: express.Response) { - const videoInstance = res.locals.videoAll - const body: VideoBlacklistCreate = req.body - - await blacklistVideo(videoInstance, body) - - logger.info('Video %s blacklisted.', videoInstance.uuid) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function updateVideoBlacklistController (req: express.Request, res: express.Response) { - const videoBlacklist = res.locals.videoBlacklist - - if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason - - await sequelizeTypescript.transaction(t => { - return videoBlacklist.save({ transaction: t }) - }) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function listBlacklist (req: express.Request, res: express.Response) { - const resultList = await VideoBlacklistModel.listForApi({ - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - search: req.query.search, - type: req.query.type - }) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function removeVideoFromBlacklistController (req: express.Request, res: express.Response) { - const videoBlacklist = res.locals.videoBlacklist - const video = res.locals.videoAll - - await unblacklistVideo(videoBlacklist, video) - - logger.info('Video %s removed from blacklist.', video.uuid) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts deleted file mode 100644 index 2b511a398..000000000 --- a/server/controllers/api/videos/captions.ts +++ /dev/null @@ -1,93 +0,0 @@ -import express from 'express' -import { Hooks } from '@server/lib/plugins/hooks' -import { MVideoCaption } from '@server/types/models' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' -import { createReqFiles } from '../../../helpers/express-utils' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { MIMETYPES } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { federateVideoIfNeeded } from '../../../lib/activitypub/videos' -import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' -import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators' -import { VideoCaptionModel } from '../../../models/video/video-caption' - -const reqVideoCaptionAdd = createReqFiles([ 'captionfile' ], MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) - -const videoCaptionsRouter = express.Router() - -videoCaptionsRouter.get('/:videoId/captions', - asyncMiddleware(listVideoCaptionsValidator), - asyncMiddleware(listVideoCaptions) -) -videoCaptionsRouter.put('/:videoId/captions/:captionLanguage', - authenticate, - reqVideoCaptionAdd, - asyncMiddleware(addVideoCaptionValidator), - asyncRetryTransactionMiddleware(addVideoCaption) -) -videoCaptionsRouter.delete('/:videoId/captions/:captionLanguage', - authenticate, - asyncMiddleware(deleteVideoCaptionValidator), - asyncRetryTransactionMiddleware(deleteVideoCaption) -) - -// --------------------------------------------------------------------------- - -export { - videoCaptionsRouter -} - -// --------------------------------------------------------------------------- - -async function listVideoCaptions (req: express.Request, res: express.Response) { - const data = await VideoCaptionModel.listVideoCaptions(res.locals.onlyVideo.id) - - return res.json(getFormattedObjects(data, data.length)) -} - -async function addVideoCaption (req: express.Request, res: express.Response) { - const videoCaptionPhysicalFile = req.files['captionfile'][0] - const video = res.locals.videoAll - - const captionLanguage = req.params.captionLanguage - - const videoCaption = new VideoCaptionModel({ - videoId: video.id, - filename: VideoCaptionModel.generateCaptionName(captionLanguage), - language: captionLanguage - }) as MVideoCaption - - // Move physical file - await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption) - - await sequelizeTypescript.transaction(async t => { - await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t) - - // Update video update - await federateVideoIfNeeded(video, false, t) - }) - - Hooks.runAction('action:api.video-caption.created', { caption: videoCaption, req, res }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function deleteVideoCaption (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - const videoCaption = res.locals.videoCaption - - await sequelizeTypescript.transaction(async t => { - await videoCaption.destroy({ transaction: t }) - - // Send video update - await federateVideoIfNeeded(video, false, t) - }) - - logger.info('Video caption %s of video %s deleted.', videoCaption.language, video.uuid) - - Hooks.runAction('action:api.video-caption.deleted', { caption: videoCaption, req, res }) - - return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts deleted file mode 100644 index 70ca21500..000000000 --- a/server/controllers/api/videos/comment.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { MCommentFormattable } from '@server/types/models' -import express from 'express' - -import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model' -import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers/database' -import { Notifier } from '../../../lib/notifier' -import { Hooks } from '../../../lib/plugins/hooks' -import { buildFormattedCommentTree, createVideoComment, removeComment } from '../../../lib/video-comment' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - ensureUserHasRight, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultSort -} from '../../../middlewares' -import { - addVideoCommentReplyValidator, - addVideoCommentThreadValidator, - listVideoCommentsValidator, - listVideoCommentThreadsValidator, - listVideoThreadCommentsValidator, - removeVideoCommentValidator, - videoCommentsValidator, - videoCommentThreadsSortValidator -} from '../../../middlewares/validators' -import { AccountModel } from '../../../models/account/account' -import { VideoCommentModel } from '../../../models/video/video-comment' - -const auditLogger = auditLoggerFactory('comments') -const videoCommentRouter = express.Router() - -videoCommentRouter.get('/:videoId/comment-threads', - paginationValidator, - videoCommentThreadsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listVideoCommentThreadsValidator), - optionalAuthenticate, - asyncMiddleware(listVideoThreads) -) -videoCommentRouter.get('/:videoId/comment-threads/:threadId', - asyncMiddleware(listVideoThreadCommentsValidator), - optionalAuthenticate, - asyncMiddleware(listVideoThreadComments) -) - -videoCommentRouter.post('/:videoId/comment-threads', - authenticate, - asyncMiddleware(addVideoCommentThreadValidator), - asyncRetryTransactionMiddleware(addVideoCommentThread) -) -videoCommentRouter.post('/:videoId/comments/:commentId', - authenticate, - asyncMiddleware(addVideoCommentReplyValidator), - asyncRetryTransactionMiddleware(addVideoCommentReply) -) -videoCommentRouter.delete('/:videoId/comments/:commentId', - authenticate, - asyncMiddleware(removeVideoCommentValidator), - asyncRetryTransactionMiddleware(removeVideoComment) -) - -videoCommentRouter.get('/comments', - authenticate, - ensureUserHasRight(UserRight.SEE_ALL_COMMENTS), - paginationValidator, - videoCommentsValidator, - setDefaultSort, - setDefaultPagination, - listVideoCommentsValidator, - asyncMiddleware(listComments) -) - -// --------------------------------------------------------------------------- - -export { - videoCommentRouter -} - -// --------------------------------------------------------------------------- - -async function listComments (req: express.Request, res: express.Response) { - const options = { - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - - isLocal: req.query.isLocal, - onLocalVideo: req.query.onLocalVideo, - search: req.query.search, - searchAccount: req.query.searchAccount, - searchVideo: req.query.searchVideo - } - - const resultList = await VideoCommentModel.listCommentsForApi(options) - - return res.json({ - total: resultList.total, - data: resultList.data.map(c => c.toFormattedAdminJSON()) - }) -} - -async function listVideoThreads (req: express.Request, res: express.Response) { - const video = res.locals.onlyVideo - const user = res.locals.oauth ? res.locals.oauth.token.User : undefined - - let resultList: ThreadsResultList - - if (video.commentsEnabled === true) { - const apiOptions = await Hooks.wrapObject({ - videoId: video.id, - isVideoOwned: video.isOwned(), - start: req.query.start, - count: req.query.count, - sort: req.query.sort, - user - }, 'filter:api.video-threads.list.params') - - resultList = await Hooks.wrapPromiseFun( - VideoCommentModel.listThreadsForApi, - apiOptions, - 'filter:api.video-threads.list.result' - ) - } else { - resultList = { - total: 0, - totalNotDeletedComments: 0, - data: [] - } - } - - return res.json({ - ...getFormattedObjects(resultList.data, resultList.total), - totalNotDeletedComments: resultList.totalNotDeletedComments - } as VideoCommentThreads) -} - -async function listVideoThreadComments (req: express.Request, res: express.Response) { - const video = res.locals.onlyVideo - const user = res.locals.oauth ? res.locals.oauth.token.User : undefined - - let resultList: ResultList - - if (video.commentsEnabled === true) { - const apiOptions = await Hooks.wrapObject({ - videoId: video.id, - threadId: res.locals.videoCommentThread.id, - user - }, 'filter:api.video-thread-comments.list.params') - - resultList = await Hooks.wrapPromiseFun( - VideoCommentModel.listThreadCommentsForApi, - apiOptions, - 'filter:api.video-thread-comments.list.result' - ) - } else { - resultList = { - total: 0, - data: [] - } - } - - if (resultList.data.length === 0) { - return res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'No comments were found' - }) - } - - return res.json(buildFormattedCommentTree(resultList)) -} - -async function addVideoCommentThread (req: express.Request, res: express.Response) { - const videoCommentInfo: VideoCommentCreate = req.body - - const comment = await sequelizeTypescript.transaction(async t => { - const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) - - return createVideoComment({ - text: videoCommentInfo.text, - inReplyToComment: null, - video: res.locals.videoAll, - account - }, t) - }) - - Notifier.Instance.notifyOnNewComment(comment) - auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) - - Hooks.runAction('action:api.video-thread.created', { comment, req, res }) - - return res.json({ comment: comment.toFormattedJSON() }) -} - -async function addVideoCommentReply (req: express.Request, res: express.Response) { - const videoCommentInfo: VideoCommentCreate = req.body - - const comment = await sequelizeTypescript.transaction(async t => { - const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) - - return createVideoComment({ - text: videoCommentInfo.text, - inReplyToComment: res.locals.videoCommentFull, - video: res.locals.videoAll, - account - }, t) - }) - - Notifier.Instance.notifyOnNewComment(comment) - auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) - - Hooks.runAction('action:api.video-comment-reply.created', { comment, req, res }) - - return res.json({ comment: comment.toFormattedJSON() }) -} - -async function removeVideoComment (req: express.Request, res: express.Response) { - const videoCommentInstance = res.locals.videoCommentFull - - await removeComment(videoCommentInstance, req, res) - - auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON())) - - return res.type('json') - .status(HttpStatusCode.NO_CONTENT_204) - .end() -} diff --git a/server/controllers/api/videos/files.ts b/server/controllers/api/videos/files.ts deleted file mode 100644 index 67b60ff63..000000000 --- a/server/controllers/api/videos/files.ts +++ /dev/null @@ -1,122 +0,0 @@ -import express from 'express' -import toInt from 'validator/lib/toInt' -import { logger, loggerTagsFactory } from '@server/helpers/logger' -import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' -import { updatePlaylistAfterFileChange } from '@server/lib/hls' -import { removeAllWebVideoFiles, removeHLSFile, removeHLSPlaylist, removeWebVideoFile } from '@server/lib/video-file' -import { VideoFileModel } from '@server/models/video/video-file' -import { HttpStatusCode, UserRight } from '@shared/models' -import { - asyncMiddleware, - authenticate, - ensureUserHasRight, - videoFileMetadataGetValidator, - videoFilesDeleteHLSFileValidator, - videoFilesDeleteHLSValidator, - videoFilesDeleteWebVideoFileValidator, - videoFilesDeleteWebVideoValidator, - videosGetValidator -} from '../../../middlewares' - -const lTags = loggerTagsFactory('api', 'video') -const filesRouter = express.Router() - -filesRouter.get('/:id/metadata/:videoFileId', - asyncMiddleware(videosGetValidator), - asyncMiddleware(videoFileMetadataGetValidator), - asyncMiddleware(getVideoFileMetadata) -) - -filesRouter.delete('/:id/hls', - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), - asyncMiddleware(videoFilesDeleteHLSValidator), - asyncMiddleware(removeHLSPlaylistController) -) -filesRouter.delete('/:id/hls/:videoFileId', - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), - asyncMiddleware(videoFilesDeleteHLSFileValidator), - asyncMiddleware(removeHLSFileController) -) - -filesRouter.delete( - [ '/:id/webtorrent', '/:id/web-videos' ], // TODO: remove webtorrent in V7 - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), - asyncMiddleware(videoFilesDeleteWebVideoValidator), - asyncMiddleware(removeAllWebVideoFilesController) -) -filesRouter.delete( - [ '/:id/webtorrent/:videoFileId', '/:id/web-videos/:videoFileId' ], // TODO: remove webtorrent in V7 - authenticate, - ensureUserHasRight(UserRight.MANAGE_VIDEO_FILES), - asyncMiddleware(videoFilesDeleteWebVideoFileValidator), - asyncMiddleware(removeWebVideoFileController) -) - -// --------------------------------------------------------------------------- - -export { - filesRouter -} - -// --------------------------------------------------------------------------- - -async function getVideoFileMetadata (req: express.Request, res: express.Response) { - const videoFile = await VideoFileModel.loadWithMetadata(toInt(req.params.videoFileId)) - - return res.json(videoFile.metadata) -} - -// --------------------------------------------------------------------------- - -async function removeHLSPlaylistController (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - - logger.info('Deleting HLS playlist of %s.', video.url, lTags(video.uuid)) - await removeHLSPlaylist(video) - - await federateVideoIfNeeded(video, false, undefined) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function removeHLSFileController (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - const videoFileId = +req.params.videoFileId - - logger.info('Deleting HLS file %d of %s.', videoFileId, video.url, lTags(video.uuid)) - - const playlist = await removeHLSFile(video, videoFileId) - if (playlist) await updatePlaylistAfterFileChange(video, playlist) - - await federateVideoIfNeeded(video, false, undefined) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -// --------------------------------------------------------------------------- - -async function removeAllWebVideoFilesController (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - - logger.info('Deleting Web Video files of %s.', video.url, lTags(video.uuid)) - - await removeAllWebVideoFiles(video) - await federateVideoIfNeeded(video, false, undefined) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function removeWebVideoFileController (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - - const videoFileId = +req.params.videoFileId - logger.info('Deleting Web Video file %d of %s.', videoFileId, video.url, lTags(video.uuid)) - - await removeWebVideoFile(video, videoFileId) - await federateVideoIfNeeded(video, false, undefined) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts deleted file mode 100644 index defe9efd4..000000000 --- a/server/controllers/api/videos/import.ts +++ /dev/null @@ -1,262 +0,0 @@ -import express from 'express' -import { move, readFile } from 'fs-extra' -import { decode } from 'magnet-uri' -import parseTorrent, { Instance } from 'parse-torrent' -import { join } from 'path' -import { buildYoutubeDLImport, buildVideoFromImport, insertFromImportIntoDB, YoutubeDlImportError } from '@server/lib/video-pre-import' -import { MThumbnail, MVideoThumbnail } from '@server/types/models' -import { HttpStatusCode, ServerErrorCode, ThumbnailType, VideoImportCreate, VideoImportPayload, VideoImportState } from '@shared/models' -import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' -import { isArray } from '../../../helpers/custom-validators/misc' -import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils' -import { logger } from '../../../helpers/logger' -import { getSecureTorrentName } from '../../../helpers/utils' -import { CONFIG } from '../../../initializers/config' -import { MIMETYPES } from '../../../initializers/constants' -import { JobQueue } from '../../../lib/job-queue/job-queue' -import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - videoImportAddValidator, - videoImportCancelValidator, - videoImportDeleteValidator -} from '../../../middlewares' - -const auditLogger = auditLoggerFactory('video-imports') -const videoImportsRouter = express.Router() - -const reqVideoFileImport = createReqFiles( - [ 'thumbnailfile', 'previewfile', 'torrentfile' ], - { ...MIMETYPES.TORRENT.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT } -) - -videoImportsRouter.post('/imports', - authenticate, - reqVideoFileImport, - asyncMiddleware(videoImportAddValidator), - asyncRetryTransactionMiddleware(handleVideoImport) -) - -videoImportsRouter.post('/imports/:id/cancel', - authenticate, - asyncMiddleware(videoImportCancelValidator), - asyncRetryTransactionMiddleware(cancelVideoImport) -) - -videoImportsRouter.delete('/imports/:id', - authenticate, - asyncMiddleware(videoImportDeleteValidator), - asyncRetryTransactionMiddleware(deleteVideoImport) -) - -// --------------------------------------------------------------------------- - -export { - videoImportsRouter -} - -// --------------------------------------------------------------------------- - -async function deleteVideoImport (req: express.Request, res: express.Response) { - const videoImport = res.locals.videoImport - - await videoImport.destroy() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function cancelVideoImport (req: express.Request, res: express.Response) { - const videoImport = res.locals.videoImport - - videoImport.state = VideoImportState.CANCELLED - await videoImport.save() - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -function handleVideoImport (req: express.Request, res: express.Response) { - if (req.body.targetUrl) return handleYoutubeDlImport(req, res) - - const file = req.files?.['torrentfile']?.[0] - if (req.body.magnetUri || file) return handleTorrentImport(req, res, file) -} - -async function handleTorrentImport (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { - const body: VideoImportCreate = req.body - const user = res.locals.oauth.token.User - - let videoName: string - let torrentName: string - let magnetUri: string - - if (torrentfile) { - const result = await processTorrentOrAbortRequest(req, res, torrentfile) - if (!result) return - - videoName = result.name - torrentName = result.torrentName - } else { - const result = processMagnetURI(body) - magnetUri = result.magnetUri - videoName = result.name - } - - const video = await buildVideoFromImport({ - channelId: res.locals.videoChannel.id, - importData: { name: videoName }, - importDataOverride: body, - importType: 'torrent' - }) - - const thumbnailModel = await processThumbnail(req, video) - const previewModel = await processPreview(req, video) - - const videoImport = await insertFromImportIntoDB({ - video, - thumbnailModel, - previewModel, - videoChannel: res.locals.videoChannel, - tags: body.tags || undefined, - user, - videoPasswords: body.videoPasswords, - videoImportAttributes: { - magnetUri, - torrentName, - state: VideoImportState.PENDING, - userId: user.id - } - }) - - const payload: VideoImportPayload = { - type: torrentfile - ? 'torrent-file' - : 'magnet-uri', - videoImportId: videoImport.id, - preventException: false - } - await JobQueue.Instance.createJob({ type: 'video-import', payload }) - - auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) - - return res.json(videoImport.toFormattedJSON()).end() -} - -function statusFromYtDlImportError (err: YoutubeDlImportError): number { - switch (err.code) { - case YoutubeDlImportError.CODE.NOT_ONLY_UNICAST_URL: - return HttpStatusCode.FORBIDDEN_403 - - case YoutubeDlImportError.CODE.FETCH_ERROR: - return HttpStatusCode.BAD_REQUEST_400 - - default: - return HttpStatusCode.INTERNAL_SERVER_ERROR_500 - } -} - -async function handleYoutubeDlImport (req: express.Request, res: express.Response) { - const body: VideoImportCreate = req.body - const targetUrl = body.targetUrl - const user = res.locals.oauth.token.User - - try { - const { job, videoImport } = await buildYoutubeDLImport({ - targetUrl, - channel: res.locals.videoChannel, - importDataOverride: body, - thumbnailFilePath: req.files?.['thumbnailfile']?.[0].path, - previewFilePath: req.files?.['previewfile']?.[0].path, - user - }) - await JobQueue.Instance.createJob(job) - - auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) - - return res.json(videoImport.toFormattedJSON()).end() - } catch (err) { - logger.error('An error occurred while importing the video %s. ', targetUrl, { err }) - - return res.fail({ - message: err.message, - status: statusFromYtDlImportError(err), - data: { - targetUrl - } - }) - } -} - -async function processThumbnail (req: express.Request, video: MVideoThumbnail) { - const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined - if (thumbnailField) { - const thumbnailPhysicalFile = thumbnailField[0] - - return updateLocalVideoMiniatureFromExisting({ - inputPath: thumbnailPhysicalFile.path, - video, - type: ThumbnailType.MINIATURE, - automaticallyGenerated: false - }) - } - - return undefined -} - -async function processPreview (req: express.Request, video: MVideoThumbnail): Promise { - const previewField = req.files ? req.files['previewfile'] : undefined - if (previewField) { - const previewPhysicalFile = previewField[0] - - return updateLocalVideoMiniatureFromExisting({ - inputPath: previewPhysicalFile.path, - video, - type: ThumbnailType.PREVIEW, - automaticallyGenerated: false - }) - } - - return undefined -} - -async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) { - const torrentName = torrentfile.originalname - - // Rename the torrent to a secured name - const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName)) - await move(torrentfile.path, newTorrentPath, { overwrite: true }) - torrentfile.path = newTorrentPath - - const buf = await readFile(torrentfile.path) - const parsedTorrent = parseTorrent(buf) as Instance - - if (parsedTorrent.files.length !== 1) { - cleanUpReqFiles(req) - - res.fail({ - type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT, - message: 'Torrents with only 1 file are supported.' - }) - return undefined - } - - return { - name: extractNameFromArray(parsedTorrent.name), - torrentName - } -} - -function processMagnetURI (body: VideoImportCreate) { - const magnetUri = body.magnetUri - const parsed = decode(magnetUri) - - return { - name: extractNameFromArray(parsed.name), - magnetUri - } -} - -function extractNameFromArray (name: string | string[]) { - return isArray(name) ? name[0] : name -} diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts deleted file mode 100644 index 3cdd42289..000000000 --- a/server/controllers/api/videos/index.ts +++ /dev/null @@ -1,228 +0,0 @@ -import express from 'express' -import { pickCommonVideoQuery } from '@server/helpers/query' -import { doJSONRequest } from '@server/helpers/requests' -import { openapiOperationDoc } from '@server/middlewares/doc' -import { getServerActor } from '@server/models/application/application' -import { MVideoAccountLight } from '@server/types/models' -import { HttpStatusCode } from '../../../../shared/models' -import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' -import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { REMOTE_SCHEME, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { JobQueue } from '../../../lib/job-queue' -import { Hooks } from '../../../lib/plugins/hooks' -import { - apiRateLimiter, - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - checkVideoFollowConstraints, - commonVideosFiltersValidator, - optionalAuthenticate, - paginationValidator, - setDefaultPagination, - setDefaultVideosSort, - videosCustomGetValidator, - videosGetValidator, - videosRemoveValidator, - videosSortValidator -} from '../../../middlewares' -import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter' -import { VideoModel } from '../../../models/video/video' -import { blacklistRouter } from './blacklist' -import { videoCaptionsRouter } from './captions' -import { videoCommentRouter } from './comment' -import { filesRouter } from './files' -import { videoImportsRouter } from './import' -import { liveRouter } from './live' -import { ownershipVideoRouter } from './ownership' -import { videoPasswordRouter } from './passwords' -import { rateVideoRouter } from './rate' -import { videoSourceRouter } from './source' -import { statsRouter } from './stats' -import { storyboardRouter } from './storyboard' -import { studioRouter } from './studio' -import { tokenRouter } from './token' -import { transcodingRouter } from './transcoding' -import { updateRouter } from './update' -import { uploadRouter } from './upload' -import { viewRouter } from './view' - -const auditLogger = auditLoggerFactory('videos') -const videosRouter = express.Router() - -videosRouter.use(apiRateLimiter) - -videosRouter.use('/', blacklistRouter) -videosRouter.use('/', statsRouter) -videosRouter.use('/', rateVideoRouter) -videosRouter.use('/', videoCommentRouter) -videosRouter.use('/', studioRouter) -videosRouter.use('/', videoCaptionsRouter) -videosRouter.use('/', videoImportsRouter) -videosRouter.use('/', ownershipVideoRouter) -videosRouter.use('/', viewRouter) -videosRouter.use('/', liveRouter) -videosRouter.use('/', uploadRouter) -videosRouter.use('/', updateRouter) -videosRouter.use('/', filesRouter) -videosRouter.use('/', transcodingRouter) -videosRouter.use('/', tokenRouter) -videosRouter.use('/', videoPasswordRouter) -videosRouter.use('/', storyboardRouter) -videosRouter.use('/', videoSourceRouter) - -videosRouter.get('/categories', - openapiOperationDoc({ operationId: 'getCategories' }), - listVideoCategories -) -videosRouter.get('/licences', - openapiOperationDoc({ operationId: 'getLicences' }), - listVideoLicences -) -videosRouter.get('/languages', - openapiOperationDoc({ operationId: 'getLanguages' }), - listVideoLanguages -) -videosRouter.get('/privacies', - openapiOperationDoc({ operationId: 'getPrivacies' }), - listVideoPrivacies -) - -videosRouter.get('/', - openapiOperationDoc({ operationId: 'getVideos' }), - paginationValidator, - videosSortValidator, - setDefaultVideosSort, - setDefaultPagination, - optionalAuthenticate, - commonVideosFiltersValidator, - asyncMiddleware(listVideos) -) - -// TODO: remove, deprecated in 5.0 now we send the complete description in VideoDetails -videosRouter.get('/:id/description', - openapiOperationDoc({ operationId: 'getVideoDesc' }), - asyncMiddleware(videosGetValidator), - asyncMiddleware(getVideoDescription) -) - -videosRouter.get('/:id', - openapiOperationDoc({ operationId: 'getVideo' }), - optionalAuthenticate, - asyncMiddleware(videosCustomGetValidator('for-api')), - asyncMiddleware(checkVideoFollowConstraints), - asyncMiddleware(getVideo) -) - -videosRouter.delete('/:id', - openapiOperationDoc({ operationId: 'delVideo' }), - authenticate, - asyncMiddleware(videosRemoveValidator), - asyncRetryTransactionMiddleware(removeVideo) -) - -// --------------------------------------------------------------------------- - -export { - videosRouter -} - -// --------------------------------------------------------------------------- - -function listVideoCategories (_req: express.Request, res: express.Response) { - res.json(VIDEO_CATEGORIES) -} - -function listVideoLicences (_req: express.Request, res: express.Response) { - res.json(VIDEO_LICENCES) -} - -function listVideoLanguages (_req: express.Request, res: express.Response) { - res.json(VIDEO_LANGUAGES) -} - -function listVideoPrivacies (_req: express.Request, res: express.Response) { - res.json(VIDEO_PRIVACIES) -} - -async function getVideo (_req: express.Request, res: express.Response) { - const videoId = res.locals.videoAPI.id - const userId = res.locals.oauth?.token.User.id - - const video = await Hooks.wrapObject(res.locals.videoAPI, 'filter:api.video.get.result', { id: videoId, userId }) - - if (video.isOutdated()) { - JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) - } - - return res.json(video.toFormattedDetailsJSON()) -} - -async function getVideoDescription (req: express.Request, res: express.Response) { - const videoInstance = res.locals.videoAll - - const description = videoInstance.isOwned() - ? videoInstance.description - : await fetchRemoteVideoDescription(videoInstance) - - return res.json({ description }) -} - -async function listVideos (req: express.Request, res: express.Response) { - const serverActor = await getServerActor() - - const query = pickCommonVideoQuery(req.query) - const countVideos = getCountVideos(req) - - const apiOptions = await Hooks.wrapObject({ - ...query, - - displayOnlyForFollower: { - actorId: serverActor.id, - orLocalVideos: true - }, - nsfw: buildNSFWFilter(res, query.nsfw), - user: res.locals.oauth ? res.locals.oauth.token.User : undefined, - countVideos - }, 'filter:api.videos.list.params') - - const resultList = await Hooks.wrapPromiseFun( - VideoModel.listForApi, - apiOptions, - 'filter:api.videos.list.result' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) -} - -async function removeVideo (req: express.Request, res: express.Response) { - const videoInstance = res.locals.videoAll - - await sequelizeTypescript.transaction(async t => { - await videoInstance.destroy({ transaction: t }) - }) - - auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON())) - logger.info('Video with name %s and uuid %s deleted.', videoInstance.name, videoInstance.uuid) - - Hooks.runAction('action:api.video.deleted', { video: videoInstance, req, res }) - - return res.type('json') - .status(HttpStatusCode.NO_CONTENT_204) - .end() -} - -// --------------------------------------------------------------------------- - -// FIXME: Should not exist, we rely on specific API -async function fetchRemoteVideoDescription (video: MVideoAccountLight) { - const host = video.VideoChannel.Account.Actor.Server.host - const path = video.getDescriptionAPIPath() - const url = REMOTE_SCHEME.HTTP + '://' + host + path - - const { body } = await doJSONRequest(url) - return body.description || '' -} diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts deleted file mode 100644 index e19e8c652..000000000 --- a/server/controllers/api/videos/live.ts +++ /dev/null @@ -1,224 +0,0 @@ -import express from 'express' -import { exists } from '@server/helpers/custom-validators/misc' -import { createReqFiles } from '@server/helpers/express-utils' -import { getFormattedObjects } from '@server/helpers/utils' -import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' -import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' -import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' -import { Hooks } from '@server/lib/plugins/hooks' -import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' -import { - videoLiveAddValidator, - videoLiveFindReplaySessionValidator, - videoLiveGetValidator, - videoLiveListSessionsValidator, - videoLiveUpdateValidator -} from '@server/middlewares/validators/videos/video-live' -import { VideoLiveModel } from '@server/models/video/video-live' -import { VideoLiveSessionModel } from '@server/models/video/video-live-session' -import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models' -import { buildUUID, uuidToShort } from '@shared/extra-utils' -import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoPrivacy, VideoState } from '@shared/models' -import { logger } from '../../../helpers/logger' -import { sequelizeTypescript } from '../../../initializers/database' -import { updateLocalVideoMiniatureFromExisting } from '../../../lib/thumbnail' -import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares' -import { VideoModel } from '../../../models/video/video' -import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting' -import { VideoPasswordModel } from '@server/models/video/video-password' - -const liveRouter = express.Router() - -const reqVideoFileLive = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) - -liveRouter.post('/live', - authenticate, - reqVideoFileLive, - asyncMiddleware(videoLiveAddValidator), - asyncRetryTransactionMiddleware(addLiveVideo) -) - -liveRouter.get('/live/:videoId/sessions', - authenticate, - asyncMiddleware(videoLiveGetValidator), - videoLiveListSessionsValidator, - asyncMiddleware(getLiveVideoSessions) -) - -liveRouter.get('/live/:videoId', - optionalAuthenticate, - asyncMiddleware(videoLiveGetValidator), - getLiveVideo -) - -liveRouter.put('/live/:videoId', - authenticate, - asyncMiddleware(videoLiveGetValidator), - videoLiveUpdateValidator, - asyncRetryTransactionMiddleware(updateLiveVideo) -) - -liveRouter.get('/:videoId/live-session', - asyncMiddleware(videoLiveFindReplaySessionValidator), - getLiveReplaySession -) - -// --------------------------------------------------------------------------- - -export { - liveRouter -} - -// --------------------------------------------------------------------------- - -function getLiveVideo (req: express.Request, res: express.Response) { - const videoLive = res.locals.videoLive - - return res.json(videoLive.toFormattedJSON(canSeePrivateLiveInformation(res))) -} - -function getLiveReplaySession (req: express.Request, res: express.Response) { - const session = res.locals.videoLiveSession - - return res.json(session.toFormattedJSON()) -} - -async function getLiveVideoSessions (req: express.Request, res: express.Response) { - const videoLive = res.locals.videoLive - - const data = await VideoLiveSessionModel.listSessionsOfLiveForAPI({ videoId: videoLive.videoId }) - - return res.json(getFormattedObjects(data, data.length)) -} - -function canSeePrivateLiveInformation (res: express.Response) { - const user = res.locals.oauth?.token.User - if (!user) return false - - if (user.hasRight(UserRight.GET_ANY_LIVE)) return true - - const video = res.locals.videoAll - return video.VideoChannel.Account.userId === user.id -} - -async function updateLiveVideo (req: express.Request, res: express.Response) { - const body: LiveVideoUpdate = req.body - - const video = res.locals.videoAll - const videoLive = res.locals.videoLive - - const newReplaySettingModel = await updateReplaySettings(videoLive, body) - if (newReplaySettingModel) videoLive.replaySettingId = newReplaySettingModel.id - else videoLive.replaySettingId = null - - if (exists(body.permanentLive)) videoLive.permanentLive = body.permanentLive - if (exists(body.latencyMode)) videoLive.latencyMode = body.latencyMode - - video.VideoLive = await videoLive.save() - - await federateVideoIfNeeded(video, false) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function updateReplaySettings (videoLive: MVideoLive, body: LiveVideoUpdate) { - if (exists(body.saveReplay)) videoLive.saveReplay = body.saveReplay - - // The live replay is not saved anymore, destroy the old model if it existed - if (!videoLive.saveReplay) { - if (videoLive.replaySettingId) { - await VideoLiveReplaySettingModel.removeSettings(videoLive.replaySettingId) - } - - return undefined - } - - const settingModel = videoLive.replaySettingId - ? await VideoLiveReplaySettingModel.load(videoLive.replaySettingId) - : new VideoLiveReplaySettingModel() - - if (exists(body.replaySettings.privacy)) settingModel.privacy = body.replaySettings.privacy - - return settingModel.save() -} - -async function addLiveVideo (req: express.Request, res: express.Response) { - const videoInfo: LiveVideoCreate = req.body - - // Prepare data so we don't block the transaction - let videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id) - videoData = await Hooks.wrapObject(videoData, 'filter:api.video.live.video-attribute.result') - - videoData.isLive = true - videoData.state = VideoState.WAITING_FOR_LIVE - videoData.duration = 0 - - const video = new VideoModel(videoData) as MVideoDetails - video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object - - const videoLive = new VideoLiveModel() - videoLive.saveReplay = videoInfo.saveReplay || false - videoLive.permanentLive = videoInfo.permanentLive || false - videoLive.latencyMode = videoInfo.latencyMode || LiveVideoLatencyMode.DEFAULT - videoLive.streamKey = buildUUID() - - const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ - video, - files: req.files, - fallback: type => { - return updateLocalVideoMiniatureFromExisting({ - inputPath: ASSETS_PATH.DEFAULT_LIVE_BACKGROUND, - video, - type, - automaticallyGenerated: true, - keepOriginal: true - }) - } - }) - - const { videoCreated } = await sequelizeTypescript.transaction(async t => { - const sequelizeOptions = { transaction: t } - - const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight - - if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) - if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) - - // Do not forget to add video channel information to the created video - videoCreated.VideoChannel = res.locals.videoChannel - - if (videoLive.saveReplay) { - const replaySettings = new VideoLiveReplaySettingModel({ - privacy: videoInfo.replaySettings.privacy - }) - await replaySettings.save(sequelizeOptions) - - videoLive.replaySettingId = replaySettings.id - } - - videoLive.videoId = videoCreated.id - videoCreated.VideoLive = await videoLive.save(sequelizeOptions) - - await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) - - await federateVideoIfNeeded(videoCreated, true, t) - - if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { - await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) - } - - logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid) - - return { videoCreated } - }) - - Hooks.runAction('action:api.live-video.created', { video: videoCreated, req, res }) - - return res.json({ - video: { - id: videoCreated.id, - shortUUID: uuidToShort(videoCreated.uuid), - uuid: videoCreated.uuid - } - }) -} diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts deleted file mode 100644 index 88355b289..000000000 --- a/server/controllers/api/videos/ownership.ts +++ /dev/null @@ -1,138 +0,0 @@ -import express from 'express' -import { MVideoFullLight } from '@server/types/models' -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers/database' -import { sendUpdateVideo } from '../../../lib/activitypub/send' -import { changeVideoChannelShare } from '../../../lib/activitypub/share' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - paginationValidator, - setDefaultPagination, - videosAcceptChangeOwnershipValidator, - videosChangeOwnershipValidator, - videosTerminateChangeOwnershipValidator -} from '../../../middlewares' -import { VideoModel } from '../../../models/video/video' -import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' -import { VideoChannelModel } from '../../../models/video/video-channel' - -const ownershipVideoRouter = express.Router() - -ownershipVideoRouter.post('/:videoId/give-ownership', - authenticate, - asyncMiddleware(videosChangeOwnershipValidator), - asyncRetryTransactionMiddleware(giveVideoOwnership) -) - -ownershipVideoRouter.get('/ownership', - authenticate, - paginationValidator, - setDefaultPagination, - asyncRetryTransactionMiddleware(listVideoOwnership) -) - -ownershipVideoRouter.post('/ownership/:id/accept', - authenticate, - asyncMiddleware(videosTerminateChangeOwnershipValidator), - asyncMiddleware(videosAcceptChangeOwnershipValidator), - asyncRetryTransactionMiddleware(acceptOwnership) -) - -ownershipVideoRouter.post('/ownership/:id/refuse', - authenticate, - asyncMiddleware(videosTerminateChangeOwnershipValidator), - asyncRetryTransactionMiddleware(refuseOwnership) -) - -// --------------------------------------------------------------------------- - -export { - ownershipVideoRouter -} - -// --------------------------------------------------------------------------- - -async function giveVideoOwnership (req: express.Request, res: express.Response) { - const videoInstance = res.locals.videoAll - const initiatorAccountId = res.locals.oauth.token.User.Account.id - const nextOwner = res.locals.nextOwner - - await sequelizeTypescript.transaction(t => { - return VideoChangeOwnershipModel.findOrCreate({ - where: { - initiatorAccountId, - nextOwnerAccountId: nextOwner.id, - videoId: videoInstance.id, - status: VideoChangeOwnershipStatus.WAITING - }, - defaults: { - initiatorAccountId, - nextOwnerAccountId: nextOwner.id, - videoId: videoInstance.id, - status: VideoChangeOwnershipStatus.WAITING - }, - transaction: t - }) - }) - - logger.info('Ownership change for video %s created.', videoInstance.name) - return res.type('json') - .status(HttpStatusCode.NO_CONTENT_204) - .end() -} - -async function listVideoOwnership (req: express.Request, res: express.Response) { - const currentAccountId = res.locals.oauth.token.User.Account.id - - const resultList = await VideoChangeOwnershipModel.listForApi( - currentAccountId, - req.query.start || 0, - req.query.count || 10, - req.query.sort || 'createdAt' - ) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -function acceptOwnership (req: express.Request, res: express.Response) { - return sequelizeTypescript.transaction(async t => { - const videoChangeOwnership = res.locals.videoChangeOwnership - const channel = res.locals.videoChannel - - // We need more attributes for federation - const targetVideo = await VideoModel.loadFull(videoChangeOwnership.Video.id, t) - - const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId, t) - - targetVideo.channelId = channel.id - - const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight - targetVideoUpdated.VideoChannel = channel - - if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) { - await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t) - await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) - } - - videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED - await videoChangeOwnership.save({ transaction: t }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() - }) -} - -function refuseOwnership (req: express.Request, res: express.Response) { - return sequelizeTypescript.transaction(async t => { - const videoChangeOwnership = res.locals.videoChangeOwnership - - videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED - await videoChangeOwnership.save({ transaction: t }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() - }) -} diff --git a/server/controllers/api/videos/passwords.ts b/server/controllers/api/videos/passwords.ts deleted file mode 100644 index d11cf5bcc..000000000 --- a/server/controllers/api/videos/passwords.ts +++ /dev/null @@ -1,105 +0,0 @@ -import express from 'express' - -import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' -import { getFormattedObjects } from '../../../helpers/utils' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - setDefaultPagination, - setDefaultSort -} from '../../../middlewares' -import { - listVideoPasswordValidator, - paginationValidator, - removeVideoPasswordValidator, - updateVideoPasswordListValidator, - videoPasswordsSortValidator -} from '../../../middlewares/validators' -import { VideoPasswordModel } from '@server/models/video/video-password' -import { logger, loggerTagsFactory } from '@server/helpers/logger' -import { Transaction } from 'sequelize' -import { getVideoWithAttributes } from '@server/helpers/video' - -const lTags = loggerTagsFactory('api', 'video') -const videoPasswordRouter = express.Router() - -videoPasswordRouter.get('/:videoId/passwords', - authenticate, - paginationValidator, - videoPasswordsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listVideoPasswordValidator), - asyncMiddleware(listVideoPasswords) -) - -videoPasswordRouter.put('/:videoId/passwords', - authenticate, - asyncMiddleware(updateVideoPasswordListValidator), - asyncMiddleware(updateVideoPasswordList) -) - -videoPasswordRouter.delete('/:videoId/passwords/:passwordId', - authenticate, - asyncMiddleware(removeVideoPasswordValidator), - asyncRetryTransactionMiddleware(removeVideoPassword) -) - -// --------------------------------------------------------------------------- - -export { - videoPasswordRouter -} - -// --------------------------------------------------------------------------- - -async function listVideoPasswords (req: express.Request, res: express.Response) { - const options = { - videoId: res.locals.videoAll.id, - start: req.query.start, - count: req.query.count, - sort: req.query.sort - } - - const resultList = await VideoPasswordModel.listPasswords(options) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function updateVideoPasswordList (req: express.Request, res: express.Response) { - const videoInstance = getVideoWithAttributes(res) - const videoId = videoInstance.id - - const passwordArray = req.body.passwords as string[] - - await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => { - await VideoPasswordModel.deleteAllPasswords(videoId, t) - await VideoPasswordModel.addPasswords(passwordArray, videoId, t) - }) - - logger.info( - `Video passwords for video with name %s and uuid %s have been updated`, - videoInstance.name, - videoInstance.uuid, - lTags(videoInstance.uuid) - ) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -async function removeVideoPassword (req: express.Request, res: express.Response) { - const videoInstance = getVideoWithAttributes(res) - const password = res.locals.videoPassword - - await VideoPasswordModel.deletePassword(password.id) - logger.info( - 'Password with id %d of video named %s and uuid %s has been deleted.', - password.id, - videoInstance.name, - videoInstance.uuid, - lTags(videoInstance.uuid) - ) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts deleted file mode 100644 index 6b26a8eee..000000000 --- a/server/controllers/api/videos/rate.ts +++ /dev/null @@ -1,87 +0,0 @@ -import express from 'express' -import { HttpStatusCode, UserVideoRateUpdate } from '@shared/models' -import { logger } from '../../../helpers/logger' -import { VIDEO_RATE_TYPES } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { getLocalRateUrl, sendVideoRateChange } from '../../../lib/activitypub/video-rates' -import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares' -import { AccountModel } from '../../../models/account/account' -import { AccountVideoRateModel } from '../../../models/account/account-video-rate' - -const rateVideoRouter = express.Router() - -rateVideoRouter.put('/:id/rate', - authenticate, - asyncMiddleware(videoUpdateRateValidator), - asyncRetryTransactionMiddleware(rateVideo) -) - -// --------------------------------------------------------------------------- - -export { - rateVideoRouter -} - -// --------------------------------------------------------------------------- - -async function rateVideo (req: express.Request, res: express.Response) { - const body: UserVideoRateUpdate = req.body - const rateType = body.rating - const videoInstance = res.locals.videoAll - const userAccount = res.locals.oauth.token.User.Account - - await sequelizeTypescript.transaction(async t => { - const sequelizeOptions = { transaction: t } - - const accountInstance = await AccountModel.load(userAccount.id, t) - const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) - - // Same rate, nothing do to - if (rateType === 'none' && !previousRate || previousRate?.type === rateType) return - - let likesToIncrement = 0 - let dislikesToIncrement = 0 - - if (rateType === VIDEO_RATE_TYPES.LIKE) likesToIncrement++ - else if (rateType === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++ - - // There was a previous rate, update it - if (previousRate) { - // We will remove the previous rate, so we will need to update the video count attribute - if (previousRate.type === 'like') likesToIncrement-- - else if (previousRate.type === 'dislike') dislikesToIncrement-- - - if (rateType === 'none') { // Destroy previous rate - await previousRate.destroy(sequelizeOptions) - } else { // Update previous rate - previousRate.type = rateType - previousRate.url = getLocalRateUrl(rateType, userAccount.Actor, videoInstance) - await previousRate.save(sequelizeOptions) - } - } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate - const query = { - accountId: accountInstance.id, - videoId: videoInstance.id, - type: rateType, - url: getLocalRateUrl(rateType, userAccount.Actor, videoInstance) - } - - await AccountVideoRateModel.create(query, sequelizeOptions) - } - - const incrementQuery = { - likes: likesToIncrement, - dislikes: dislikesToIncrement - } - - await videoInstance.increment(incrementQuery, sequelizeOptions) - - await sendVideoRateChange(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t) - - logger.info('Account video rate for video %s of account %s updated.', videoInstance.name, accountInstance.name) - }) - - return res.type('json') - .status(HttpStatusCode.NO_CONTENT_204) - .end() -} diff --git a/server/controllers/api/videos/source.ts b/server/controllers/api/videos/source.ts deleted file mode 100644 index 75fe68b6c..000000000 --- a/server/controllers/api/videos/source.ts +++ /dev/null @@ -1,206 +0,0 @@ -import express from 'express' -import { move } from 'fs-extra' -import { sequelizeTypescript } from '@server/initializers/database' -import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue' -import { Hooks } from '@server/lib/plugins/hooks' -import { regenerateMiniaturesIfNeeded } from '@server/lib/thumbnail' -import { uploadx } from '@server/lib/uploadx' -import { buildMoveToObjectStorageJob } from '@server/lib/video' -import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' -import { buildNewFile } from '@server/lib/video-file' -import { VideoPathManager } from '@server/lib/video-path-manager' -import { buildNextVideoState } from '@server/lib/video-state' -import { openapiOperationDoc } from '@server/middlewares/doc' -import { VideoModel } from '@server/models/video/video' -import { VideoSourceModel } from '@server/models/video/video-source' -import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' -import { VideoState } from '@shared/models' -import { logger, loggerTagsFactory } from '../../../helpers/logger' -import { - asyncMiddleware, - authenticate, - replaceVideoSourceResumableInitValidator, - replaceVideoSourceResumableValidator, - videoSourceGetLatestValidator -} from '../../../middlewares' - -const lTags = loggerTagsFactory('api', 'video') - -const videoSourceRouter = express.Router() - -videoSourceRouter.get('/:id/source', - openapiOperationDoc({ operationId: 'getVideoSource' }), - authenticate, - asyncMiddleware(videoSourceGetLatestValidator), - getVideoLatestSource -) - -videoSourceRouter.post('/:id/source/replace-resumable', - authenticate, - asyncMiddleware(replaceVideoSourceResumableInitValidator), - (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end -) - -videoSourceRouter.delete('/:id/source/replace-resumable', - authenticate, - (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end -) - -videoSourceRouter.put('/:id/source/replace-resumable', - authenticate, - uploadx.upload, // uploadx doesn't next() before the file upload completes - asyncMiddleware(replaceVideoSourceResumableValidator), - asyncMiddleware(replaceVideoSourceResumable) -) - -// --------------------------------------------------------------------------- - -export { - videoSourceRouter -} - -// --------------------------------------------------------------------------- - -function getVideoLatestSource (req: express.Request, res: express.Response) { - return res.json(res.locals.videoSource.toFormattedJSON()) -} - -async function replaceVideoSourceResumable (req: express.Request, res: express.Response) { - const videoPhysicalFile = res.locals.updateVideoFileResumable - const user = res.locals.oauth.token.User - - const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' }) - const originalFilename = videoPhysicalFile.originalname - - const videoFileMutexReleaser = await VideoPathManager.Instance.lockFiles(res.locals.videoAll.uuid) - - try { - const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(res.locals.videoAll, videoFile) - await move(videoPhysicalFile.path, destination) - - let oldWebVideoFiles: MVideoFile[] = [] - let oldStreamingPlaylists: MStreamingPlaylistFiles[] = [] - - const inputFileUpdatedAt = new Date() - - const video = await sequelizeTypescript.transaction(async transaction => { - const video = await VideoModel.loadFull(res.locals.videoAll.id, transaction) - - oldWebVideoFiles = video.VideoFiles - oldStreamingPlaylists = video.VideoStreamingPlaylists - - for (const file of video.VideoFiles) { - await file.destroy({ transaction }) - } - for (const playlist of oldStreamingPlaylists) { - await playlist.destroy({ transaction }) - } - - videoFile.videoId = video.id - await videoFile.save({ transaction }) - - video.VideoFiles = [ videoFile ] - video.VideoStreamingPlaylists = [] - - video.state = buildNextVideoState() - video.duration = videoPhysicalFile.duration - video.inputFileUpdatedAt = inputFileUpdatedAt - await video.save({ transaction }) - - await autoBlacklistVideoIfNeeded({ - video, - user, - isRemote: false, - isNew: false, - isNewFile: true, - transaction - }) - - return video - }) - - await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists }) - - const source = await VideoSourceModel.create({ - filename: originalFilename, - videoId: video.id, - createdAt: inputFileUpdatedAt - }) - - await regenerateMiniaturesIfNeeded(video) - await video.VideoChannel.setAsUpdated() - await addVideoJobsAfterUpload(video, video.getMaxQualityFile()) - - logger.info('Replaced video file of video %s with uuid %s.', video.name, video.uuid, lTags(video.uuid)) - - Hooks.runAction('action:api.video.file-updated', { video, req, res }) - - return res.json(source.toFormattedJSON()) - } finally { - videoFileMutexReleaser() - } -} - -async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) { - const jobs: (CreateJobArgument & CreateJobOptions)[] = [ - { - type: 'manage-video-torrent' as 'manage-video-torrent', - payload: { - videoId: video.id, - videoFileId: videoFile.id, - action: 'create' - } - }, - - { - type: 'generate-video-storyboard' as 'generate-video-storyboard', - payload: { - videoUUID: video.uuid, - // No need to federate, we process these jobs sequentially - federate: false - } - }, - - { - type: 'federate-video' as 'federate-video', - payload: { - videoUUID: video.uuid, - isNewVideo: false - } - } - ] - - if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { - jobs.push(await buildMoveToObjectStorageJob({ video, isNewVideo: false, previousVideoState: undefined })) - } - - if (video.state === VideoState.TO_TRANSCODE) { - jobs.push({ - type: 'transcoding-job-builder' as 'transcoding-job-builder', - payload: { - videoUUID: video.uuid, - optimizeJob: { - isNewVideo: false - } - } - }) - } - - return JobQueue.Instance.createSequentialJobFlow(...jobs) -} - -async function removeOldFiles (options: { - video: MVideo - files: MVideoFile[] - playlists: MStreamingPlaylistFiles[] -}) { - const { video, files, playlists } = options - - for (const file of files) { - await video.removeWebVideoFile(file) - } - - for (const playlist of playlists) { - await video.removeStreamingPlaylistFiles(playlist) - } -} diff --git a/server/controllers/api/videos/stats.ts b/server/controllers/api/videos/stats.ts deleted file mode 100644 index e79f01888..000000000 --- a/server/controllers/api/videos/stats.ts +++ /dev/null @@ -1,75 +0,0 @@ -import express from 'express' -import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' -import { VideoStatsOverallQuery, VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models' -import { - asyncMiddleware, - authenticate, - videoOverallStatsValidator, - videoRetentionStatsValidator, - videoTimeserieStatsValidator -} from '../../../middlewares' - -const statsRouter = express.Router() - -statsRouter.get('/:videoId/stats/overall', - authenticate, - asyncMiddleware(videoOverallStatsValidator), - asyncMiddleware(getOverallStats) -) - -statsRouter.get('/:videoId/stats/timeseries/:metric', - authenticate, - asyncMiddleware(videoTimeserieStatsValidator), - asyncMiddleware(getTimeserieStats) -) - -statsRouter.get('/:videoId/stats/retention', - authenticate, - asyncMiddleware(videoRetentionStatsValidator), - asyncMiddleware(getRetentionStats) -) - -// --------------------------------------------------------------------------- - -export { - statsRouter -} - -// --------------------------------------------------------------------------- - -async function getOverallStats (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - const query = req.query as VideoStatsOverallQuery - - const stats = await LocalVideoViewerModel.getOverallStats({ - video, - startDate: query.startDate, - endDate: query.endDate - }) - - return res.json(stats) -} - -async function getRetentionStats (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - - const stats = await LocalVideoViewerModel.getRetentionStats(video) - - return res.json(stats) -} - -async function getTimeserieStats (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - const metric = req.params.metric as VideoStatsTimeserieMetric - - const query = req.query as VideoStatsTimeserieQuery - - const stats = await LocalVideoViewerModel.getTimeserieStats({ - video, - metric, - startDate: query.startDate ?? video.createdAt.toISOString(), - endDate: query.endDate ?? new Date().toISOString() - }) - - return res.json(stats) -} diff --git a/server/controllers/api/videos/storyboard.ts b/server/controllers/api/videos/storyboard.ts deleted file mode 100644 index 47a22011d..000000000 --- a/server/controllers/api/videos/storyboard.ts +++ /dev/null @@ -1,29 +0,0 @@ -import express from 'express' -import { getVideoWithAttributes } from '@server/helpers/video' -import { StoryboardModel } from '@server/models/video/storyboard' -import { asyncMiddleware, videosGetValidator } from '../../../middlewares' - -const storyboardRouter = express.Router() - -storyboardRouter.get('/:id/storyboards', - asyncMiddleware(videosGetValidator), - asyncMiddleware(listStoryboards) -) - -// --------------------------------------------------------------------------- - -export { - storyboardRouter -} - -// --------------------------------------------------------------------------- - -async function listStoryboards (req: express.Request, res: express.Response) { - const video = getVideoWithAttributes(res) - - const storyboards = await StoryboardModel.listStoryboardsOf(video) - - return res.json({ - storyboards: storyboards.map(s => s.toFormattedJSON()) - }) -} diff --git a/server/controllers/api/videos/studio.ts b/server/controllers/api/videos/studio.ts deleted file mode 100644 index 7c31dfd2b..000000000 --- a/server/controllers/api/videos/studio.ts +++ /dev/null @@ -1,143 +0,0 @@ -import Bluebird from 'bluebird' -import express from 'express' -import { move } from 'fs-extra' -import { basename } from 'path' -import { createAnyReqFiles } from '@server/helpers/express-utils' -import { MIMETYPES, VIDEO_FILTERS } from '@server/initializers/constants' -import { buildTaskFileFieldname, createVideoStudioJob, getStudioTaskFilePath, getTaskFileFromReq } from '@server/lib/video-studio' -import { - HttpStatusCode, - VideoState, - VideoStudioCreateEdition, - VideoStudioTask, - VideoStudioTaskCut, - VideoStudioTaskIntro, - VideoStudioTaskOutro, - VideoStudioTaskPayload, - VideoStudioTaskWatermark -} from '@shared/models' -import { asyncMiddleware, authenticate, videoStudioAddEditionValidator } from '../../../middlewares' - -const studioRouter = express.Router() - -const tasksFiles = createAnyReqFiles( - MIMETYPES.VIDEO.MIMETYPE_EXT, - (req: express.Request, file: Express.Multer.File, cb: (err: Error, result?: boolean) => void) => { - const body = req.body as VideoStudioCreateEdition - - // Fetch array element - const matches = file.fieldname.match(/tasks\[(\d+)\]/) - if (!matches) return cb(new Error('Cannot find array element indice for ' + file.fieldname)) - - const indice = parseInt(matches[1]) - const task = body.tasks[indice] - - if (!task) return cb(new Error('Cannot find array element of indice ' + indice + ' for ' + file.fieldname)) - - if ( - [ 'add-intro', 'add-outro', 'add-watermark' ].includes(task.name) && - file.fieldname === buildTaskFileFieldname(indice) - ) { - return cb(null, true) - } - - return cb(null, false) - } -) - -studioRouter.post('/:videoId/studio/edit', - authenticate, - tasksFiles, - asyncMiddleware(videoStudioAddEditionValidator), - asyncMiddleware(createEditionTasks) -) - -// --------------------------------------------------------------------------- - -export { - studioRouter -} - -// --------------------------------------------------------------------------- - -async function createEditionTasks (req: express.Request, res: express.Response) { - const files = req.files as Express.Multer.File[] - const body = req.body as VideoStudioCreateEdition - const video = res.locals.videoAll - - video.state = VideoState.TO_EDIT - await video.save() - - const payload = { - videoUUID: video.uuid, - tasks: await Bluebird.mapSeries(body.tasks, (t, i) => buildTaskPayload(t, i, files)) - } - - await createVideoStudioJob({ - user: res.locals.oauth.token.User, - payload, - video - }) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} - -const taskPayloadBuilders: { - [id in VideoStudioTask['name']]: ( - task: VideoStudioTask, - indice?: number, - files?: Express.Multer.File[] - ) => Promise -} = { - 'add-intro': buildIntroOutroTask, - 'add-outro': buildIntroOutroTask, - 'cut': buildCutTask, - 'add-watermark': buildWatermarkTask -} - -function buildTaskPayload (task: VideoStudioTask, indice: number, files: Express.Multer.File[]): Promise { - return taskPayloadBuilders[task.name](task, indice, files) -} - -async function buildIntroOutroTask (task: VideoStudioTaskIntro | VideoStudioTaskOutro, indice: number, files: Express.Multer.File[]) { - const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path) - - return { - name: task.name, - options: { - file: destination - } - } -} - -function buildCutTask (task: VideoStudioTaskCut) { - return Promise.resolve({ - name: task.name, - options: { - start: task.options.start, - end: task.options.end - } - }) -} - -async function buildWatermarkTask (task: VideoStudioTaskWatermark, indice: number, files: Express.Multer.File[]) { - const destination = await moveStudioFileToPersistentTMP(getTaskFileFromReq(files, indice).path) - - return { - name: task.name, - options: { - file: destination, - watermarkSizeRatio: VIDEO_FILTERS.WATERMARK.SIZE_RATIO, - horitonzalMarginRatio: VIDEO_FILTERS.WATERMARK.HORIZONTAL_MARGIN_RATIO, - verticalMarginRatio: VIDEO_FILTERS.WATERMARK.VERTICAL_MARGIN_RATIO - } - } -} - -async function moveStudioFileToPersistentTMP (file: string) { - const destination = getStudioTaskFilePath(basename(file)) - - await move(file, destination) - - return destination -} diff --git a/server/controllers/api/videos/token.ts b/server/controllers/api/videos/token.ts deleted file mode 100644 index e961ffd9e..000000000 --- a/server/controllers/api/videos/token.ts +++ /dev/null @@ -1,33 +0,0 @@ -import express from 'express' -import { VideoTokensManager } from '@server/lib/video-tokens-manager' -import { VideoPrivacy, VideoToken } from '@shared/models' -import { asyncMiddleware, optionalAuthenticate, videoFileTokenValidator, videosCustomGetValidator } from '../../../middlewares' - -const tokenRouter = express.Router() - -tokenRouter.post('/:id/token', - optionalAuthenticate, - asyncMiddleware(videosCustomGetValidator('only-video')), - videoFileTokenValidator, - generateToken -) - -// --------------------------------------------------------------------------- - -export { - tokenRouter -} - -// --------------------------------------------------------------------------- - -function generateToken (req: express.Request, res: express.Response) { - const video = res.locals.onlyVideo - - const files = video.privacy === VideoPrivacy.PASSWORD_PROTECTED - ? VideoTokensManager.Instance.createForPasswordProtectedVideo({ videoUUID: video.uuid }) - : VideoTokensManager.Instance.createForAuthUser({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) - - return res.json({ - files - } as VideoToken) -} diff --git a/server/controllers/api/videos/transcoding.ts b/server/controllers/api/videos/transcoding.ts deleted file mode 100644 index c0b93742f..000000000 --- a/server/controllers/api/videos/transcoding.ts +++ /dev/null @@ -1,60 +0,0 @@ -import express from 'express' -import { logger, loggerTagsFactory } from '@server/helpers/logger' -import { Hooks } from '@server/lib/plugins/hooks' -import { createTranscodingJobs } from '@server/lib/transcoding/create-transcoding-job' -import { computeResolutionsToTranscode } from '@server/lib/transcoding/transcoding-resolutions' -import { VideoJobInfoModel } from '@server/models/video/video-job-info' -import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' -import { asyncMiddleware, authenticate, createTranscodingValidator, ensureUserHasRight } from '../../../middlewares' - -const lTags = loggerTagsFactory('api', 'video') -const transcodingRouter = express.Router() - -transcodingRouter.post('/:videoId/transcoding', - authenticate, - ensureUserHasRight(UserRight.RUN_VIDEO_TRANSCODING), - asyncMiddleware(createTranscodingValidator), - asyncMiddleware(createTranscoding) -) - -// --------------------------------------------------------------------------- - -export { - transcodingRouter -} - -// --------------------------------------------------------------------------- - -async function createTranscoding (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - logger.info('Creating %s transcoding job for %s.', req.body.transcodingType, video.url, lTags()) - - const body: VideoTranscodingCreate = req.body - - await VideoJobInfoModel.abortAllTasks(video.uuid, 'pendingTranscode') - - const { resolution: maxResolution, hasAudio } = await video.probeMaxQualityFile() - - const resolutions = await Hooks.wrapObject( - computeResolutionsToTranscode({ input: maxResolution, type: 'vod', includeInput: true, strictLower: false, hasAudio }), - 'filter:transcoding.manual.resolutions-to-transcode.result', - body - ) - - if (resolutions.length === 0) { - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) - } - - video.state = VideoState.TO_TRANSCODE - await video.save() - - await createTranscodingJobs({ - video, - resolutions, - transcodingType: body.transcodingType, - isNewVideo: false, - user: null // Don't specify priority since these transcoding jobs are fired by the admin - }) - - return res.sendStatus(HttpStatusCode.NO_CONTENT_204) -} diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts deleted file mode 100644 index 1edc509dc..000000000 --- a/server/controllers/api/videos/update.ts +++ /dev/null @@ -1,210 +0,0 @@ -import express from 'express' -import { Transaction } from 'sequelize/types' -import { changeVideoChannelShare } from '@server/lib/activitypub/share' -import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' -import { setVideoPrivacy } from '@server/lib/video-privacy' -import { openapiOperationDoc } from '@server/middlewares/doc' -import { FilteredModelAttributes } from '@server/types' -import { MVideoFullLight } from '@server/types/models' -import { forceNumber } from '@shared/core-utils' -import { HttpStatusCode, VideoPrivacy, VideoUpdate } from '@shared/models' -import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' -import { resetSequelizeInstance } from '../../../helpers/database-utils' -import { createReqFiles } from '../../../helpers/express-utils' -import { logger, loggerTagsFactory } from '../../../helpers/logger' -import { MIMETYPES } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { Hooks } from '../../../lib/plugins/hooks' -import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' -import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares' -import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' -import { VideoModel } from '../../../models/video/video' -import { VideoPathManager } from '@server/lib/video-path-manager' -import { VideoPasswordModel } from '@server/models/video/video-password' -import { exists } from '@server/helpers/custom-validators/misc' - -const lTags = loggerTagsFactory('api', 'video') -const auditLogger = auditLoggerFactory('videos') -const updateRouter = express.Router() - -const reqVideoFileUpdate = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) - -updateRouter.put('/:id', - openapiOperationDoc({ operationId: 'putVideo' }), - authenticate, - reqVideoFileUpdate, - asyncMiddleware(videosUpdateValidator), - asyncRetryTransactionMiddleware(updateVideo) -) - -// --------------------------------------------------------------------------- - -export { - updateRouter -} - -// --------------------------------------------------------------------------- - -async function updateVideo (req: express.Request, res: express.Response) { - const videoFromReq = res.locals.videoAll - const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON()) - const videoInfoToUpdate: VideoUpdate = req.body - - const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation() - const oldPrivacy = videoFromReq.privacy - - const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ - video: videoFromReq, - files: req.files, - fallback: () => Promise.resolve(undefined), - automaticallyGenerated: false - }) - - const videoFileLockReleaser = await VideoPathManager.Instance.lockFiles(videoFromReq.uuid) - - try { - const { videoInstanceUpdated, isNewVideo } = await sequelizeTypescript.transaction(async t => { - // Refresh video since thumbnails to prevent concurrent updates - const video = await VideoModel.loadFull(videoFromReq.id, t) - - const oldVideoChannel = video.VideoChannel - - const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes)[] = [ - 'name', - 'category', - 'licence', - 'language', - 'nsfw', - 'waitTranscoding', - 'support', - 'description', - 'commentsEnabled', - 'downloadEnabled' - ] - - for (const key of keysToUpdate) { - if (videoInfoToUpdate[key] !== undefined) video.set(key, videoInfoToUpdate[key]) - } - - if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) { - video.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt) - } - - // Privacy update? - let isNewVideo = false - if (videoInfoToUpdate.privacy !== undefined) { - isNewVideo = await updateVideoPrivacy({ videoInstance: video, videoInfoToUpdate, hadPrivacyForFederation, transaction: t }) - } - - // Force updatedAt attribute change - if (!video.changed()) { - await video.setAsRefreshed(t) - } - - const videoInstanceUpdated = await video.save({ transaction: t }) as MVideoFullLight - - // Thumbnail & preview updates? - if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) - if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) - - // Video tags update? - if (videoInfoToUpdate.tags !== undefined) { - await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t }) - } - - // Video channel update? - if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { - await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) - videoInstanceUpdated.VideoChannel = res.locals.videoChannel - - if (hadPrivacyForFederation === true) { - await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) - } - } - - // Schedule an update in the future? - await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t) - - await autoBlacklistVideoIfNeeded({ - video: videoInstanceUpdated, - user: res.locals.oauth.token.User, - isRemote: false, - isNew: false, - isNewFile: false, - transaction: t - }) - - auditLogger.update( - getAuditIdFromRes(res), - new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), - oldVideoAuditView - ) - logger.info('Video with name %s and uuid %s updated.', video.name, video.uuid, lTags(video.uuid)) - - return { videoInstanceUpdated, isNewVideo } - }) - - Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res }) - - await addVideoJobsAfterUpdate({ - video: videoInstanceUpdated, - nameChanged: !!videoInfoToUpdate.name, - oldPrivacy, - isNewVideo - }) - } catch (err) { - // If the transaction is retried, sequelize will think the object has not changed - // So we need to restore the previous fields - await resetSequelizeInstance(videoFromReq) - - throw err - } finally { - videoFileLockReleaser() - } - - return res.type('json') - .status(HttpStatusCode.NO_CONTENT_204) - .end() -} - -async function updateVideoPrivacy (options: { - videoInstance: MVideoFullLight - videoInfoToUpdate: VideoUpdate - hadPrivacyForFederation: boolean - transaction: Transaction -}) { - const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options - const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy) - - const newPrivacy = forceNumber(videoInfoToUpdate.privacy) - setVideoPrivacy(videoInstance, newPrivacy) - - // Delete passwords if video is not anymore password protected - if (videoInstance.privacy === VideoPrivacy.PASSWORD_PROTECTED && newPrivacy !== VideoPrivacy.PASSWORD_PROTECTED) { - await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) - } - - if (newPrivacy === VideoPrivacy.PASSWORD_PROTECTED && exists(videoInfoToUpdate.videoPasswords)) { - await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction) - await VideoPasswordModel.addPasswords(videoInfoToUpdate.videoPasswords, videoInstance.id, transaction) - } - - // Unfederate the video if the new privacy is not compatible with federation - if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) { - await VideoModel.sendDelete(videoInstance, { transaction }) - } - - return isNewVideo -} - -function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) { - if (videoInfoToUpdate.scheduleUpdate) { - return ScheduleVideoUpdateModel.upsert({ - videoId: videoInstance.id, - updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt), - privacy: videoInfoToUpdate.scheduleUpdate.privacy || null - }, { transaction }) - } else if (videoInfoToUpdate.scheduleUpdate === null) { - return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) - } -} diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts deleted file mode 100644 index e520bf4b5..000000000 --- a/server/controllers/api/videos/upload.ts +++ /dev/null @@ -1,287 +0,0 @@ -import express from 'express' -import { move } from 'fs-extra' -import { basename } from 'path' -import { getResumableUploadPath } from '@server/helpers/upload' -import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' -import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue' -import { Redis } from '@server/lib/redis' -import { uploadx } from '@server/lib/uploadx' -import { buildLocalVideoFromReq, buildMoveToObjectStorageJob, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' -import { buildNewFile } from '@server/lib/video-file' -import { VideoPathManager } from '@server/lib/video-path-manager' -import { buildNextVideoState } from '@server/lib/video-state' -import { openapiOperationDoc } from '@server/middlewares/doc' -import { VideoPasswordModel } from '@server/models/video/video-password' -import { VideoSourceModel } from '@server/models/video/video-source' -import { MVideoFile, MVideoFullLight } from '@server/types/models' -import { uuidToShort } from '@shared/extra-utils' -import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models' -import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' -import { createReqFiles } from '../../../helpers/express-utils' -import { logger, loggerTagsFactory } from '../../../helpers/logger' -import { MIMETYPES } from '../../../initializers/constants' -import { sequelizeTypescript } from '../../../initializers/database' -import { Hooks } from '../../../lib/plugins/hooks' -import { generateLocalVideoMiniature } from '../../../lib/thumbnail' -import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' -import { - asyncMiddleware, - asyncRetryTransactionMiddleware, - authenticate, - videosAddLegacyValidator, - videosAddResumableInitValidator, - videosAddResumableValidator -} from '../../../middlewares' -import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' -import { VideoModel } from '../../../models/video/video' - -const lTags = loggerTagsFactory('api', 'video') -const auditLogger = auditLoggerFactory('videos') -const uploadRouter = express.Router() - -const reqVideoFileAdd = createReqFiles( - [ 'videofile', 'thumbnailfile', 'previewfile' ], - { ...MIMETYPES.VIDEO.MIMETYPE_EXT, ...MIMETYPES.IMAGE.MIMETYPE_EXT } -) - -const reqVideoFileAddResumable = createReqFiles( - [ 'thumbnailfile', 'previewfile' ], - MIMETYPES.IMAGE.MIMETYPE_EXT, - getResumableUploadPath() -) - -uploadRouter.post('/upload', - openapiOperationDoc({ operationId: 'uploadLegacy' }), - authenticate, - reqVideoFileAdd, - asyncMiddleware(videosAddLegacyValidator), - asyncRetryTransactionMiddleware(addVideoLegacy) -) - -uploadRouter.post('/upload-resumable', - openapiOperationDoc({ operationId: 'uploadResumableInit' }), - authenticate, - reqVideoFileAddResumable, - asyncMiddleware(videosAddResumableInitValidator), - (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end -) - -uploadRouter.delete('/upload-resumable', - authenticate, - asyncMiddleware(deleteUploadResumableCache), - (req, res) => uploadx.upload(req, res) // Prevent next() call, explicitely tell to uploadx it's the end -) - -uploadRouter.put('/upload-resumable', - openapiOperationDoc({ operationId: 'uploadResumable' }), - authenticate, - uploadx.upload, // uploadx doesn't next() before the file upload completes - asyncMiddleware(videosAddResumableValidator), - asyncMiddleware(addVideoResumable) -) - -// --------------------------------------------------------------------------- - -export { - uploadRouter -} - -// --------------------------------------------------------------------------- - -async function addVideoLegacy (req: express.Request, res: express.Response) { - // Uploading the video could be long - // Set timeout to 10 minutes, as Express's default is 2 minutes - req.setTimeout(1000 * 60 * 10, () => { - logger.error('Video upload has timed out.') - return res.fail({ - status: HttpStatusCode.REQUEST_TIMEOUT_408, - message: 'Video upload has timed out.' - }) - }) - - const videoPhysicalFile = req.files['videofile'][0] - const videoInfo: VideoCreate = req.body - const files = req.files - - const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) - - return res.json(response) -} - -async function addVideoResumable (req: express.Request, res: express.Response) { - const videoPhysicalFile = res.locals.uploadVideoFileResumable - const videoInfo = videoPhysicalFile.metadata - const files = { previewfile: videoInfo.previewfile, thumbnailfile: videoInfo.thumbnailfile } - - const response = await addVideo({ req, res, videoPhysicalFile, videoInfo, files }) - await Redis.Instance.setUploadSession(req.query.upload_id, response) - - return res.json(response) -} - -async function addVideo (options: { - req: express.Request - res: express.Response - videoPhysicalFile: express.VideoUploadFile - videoInfo: VideoCreate - files: express.UploadFiles -}) { - const { req, res, videoPhysicalFile, videoInfo, files } = options - const videoChannel = res.locals.videoChannel - const user = res.locals.oauth.token.User - - let videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) - videoData = await Hooks.wrapObject(videoData, 'filter:api.video.upload.video-attribute.result') - - videoData.state = buildNextVideoState() - videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware - - const video = new VideoModel(videoData) as MVideoFullLight - video.VideoChannel = videoChannel - video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object - - const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' }) - const originalFilename = videoPhysicalFile.originalname - - // Move physical file - const destination = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile) - await move(videoPhysicalFile.path, destination) - // This is important in case if there is another attempt in the retry process - videoPhysicalFile.filename = basename(destination) - videoPhysicalFile.path = destination - - const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ - video, - files, - fallback: type => generateLocalVideoMiniature({ video, videoFile, type }) - }) - - const { videoCreated } = await sequelizeTypescript.transaction(async t => { - const sequelizeOptions = { transaction: t } - - const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight - - await videoCreated.addAndSaveThumbnail(thumbnailModel, t) - await videoCreated.addAndSaveThumbnail(previewModel, t) - - // Do not forget to add video channel information to the created video - videoCreated.VideoChannel = res.locals.videoChannel - - videoFile.videoId = video.id - await videoFile.save(sequelizeOptions) - - video.VideoFiles = [ videoFile ] - - await VideoSourceModel.create({ - filename: originalFilename, - videoId: video.id - }, { transaction: t }) - - await setVideoTags({ video, tags: videoInfo.tags, transaction: t }) - - // Schedule an update in the future? - if (videoInfo.scheduleUpdate) { - await ScheduleVideoUpdateModel.create({ - videoId: video.id, - updateAt: new Date(videoInfo.scheduleUpdate.updateAt), - privacy: videoInfo.scheduleUpdate.privacy || null - }, sequelizeOptions) - } - - await autoBlacklistVideoIfNeeded({ - video, - user, - isRemote: false, - isNew: true, - isNewFile: true, - transaction: t - }) - - if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) { - await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t) - } - - auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) - logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid)) - - return { videoCreated } - }) - - // Channel has a new content, set as updated - await videoCreated.VideoChannel.setAsUpdated() - - addVideoJobsAfterUpload(videoCreated, videoFile) - .catch(err => logger.error('Cannot build new video jobs of %s.', videoCreated.uuid, { err, ...lTags(videoCreated.uuid) })) - - Hooks.runAction('action:api.video.uploaded', { video: videoCreated, req, res }) - - return { - video: { - id: videoCreated.id, - shortUUID: uuidToShort(videoCreated.uuid), - uuid: videoCreated.uuid - } - } -} - -async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) { - const jobs: (CreateJobArgument & CreateJobOptions)[] = [ - { - type: 'manage-video-torrent' as 'manage-video-torrent', - payload: { - videoId: video.id, - videoFileId: videoFile.id, - action: 'create' - } - }, - - { - type: 'generate-video-storyboard' as 'generate-video-storyboard', - payload: { - videoUUID: video.uuid, - // No need to federate, we process these jobs sequentially - federate: false - } - }, - - { - type: 'notify', - payload: { - action: 'new-video', - videoUUID: video.uuid - } - }, - - { - type: 'federate-video' as 'federate-video', - payload: { - videoUUID: video.uuid, - isNewVideo: true - } - } - ] - - if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { - jobs.push(await buildMoveToObjectStorageJob({ video, previousVideoState: undefined })) - } - - if (video.state === VideoState.TO_TRANSCODE) { - jobs.push({ - type: 'transcoding-job-builder' as 'transcoding-job-builder', - payload: { - videoUUID: video.uuid, - optimizeJob: { - isNewVideo: true - } - } - }) - } - - return JobQueue.Instance.createSequentialJobFlow(...jobs) -} - -async function deleteUploadResumableCache (req: express.Request, res: express.Response, next: express.NextFunction) { - await Redis.Instance.deleteUploadSession(req.query.upload_id) - - return next() -} diff --git a/server/controllers/api/videos/view.ts b/server/controllers/api/videos/view.ts deleted file mode 100644 index a747fa334..000000000 --- a/server/controllers/api/videos/view.ts +++ /dev/null @@ -1,60 +0,0 @@ -import express from 'express' -import { Hooks } from '@server/lib/plugins/hooks' -import { VideoViewsManager } from '@server/lib/views/video-views-manager' -import { MVideoId } from '@server/types/models' -import { HttpStatusCode, VideoView } from '@shared/models' -import { asyncMiddleware, methodsValidator, openapiOperationDoc, optionalAuthenticate, videoViewValidator } from '../../../middlewares' -import { UserVideoHistoryModel } from '../../../models/user/user-video-history' - -const viewRouter = express.Router() - -viewRouter.all( - [ '/:videoId/views', '/:videoId/watching' ], - openapiOperationDoc({ operationId: 'addView' }), - methodsValidator([ 'PUT', 'POST' ]), - optionalAuthenticate, - asyncMiddleware(videoViewValidator), - asyncMiddleware(viewVideo) -) - -// --------------------------------------------------------------------------- - -export { - viewRouter -} - -// --------------------------------------------------------------------------- - -async function viewVideo (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo - - const body = req.body as VideoView - - const ip = req.ip - const { successView } = await VideoViewsManager.Instance.processLocalView({ - video, - ip, - currentTime: body.currentTime, - viewEvent: body.viewEvent - }) - - if (successView) { - Hooks.runAction('action:api.video.viewed', { video, ip, req, res }) - } - - await updateUserHistoryIfNeeded(body, video, res) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} - -async function updateUserHistoryIfNeeded (body: VideoView, video: MVideoId, res: express.Response) { - const user = res.locals.oauth?.token.User - if (!user) return - if (user.videosHistoryEnabled !== true) return - - await UserVideoHistoryModel.upsert({ - videoId: video.id, - userId: user.id, - currentTime: body.currentTime - }) -} -- cgit v1.2.3