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/activitypub/client.ts | 482 ------------------- server/controllers/activitypub/inbox.ts | 85 ---- server/controllers/activitypub/index.ts | 17 - server/controllers/activitypub/outbox.ts | 86 ---- server/controllers/activitypub/utils.ts | 12 - 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 --- server/controllers/client.ts | 236 ---------- server/controllers/download.ts | 213 --------- server/controllers/feeds/comment-feeds.ts | 96 ---- server/controllers/feeds/index.ts | 25 - .../controllers/feeds/shared/common-feed-utils.ts | 149 ------ server/controllers/feeds/shared/index.ts | 2 - .../controllers/feeds/shared/video-feed-utils.ts | 66 --- server/controllers/feeds/video-feeds.ts | 189 -------- server/controllers/feeds/video-podcast-feeds.ts | 313 ------------- server/controllers/index.ts | 14 - server/controllers/lazy-static.ts | 128 ----- server/controllers/misc.ts | 210 --------- server/controllers/object-storage-proxy.ts | 60 --- server/controllers/plugins.ts | 175 ------- server/controllers/services.ts | 165 ------- server/controllers/shared/m3u8-playlist.ts | 18 - server/controllers/sitemap.ts | 115 ----- server/controllers/static.ts | 116 ----- server/controllers/tracker.ts | 148 ------ server/controllers/well-known.ts | 125 ----- 90 files changed, 12566 deletions(-) delete mode 100644 server/controllers/activitypub/client.ts delete mode 100644 server/controllers/activitypub/inbox.ts delete mode 100644 server/controllers/activitypub/index.ts delete mode 100644 server/controllers/activitypub/outbox.ts delete mode 100644 server/controllers/activitypub/utils.ts 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 delete mode 100644 server/controllers/client.ts delete mode 100644 server/controllers/download.ts delete mode 100644 server/controllers/feeds/comment-feeds.ts delete mode 100644 server/controllers/feeds/index.ts delete mode 100644 server/controllers/feeds/shared/common-feed-utils.ts delete mode 100644 server/controllers/feeds/shared/index.ts delete mode 100644 server/controllers/feeds/shared/video-feed-utils.ts delete mode 100644 server/controllers/feeds/video-feeds.ts delete mode 100644 server/controllers/feeds/video-podcast-feeds.ts delete mode 100644 server/controllers/index.ts delete mode 100644 server/controllers/lazy-static.ts delete mode 100644 server/controllers/misc.ts delete mode 100644 server/controllers/object-storage-proxy.ts delete mode 100644 server/controllers/plugins.ts delete mode 100644 server/controllers/services.ts delete mode 100644 server/controllers/shared/m3u8-playlist.ts delete mode 100644 server/controllers/sitemap.ts delete mode 100644 server/controllers/static.ts delete mode 100644 server/controllers/tracker.ts delete mode 100644 server/controllers/well-known.ts (limited to 'server/controllers') diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts deleted file mode 100644 index be52f1662..000000000 --- a/server/controllers/activitypub/client.ts +++ /dev/null @@ -1,482 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { activityPubCollectionPagination } from '@server/lib/activitypub/collection' -import { activityPubContextify } from '@server/lib/activitypub/context' -import { getServerActor } from '@server/models/application/application' -import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' -import { VideoCommentObject } from '@shared/models' -import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' -import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' -import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' -import { audiencify, getAudience } from '../../lib/activitypub/audience' -import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send' -import { buildCreateActivity } from '../../lib/activitypub/send/send-create' -import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike' -import { - getLocalVideoCommentsActivityPubUrl, - getLocalVideoDislikesActivityPubUrl, - getLocalVideoLikesActivityPubUrl, - getLocalVideoSharesActivityPubUrl -} from '../../lib/activitypub/url' -import { - activityPubRateLimiter, - asyncMiddleware, - ensureIsLocalChannel, - executeIfActivityPub, - localAccountValidator, - videoChannelsNameWithHostValidator, - videosCustomGetValidator, - videosShareValidator -} from '../../middlewares' -import { cacheRoute } from '../../middlewares/cache/cache' -import { getAccountVideoRateValidatorFactory, getVideoLocalViewerValidator, videoCommentGetValidator } from '../../middlewares/validators' -import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy' -import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' -import { AccountModel } from '../../models/account/account' -import { AccountVideoRateModel } from '../../models/account/account-video-rate' -import { ActorFollowModel } from '../../models/actor/actor-follow' -import { VideoCommentModel } from '../../models/video/video-comment' -import { VideoPlaylistModel } from '../../models/video/video-playlist' -import { VideoShareModel } from '../../models/video/video-share' -import { activityPubResponse } from './utils' - -const activityPubClientRouter = express.Router() -activityPubClientRouter.use(cors()) - -// Intercept ActivityPub client requests - -activityPubClientRouter.get( - [ '/accounts?/:name', '/accounts?/:name/video-channels', '/a/:name', '/a/:name/video-channels' ], - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(localAccountValidator), - asyncMiddleware(accountController) -) -activityPubClientRouter.get('/accounts?/:name/followers', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(localAccountValidator), - asyncMiddleware(accountFollowersController) -) -activityPubClientRouter.get('/accounts?/:name/following', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(localAccountValidator), - asyncMiddleware(accountFollowingController) -) -activityPubClientRouter.get('/accounts?/:name/playlists', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(localAccountValidator), - asyncMiddleware(accountPlaylistsController) -) -activityPubClientRouter.get('/accounts?/:name/likes/:videoId', - executeIfActivityPub, - activityPubRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), - asyncMiddleware(getAccountVideoRateValidatorFactory('like')), - asyncMiddleware(getAccountVideoRateFactory('like')) -) -activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', - executeIfActivityPub, - activityPubRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), - asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')), - asyncMiddleware(getAccountVideoRateFactory('dislike')) -) - -activityPubClientRouter.get( - [ '/videos/watch/:id', '/w/:id' ], - executeIfActivityPub, - activityPubRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS), - asyncMiddleware(videosCustomGetValidator('all')), - asyncMiddleware(videoController) -) -activityPubClientRouter.get('/videos/watch/:id/activity', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('all')), - asyncMiddleware(videoController) -) -activityPubClientRouter.get('/videos/watch/:id/announces', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), - asyncMiddleware(videoAnnouncesController) -) -activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videosShareValidator), - asyncMiddleware(videoAnnounceController) -) -activityPubClientRouter.get('/videos/watch/:id/likes', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), - asyncMiddleware(videoLikesController) -) -activityPubClientRouter.get('/videos/watch/:id/dislikes', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), - asyncMiddleware(videoDislikesController) -) -activityPubClientRouter.get('/videos/watch/:id/comments', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')), - asyncMiddleware(videoCommentsController) -) -activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoCommentGetValidator), - asyncMiddleware(videoCommentController) -) -activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoCommentGetValidator), - asyncMiddleware(videoCommentController) -) - -activityPubClientRouter.get( - [ '/video-channels/:nameWithHost', '/video-channels/:nameWithHost/videos', '/c/:nameWithHost', '/c/:nameWithHost/videos' ], - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - asyncMiddleware(videoChannelController) -) -activityPubClientRouter.get('/video-channels/:nameWithHost/followers', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - asyncMiddleware(videoChannelFollowersController) -) -activityPubClientRouter.get('/video-channels/:nameWithHost/following', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - asyncMiddleware(videoChannelFollowingController) -) -activityPubClientRouter.get('/video-channels/:nameWithHost/playlists', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - asyncMiddleware(videoChannelPlaylistsController) -) - -activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoFileRedundancyGetValidator), - asyncMiddleware(videoRedundancyController) -) -activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistType/:videoId', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoPlaylistRedundancyGetValidator), - asyncMiddleware(videoRedundancyController) -) - -activityPubClientRouter.get( - [ '/video-playlists/:playlistId', '/videos/watch/playlist/:playlistId', '/w/p/:playlistId' ], - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoPlaylistsGetValidator('all')), - asyncMiddleware(videoPlaylistController) -) -activityPubClientRouter.get('/video-playlists/:playlistId/videos/:playlistElementId', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(videoPlaylistElementAPGetValidator), - asyncMiddleware(videoPlaylistElementController) -) - -activityPubClientRouter.get('/videos/local-viewer/:localViewerId', - executeIfActivityPub, - activityPubRateLimiter, - asyncMiddleware(getVideoLocalViewerValidator), - asyncMiddleware(getVideoLocalViewerController) -) - -// --------------------------------------------------------------------------- - -export { - activityPubClientRouter -} - -// --------------------------------------------------------------------------- - -async function accountController (req: express.Request, res: express.Response) { - const account = res.locals.account - - return activityPubResponse(activityPubContextify(await account.toActivityPubObject(), 'Actor'), res) -} - -async function accountFollowersController (req: express.Request, res: express.Response) { - const account = res.locals.account - const activityPubResult = await actorFollowers(req, account.Actor) - - return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) -} - -async function accountFollowingController (req: express.Request, res: express.Response) { - const account = res.locals.account - const activityPubResult = await actorFollowing(req, account.Actor) - - return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) -} - -async function accountPlaylistsController (req: express.Request, res: express.Response) { - const account = res.locals.account - const activityPubResult = await actorPlaylists(req, { account }) - - return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) -} - -async function videoChannelPlaylistsController (req: express.Request, res: express.Response) { - const channel = res.locals.videoChannel - const activityPubResult = await actorPlaylists(req, { channel }) - - return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) -} - -function getAccountVideoRateFactory (rateType: VideoRateType) { - return (req: express.Request, res: express.Response) => { - const accountVideoRate = res.locals.accountVideoRate - - const byActor = accountVideoRate.Account.Actor - const APObject = rateType === 'like' - ? buildLikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video) - : buildDislikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video) - - return activityPubResponse(activityPubContextify(APObject, 'Rate'), res) - } -} - -async function videoController (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - - if (redirectIfNotOwned(video.url, res)) return - - // We need captions to render AP object - const videoAP = await video.lightAPToFullAP(undefined) - - const audience = getAudience(videoAP.VideoChannel.Account.Actor, videoAP.privacy === VideoPrivacy.PUBLIC) - const videoObject = audiencify(await videoAP.toActivityPubObject(), audience) - - if (req.path.endsWith('/activity')) { - const data = buildCreateActivity(videoAP.url, video.VideoChannel.Account.Actor, videoObject, audience) - return activityPubResponse(activityPubContextify(data, 'Video'), res) - } - - return activityPubResponse(activityPubContextify(videoObject, 'Video'), res) -} - -async function videoAnnounceController (req: express.Request, res: express.Response) { - const share = res.locals.videoShare - - if (redirectIfNotOwned(share.url, res)) return - - const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll, undefined) - - return activityPubResponse(activityPubContextify(activity, 'Announce'), res) -} - -async function videoAnnouncesController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo - - if (redirectIfNotOwned(video.url, res)) return - - const handler = async (start: number, count: number) => { - const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) - return { - total: result.total, - data: result.data.map(r => r.url) - } - } - const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page) - - return activityPubResponse(activityPubContextify(json, 'Collection'), res) -} - -async function videoLikesController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo - - if (redirectIfNotOwned(video.url, res)) return - - const json = await videoRates(req, 'like', video, getLocalVideoLikesActivityPubUrl(video)) - - return activityPubResponse(activityPubContextify(json, 'Collection'), res) -} - -async function videoDislikesController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo - - if (redirectIfNotOwned(video.url, res)) return - - const json = await videoRates(req, 'dislike', video, getLocalVideoDislikesActivityPubUrl(video)) - - return activityPubResponse(activityPubContextify(json, 'Collection'), res) -} - -async function videoCommentsController (req: express.Request, res: express.Response) { - const video = res.locals.onlyImmutableVideo - - if (redirectIfNotOwned(video.url, res)) return - - const handler = async (start: number, count: number) => { - const result = await VideoCommentModel.listAndCountByVideoForAP({ video, start, count }) - - return { - total: result.total, - data: result.data.map(r => r.url) - } - } - const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page) - - return activityPubResponse(activityPubContextify(json, 'Collection'), res) -} - -async function videoChannelController (req: express.Request, res: express.Response) { - const videoChannel = res.locals.videoChannel - - return activityPubResponse(activityPubContextify(await videoChannel.toActivityPubObject(), 'Actor'), res) -} - -async function videoChannelFollowersController (req: express.Request, res: express.Response) { - const videoChannel = res.locals.videoChannel - const activityPubResult = await actorFollowers(req, videoChannel.Actor) - - return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) -} - -async function videoChannelFollowingController (req: express.Request, res: express.Response) { - const videoChannel = res.locals.videoChannel - const activityPubResult = await actorFollowing(req, videoChannel.Actor) - - return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) -} - -async function videoCommentController (req: express.Request, res: express.Response) { - const videoComment = res.locals.videoCommentFull - - if (redirectIfNotOwned(videoComment.url, res)) return - - const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) - const isPublic = true // Comments are always public - let videoCommentObject = videoComment.toActivityPubObject(threadParentComments) - - if (videoComment.Account) { - const audience = getAudience(videoComment.Account.Actor, isPublic) - videoCommentObject = audiencify(videoCommentObject, audience) - - if (req.path.endsWith('/activity')) { - const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience) - return activityPubResponse(activityPubContextify(data, 'Comment'), res) - } - } - - return activityPubResponse(activityPubContextify(videoCommentObject, 'Comment'), res) -} - -async function videoRedundancyController (req: express.Request, res: express.Response) { - const videoRedundancy = res.locals.videoRedundancy - - if (redirectIfNotOwned(videoRedundancy.url, res)) return - - const serverActor = await getServerActor() - - const audience = getAudience(serverActor) - const object = audiencify(videoRedundancy.toActivityPubObject(), audience) - - if (req.path.endsWith('/activity')) { - const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience) - return activityPubResponse(activityPubContextify(data, 'CacheFile'), res) - } - - return activityPubResponse(activityPubContextify(object, 'CacheFile'), res) -} - -async function videoPlaylistController (req: express.Request, res: express.Response) { - const playlist = res.locals.videoPlaylistFull - - if (redirectIfNotOwned(playlist.url, res)) return - - // We need more attributes - playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId) - - const json = await playlist.toActivityPubObject(req.query.page, null) - const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) - const object = audiencify(json, audience) - - return activityPubResponse(activityPubContextify(object, 'Playlist'), res) -} - -function videoPlaylistElementController (req: express.Request, res: express.Response) { - const videoPlaylistElement = res.locals.videoPlaylistElementAP - - if (redirectIfNotOwned(videoPlaylistElement.url, res)) return - - const json = videoPlaylistElement.toActivityPubObject() - return activityPubResponse(activityPubContextify(json, 'Playlist'), res) -} - -function getVideoLocalViewerController (req: express.Request, res: express.Response) { - const localViewer = res.locals.localViewerFull - - return activityPubResponse(activityPubContextify(localViewer.toActivityPubObject(), 'WatchAction'), res) -} - -// --------------------------------------------------------------------------- - -function actorFollowing (req: express.Request, actor: MActorId) { - const handler = (start: number, count: number) => { - return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) - } - - return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) -} - -function actorFollowers (req: express.Request, actor: MActorId) { - const handler = (start: number, count: number) => { - return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count) - } - - return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) -} - -function actorPlaylists (req: express.Request, options: { account: MAccountId } | { channel: MChannelId }) { - const handler = (start: number, count: number) => { - return VideoPlaylistModel.listPublicUrlsOfForAP(options, start, count) - } - - return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) -} - -function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) { - const handler = async (start: number, count: number) => { - const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) - return { - total: result.total, - data: result.data.map(r => r.url) - } - } - return activityPubCollectionPagination(url, handler, req.query.page) -} - -function redirectIfNotOwned (url: string, res: express.Response) { - if (url.startsWith(WEBSERVER.URL) === false) { - res.redirect(url) - return true - } - - return false -} diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts deleted file mode 100644 index 862c7baf1..000000000 --- a/server/controllers/activitypub/inbox.ts +++ /dev/null @@ -1,85 +0,0 @@ -import express from 'express' -import { InboxManager } from '@server/lib/activitypub/inbox-manager' -import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActivity } from '@shared/models' -import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' -import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' -import { logger } from '../../helpers/logger' -import { - activityPubRateLimiter, - asyncMiddleware, - checkSignature, - ensureIsLocalChannel, - localAccountValidator, - signatureValidator, - videoChannelsNameWithHostValidator -} from '../../middlewares' -import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' - -const inboxRouter = express.Router() - -inboxRouter.post('/inbox', - activityPubRateLimiter, - signatureValidator, - asyncMiddleware(checkSignature), - asyncMiddleware(activityPubValidator), - inboxController -) - -inboxRouter.post('/accounts/:name/inbox', - activityPubRateLimiter, - signatureValidator, - asyncMiddleware(checkSignature), - asyncMiddleware(localAccountValidator), - asyncMiddleware(activityPubValidator), - inboxController -) - -inboxRouter.post('/video-channels/:nameWithHost/inbox', - activityPubRateLimiter, - signatureValidator, - asyncMiddleware(checkSignature), - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - asyncMiddleware(activityPubValidator), - inboxController -) - -// --------------------------------------------------------------------------- - -export { - inboxRouter -} - -// --------------------------------------------------------------------------- - -function inboxController (req: express.Request, res: express.Response) { - const rootActivity: RootActivity = req.body - let activities: Activity[] - - if ([ 'Collection', 'CollectionPage' ].includes(rootActivity.type)) { - activities = (rootActivity as ActivityPubCollection).items - } else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].includes(rootActivity.type)) { - activities = (rootActivity as ActivityPubOrderedCollection).orderedItems - } else { - activities = [ rootActivity as Activity ] - } - - // Only keep activities we are able to process - logger.debug('Filtering %d activities...', activities.length) - activities = activities.filter(a => isActivityValid(a)) - logger.debug('We keep %d activities.', activities.length, { activities }) - - const accountOrChannel = res.locals.account || res.locals.videoChannel - - logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url) - - InboxManager.Instance.addInboxMessage({ - activities, - signatureActor: res.locals.signature.actor, - inboxActor: accountOrChannel - ? accountOrChannel.Actor - : undefined - }) - - return res.status(HttpStatusCode.NO_CONTENT_204).end() -} diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts deleted file mode 100644 index c14d95108..000000000 --- a/server/controllers/activitypub/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import express from 'express' - -import { activityPubClientRouter } from './client' -import { inboxRouter } from './inbox' -import { outboxRouter } from './outbox' - -const activityPubRouter = express.Router() - -activityPubRouter.use('/', inboxRouter) -activityPubRouter.use('/', outboxRouter) -activityPubRouter.use('/', activityPubClientRouter) - -// --------------------------------------------------------------------------- - -export { - activityPubRouter -} diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts deleted file mode 100644 index 8c88b6971..000000000 --- a/server/controllers/activitypub/outbox.ts +++ /dev/null @@ -1,86 +0,0 @@ -import express from 'express' -import { activityPubCollectionPagination } from '@server/lib/activitypub/collection' -import { activityPubContextify } from '@server/lib/activitypub/context' -import { MActorLight } from '@server/types/models' -import { Activity } from '../../../shared/models/activitypub/activity' -import { VideoPrivacy } from '../../../shared/models/videos' -import { logger } from '../../helpers/logger' -import { buildAudience } from '../../lib/activitypub/audience' -import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send' -import { - activityPubRateLimiter, - asyncMiddleware, - ensureIsLocalChannel, - localAccountValidator, - videoChannelsNameWithHostValidator -} from '../../middlewares' -import { apPaginationValidator } from '../../middlewares/validators/activitypub' -import { VideoModel } from '../../models/video/video' -import { activityPubResponse } from './utils' - -const outboxRouter = express.Router() - -outboxRouter.get('/accounts/:name/outbox', - activityPubRateLimiter, - apPaginationValidator, - localAccountValidator, - asyncMiddleware(outboxController) -) - -outboxRouter.get('/video-channels/:nameWithHost/outbox', - activityPubRateLimiter, - apPaginationValidator, - asyncMiddleware(videoChannelsNameWithHostValidator), - ensureIsLocalChannel, - asyncMiddleware(outboxController) -) - -// --------------------------------------------------------------------------- - -export { - outboxRouter -} - -// --------------------------------------------------------------------------- - -async function outboxController (req: express.Request, res: express.Response) { - const accountOrVideoChannel = res.locals.account || res.locals.videoChannel - const actor = accountOrVideoChannel.Actor - const actorOutboxUrl = actor.url + '/outbox' - - logger.info('Receiving outbox request for %s.', actorOutboxUrl) - - const handler = (start: number, count: number) => buildActivities(actor, start, count) - const json = await activityPubCollectionPagination(actorOutboxUrl, handler, req.query.page, req.query.size) - - return activityPubResponse(activityPubContextify(json, 'Collection'), res) -} - -async function buildActivities (actor: MActorLight, start: number, count: number) { - const data = await VideoModel.listAllAndSharedByActorForOutbox(actor.id, start, count) - const activities: Activity[] = [] - - for (const video of data.data) { - const byActor = video.VideoChannel.Account.Actor - const createActivityAudience = buildAudience([ byActor.followersUrl ], video.privacy === VideoPrivacy.PUBLIC) - - // This is a shared video - if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { - const videoShare = video.VideoShares[0] - const announceActivity = buildAnnounceActivity(videoShare.url, actor, video.url, createActivityAudience) - - activities.push(announceActivity) - } else { - // FIXME: only use the video URL to reduce load. Breaks compat with PeerTube < 6.0.0 - const videoObject = await video.toActivityPubObject() - const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) - - activities.push(createActivity) - } - } - - return { - data: activities, - total: data.total - } -} diff --git a/server/controllers/activitypub/utils.ts b/server/controllers/activitypub/utils.ts deleted file mode 100644 index 5de38eb43..000000000 --- a/server/controllers/activitypub/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import express from 'express' - -async function activityPubResponse (promise: Promise, res: express.Response) { - const data = await promise - - return res.type('application/activity+json; charset=utf-8') - .json(data) -} - -export { - activityPubResponse -} 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 - }) -} diff --git a/server/controllers/client.ts b/server/controllers/client.ts deleted file mode 100644 index 2d0c49904..000000000 --- a/server/controllers/client.ts +++ /dev/null @@ -1,236 +0,0 @@ -import express from 'express' -import { constants, promises as fs } from 'fs' -import { readFile } from 'fs-extra' -import { join } from 'path' -import { logger } from '@server/helpers/logger' -import { CONFIG } from '@server/initializers/config' -import { Hooks } from '@server/lib/plugins/hooks' -import { root } from '@shared/core-utils' -import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' -import { HttpStatusCode } from '@shared/models' -import { STATIC_MAX_AGE } from '../initializers/constants' -import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html' -import { asyncMiddleware, buildRateLimiter, embedCSP } from '../middlewares' - -const clientsRouter = express.Router() - -const clientsRateLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.CLIENT.WINDOW_MS, - max: CONFIG.RATES_LIMIT.CLIENT.MAX -}) - -const distPath = join(root(), 'client', 'dist') -const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html') - -// Special route that add OpenGraph and oEmbed tags -// Do not use a template engine for a so little thing -clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ], - clientsRateLimiter, - asyncMiddleware(generateWatchPlaylistHtmlPage) -) - -clientsRouter.use([ '/w/:id', '/videos/watch/:id' ], - clientsRateLimiter, - asyncMiddleware(generateWatchHtmlPage) -) - -clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], - clientsRateLimiter, - asyncMiddleware(generateAccountHtmlPage) -) - -clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], - clientsRateLimiter, - asyncMiddleware(generateVideoChannelHtmlPage) -) - -clientsRouter.use('/@:nameWithHost', - clientsRateLimiter, - asyncMiddleware(generateActorHtmlPage) -) - -const embedMiddlewares = [ - clientsRateLimiter, - - CONFIG.CSP.ENABLED - ? embedCSP - : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), - - // Set headers - (req: express.Request, res: express.Response, next: express.NextFunction) => { - res.removeHeader('X-Frame-Options') - - // Don't cache HTML file since it's an index to the immutable JS/CSS files - res.setHeader('Cache-Control', 'public, max-age=0') - - next() - }, - - asyncMiddleware(generateEmbedHtmlPage) -] - -clientsRouter.use('/videos/embed', ...embedMiddlewares) -clientsRouter.use('/video-playlists/embed', ...embedMiddlewares) - -const testEmbedController = (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath) - -clientsRouter.use('/videos/test-embed', clientsRateLimiter, testEmbedController) -clientsRouter.use('/video-playlists/test-embed', clientsRateLimiter, testEmbedController) - -// Dynamic PWA manifest -clientsRouter.get('/manifest.webmanifest', clientsRateLimiter, asyncMiddleware(generateManifest)) - -// Static client overrides -// Must be consistent with static client overrides redirections in /support/nginx/peertube -const staticClientOverrides = [ - 'assets/images/logo.svg', - 'assets/images/favicon.png', - 'assets/images/icons/icon-36x36.png', - 'assets/images/icons/icon-48x48.png', - 'assets/images/icons/icon-72x72.png', - 'assets/images/icons/icon-96x96.png', - 'assets/images/icons/icon-144x144.png', - 'assets/images/icons/icon-192x192.png', - 'assets/images/icons/icon-512x512.png', - 'assets/images/default-playlist.jpg', - 'assets/images/default-avatar-account.png', - 'assets/images/default-avatar-account-48x48.png', - 'assets/images/default-avatar-video-channel.png', - 'assets/images/default-avatar-video-channel-48x48.png' -] - -for (const staticClientOverride of staticClientOverrides) { - const overridePhysicalPath = join(CONFIG.STORAGE.CLIENT_OVERRIDES_DIR, staticClientOverride) - clientsRouter.use(`/client/${staticClientOverride}`, asyncMiddleware(serveClientOverride(overridePhysicalPath))) -} - -clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations) -clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT })) - -// 404 for static files not found -clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => { - res.status(HttpStatusCode.NOT_FOUND_404).end() -}) - -// Always serve index client page (the client is a single page application, let it handle routing) -// Try to provide the right language index.html -clientsRouter.use('/(:language)?', - clientsRateLimiter, - asyncMiddleware(serveIndexHTML) -) - -// --------------------------------------------------------------------------- - -export { - clientsRouter -} - -// --------------------------------------------------------------------------- - -function serveServerTranslations (req: express.Request, res: express.Response) { - const locale = req.params.locale - const file = req.params.file - - if (is18nLocale(locale) && LOCALE_FILES.includes(file)) { - const completeLocale = getCompleteLocale(locale) - const completeFileLocale = buildFileLocale(completeLocale) - - const path = join(__dirname, `../../../client/dist/locale/${file}.${completeFileLocale}.json`) - return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) - } - - return res.status(HttpStatusCode.NOT_FOUND_404).end() -} - -async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { - const hookName = req.originalUrl.startsWith('/video-playlists/') - ? 'filter:html.embed.video-playlist.allowed.result' - : 'filter:html.embed.video.allowed.result' - - const allowParameters = { req } - - const allowedResult = await Hooks.wrapFun( - isEmbedAllowed, - allowParameters, - hookName - ) - - if (!allowedResult || allowedResult.allowed !== true) { - logger.info('Embed is not allowed.', { allowedResult }) - - return sendHTML(allowedResult?.html || '', res) - } - - const html = await ClientHtml.getEmbedHTML() - - return sendHTML(html, res) -} - -async function generateWatchHtmlPage (req: express.Request, res: express.Response) { - // Thread link is '/w/:videoId;threadId=:threadId' - // So to get the videoId we need to remove the last part - let videoId = req.params.id + '' - - const threadIdIndex = videoId.indexOf(';threadId') - if (threadIdIndex !== -1) videoId = videoId.substring(0, threadIdIndex) - - const html = await ClientHtml.getWatchHTMLPage(videoId, req, res) - - return sendHTML(html, res, true) -} - -async function generateWatchPlaylistHtmlPage (req: express.Request, res: express.Response) { - const html = await ClientHtml.getWatchPlaylistHTMLPage(req.params.id + '', req, res) - - return sendHTML(html, res, true) -} - -async function generateAccountHtmlPage (req: express.Request, res: express.Response) { - const html = await ClientHtml.getAccountHTMLPage(req.params.nameWithHost, req, res) - - return sendHTML(html, res, true) -} - -async function generateVideoChannelHtmlPage (req: express.Request, res: express.Response) { - const html = await ClientHtml.getVideoChannelHTMLPage(req.params.nameWithHost, req, res) - - return sendHTML(html, res, true) -} - -async function generateActorHtmlPage (req: express.Request, res: express.Response) { - const html = await ClientHtml.getActorHTMLPage(req.params.nameWithHost, req, res) - - return sendHTML(html, res, true) -} - -async function generateManifest (req: express.Request, res: express.Response) { - const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest') - const manifestJson = await readFile(manifestPhysicalPath, 'utf8') - const manifest = JSON.parse(manifestJson) - - manifest.name = CONFIG.INSTANCE.NAME - manifest.short_name = CONFIG.INSTANCE.NAME - manifest.description = CONFIG.INSTANCE.SHORT_DESCRIPTION - - res.json(manifest) -} - -function serveClientOverride (path: string) { - return async (req: express.Request, res: express.Response, next: express.NextFunction) => { - try { - await fs.access(path, constants.F_OK) - // Serve override client - res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) - } catch { - // Serve dist client - next() - } - } -} - -type AllowedResult = { allowed: boolean, html?: string } -function isEmbedAllowed (_object: { - req: express.Request -}): AllowedResult { - return { allowed: true } -} diff --git a/server/controllers/download.ts b/server/controllers/download.ts deleted file mode 100644 index 4b94e34bd..000000000 --- a/server/controllers/download.ts +++ /dev/null @@ -1,213 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { logger } from '@server/helpers/logger' -import { VideoTorrentsSimpleFileCache } from '@server/lib/files-cache' -import { generateHLSFilePresignedUrl, generateWebVideoPresignedUrl } from '@server/lib/object-storage' -import { Hooks } from '@server/lib/plugins/hooks' -import { VideoPathManager } from '@server/lib/video-path-manager' -import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' -import { forceNumber } from '@shared/core-utils' -import { HttpStatusCode, VideoStorage, VideoStreamingPlaylistType } from '@shared/models' -import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' -import { asyncMiddleware, optionalAuthenticate, videosDownloadValidator } from '../middlewares' - -const downloadRouter = express.Router() - -downloadRouter.use(cors()) - -downloadRouter.use( - STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', - asyncMiddleware(downloadTorrent) -) - -downloadRouter.use( - STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', - optionalAuthenticate, - asyncMiddleware(videosDownloadValidator), - asyncMiddleware(downloadVideoFile) -) - -downloadRouter.use( - STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', - optionalAuthenticate, - asyncMiddleware(videosDownloadValidator), - asyncMiddleware(downloadHLSVideoFile) -) - -// --------------------------------------------------------------------------- - -export { - downloadRouter -} - -// --------------------------------------------------------------------------- - -async function downloadTorrent (req: express.Request, res: express.Response) { - const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) - if (!result) { - return res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'Torrent file not found' - }) - } - - const allowParameters = { - req, - res, - torrentPath: result.path, - downloadName: result.downloadName - } - - const allowedResult = await Hooks.wrapFun( - isTorrentDownloadAllowed, - allowParameters, - 'filter:api.download.torrent.allowed.result' - ) - - if (!checkAllowResult(res, allowParameters, allowedResult)) return - - return res.download(result.path, result.downloadName) -} - -async function downloadVideoFile (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - - const videoFile = getVideoFile(req, video.VideoFiles) - if (!videoFile) { - return res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'Video file not found' - }) - } - - const allowParameters = { - req, - res, - video, - videoFile - } - - const allowedResult = await Hooks.wrapFun( - isVideoDownloadAllowed, - allowParameters, - 'filter:api.download.video.allowed.result' - ) - - if (!checkAllowResult(res, allowParameters, allowedResult)) return - - // Express uses basename on filename parameter - const videoName = video.name.replace(/[/\\]/g, '_') - const downloadFilename = `${videoName}-${videoFile.resolution}p${videoFile.extname}` - - if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { - return redirectToObjectStorage({ req, res, video, file: videoFile, downloadFilename }) - } - - await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(video), path => { - return res.download(path, downloadFilename) - }) -} - -async function downloadHLSVideoFile (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - const streamingPlaylist = getHLSPlaylist(video) - if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end - - const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) - if (!videoFile) { - return res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'Video file not found' - }) - } - - const allowParameters = { - req, - res, - video, - streamingPlaylist, - videoFile - } - - const allowedResult = await Hooks.wrapFun( - isVideoDownloadAllowed, - allowParameters, - 'filter:api.download.video.allowed.result' - ) - - if (!checkAllowResult(res, allowParameters, allowedResult)) return - - const downloadFilename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` - - if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { - return redirectToObjectStorage({ req, res, video, streamingPlaylist, file: videoFile, downloadFilename }) - } - - await VideoPathManager.Instance.makeAvailableVideoFile(videoFile.withVideoOrPlaylist(streamingPlaylist), path => { - return res.download(path, downloadFilename) - }) -} - -function getVideoFile (req: express.Request, files: MVideoFile[]) { - const resolution = forceNumber(req.params.resolution) - return files.find(f => f.resolution === resolution) -} - -function getHLSPlaylist (video: MVideoFullLight) { - const playlist = video.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) - if (!playlist) return undefined - - return Object.assign(playlist, { Video: video }) -} - -type AllowedResult = { - allowed: boolean - errorMessage?: string -} - -function isTorrentDownloadAllowed (_object: { - torrentPath: string -}): AllowedResult { - return { allowed: true } -} - -function isVideoDownloadAllowed (_object: { - video: MVideo - videoFile: MVideoFile - streamingPlaylist?: MStreamingPlaylist -}): AllowedResult { - return { allowed: true } -} - -function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { - if (!result || result.allowed !== true) { - logger.info('Download is not allowed.', { result, allowParameters }) - - res.fail({ - status: HttpStatusCode.FORBIDDEN_403, - message: result?.errorMessage || 'Refused download' - }) - return false - } - - return true -} - -async function redirectToObjectStorage (options: { - req: express.Request - res: express.Response - video: MVideo - file: MVideoFile - streamingPlaylist?: MStreamingPlaylistVideo - downloadFilename: string -}) { - const { res, video, streamingPlaylist, file, downloadFilename } = options - - const url = streamingPlaylist - ? await generateHLSFilePresignedUrl({ streamingPlaylist, file, downloadFilename }) - : await generateWebVideoPresignedUrl({ file, downloadFilename }) - - logger.debug('Generating pre-signed URL %s for video %s', url, video.uuid) - - return res.redirect(url) -} diff --git a/server/controllers/feeds/comment-feeds.ts b/server/controllers/feeds/comment-feeds.ts deleted file mode 100644 index c013662ea..000000000 --- a/server/controllers/feeds/comment-feeds.ts +++ /dev/null @@ -1,96 +0,0 @@ -import express from 'express' -import { toSafeHtml } from '@server/helpers/markdown' -import { cacheRouteFactory } from '@server/middlewares' -import { CONFIG } from '../../initializers/config' -import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' -import { - asyncMiddleware, - feedsFormatValidator, - setFeedFormatContentType, - videoCommentsFeedsValidator, - feedsAccountOrChannelFiltersValidator -} from '../../middlewares' -import { VideoCommentModel } from '../../models/video/video-comment' -import { buildFeedMetadata, initFeed, sendFeed } from './shared' - -const commentFeedsRouter = express.Router() - -// --------------------------------------------------------------------------- - -const { middleware: cacheRouteMiddleware } = cacheRouteFactory({ - headerBlacklist: [ 'Content-Type' ] -}) - -// --------------------------------------------------------------------------- - -commentFeedsRouter.get('/video-comments.:format', - feedsFormatValidator, - setFeedFormatContentType, - cacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS), - asyncMiddleware(feedsAccountOrChannelFiltersValidator), - asyncMiddleware(videoCommentsFeedsValidator), - asyncMiddleware(generateVideoCommentsFeed) -) - -// --------------------------------------------------------------------------- - -export { - commentFeedsRouter -} - -// --------------------------------------------------------------------------- - -async function generateVideoCommentsFeed (req: express.Request, res: express.Response) { - const start = 0 - const video = res.locals.videoAll - const account = res.locals.account - const videoChannel = res.locals.videoChannel - - const comments = await VideoCommentModel.listForFeed({ - start, - count: CONFIG.FEEDS.COMMENTS.COUNT, - videoId: video ? video.id : undefined, - accountId: account ? account.id : undefined, - videoChannelId: videoChannel ? videoChannel.id : undefined - }) - - const { name, description, imageUrl, link } = await buildFeedMetadata({ video, account, videoChannel }) - - const feed = initFeed({ - name, - description, - imageUrl, - isPodcast: false, - link, - resourceType: 'video-comments', - queryString: new URL(WEBSERVER.URL + req.originalUrl).search - }) - - // Adding video items to the feed, one at a time - for (const comment of comments) { - const localLink = WEBSERVER.URL + comment.getCommentStaticPath() - - let title = comment.Video.name - const author: { name: string, link: string }[] = [] - - if (comment.Account) { - title += ` - ${comment.Account.getDisplayName()}` - author.push({ - name: comment.Account.getDisplayName(), - link: comment.Account.Actor.url - }) - } - - feed.addItem({ - title, - id: localLink, - link: localLink, - content: toSafeHtml(comment.text), - author, - date: comment.createdAt - }) - } - - // Now the feed generation is done, let's send it! - return sendFeed(feed, req, res) -} diff --git a/server/controllers/feeds/index.ts b/server/controllers/feeds/index.ts deleted file mode 100644 index 19352318d..000000000 --- a/server/controllers/feeds/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import express from 'express' -import { CONFIG } from '@server/initializers/config' -import { buildRateLimiter } from '@server/middlewares' -import { commentFeedsRouter } from './comment-feeds' -import { videoFeedsRouter } from './video-feeds' -import { videoPodcastFeedsRouter } from './video-podcast-feeds' - -const feedsRouter = express.Router() - -const feedsRateLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.FEEDS.WINDOW_MS, - max: CONFIG.RATES_LIMIT.FEEDS.MAX -}) - -feedsRouter.use('/feeds', feedsRateLimiter) - -feedsRouter.use('/feeds', commentFeedsRouter) -feedsRouter.use('/feeds', videoFeedsRouter) -feedsRouter.use('/feeds', videoPodcastFeedsRouter) - -// --------------------------------------------------------------------------- - -export { - feedsRouter -} diff --git a/server/controllers/feeds/shared/common-feed-utils.ts b/server/controllers/feeds/shared/common-feed-utils.ts deleted file mode 100644 index 9e2f8adbb..000000000 --- a/server/controllers/feeds/shared/common-feed-utils.ts +++ /dev/null @@ -1,149 +0,0 @@ -import express from 'express' -import { Feed } from '@peertube/feed' -import { CustomTag, CustomXMLNS, Person } from '@peertube/feed/lib/typings' -import { mdToOneLinePlainText } from '@server/helpers/markdown' -import { CONFIG } from '@server/initializers/config' -import { WEBSERVER } from '@server/initializers/constants' -import { getBiggestActorImage } from '@server/lib/actor-image' -import { UserModel } from '@server/models/user/user' -import { MAccountDefault, MChannelBannerAccountDefault, MUser, MVideoFullLight } from '@server/types/models' -import { pick } from '@shared/core-utils' -import { ActorImageType } from '@shared/models' - -export function initFeed (parameters: { - name: string - description: string - imageUrl: string - isPodcast: boolean - link?: string - locked?: { isLocked: boolean, email: string } - author?: { - name: string - link: string - imageUrl: string - } - person?: Person[] - resourceType?: 'videos' | 'video-comments' - queryString?: string - medium?: string - stunServers?: string[] - trackers?: string[] - customXMLNS?: CustomXMLNS[] - customTags?: CustomTag[] -}) { - const webserverUrl = WEBSERVER.URL - const { name, description, link, imageUrl, isPodcast, resourceType, queryString, medium } = parameters - - return new Feed({ - title: name, - description: mdToOneLinePlainText(description), - // updated: TODO: somehowGetLatestUpdate, // optional, default = today - id: link || webserverUrl, - link: link || webserverUrl, - image: imageUrl, - favicon: webserverUrl + '/client/assets/images/favicon.png', - copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` + - ` and potential licenses granted by each content's rightholder.`, - generator: `Toraifōsu`, // ^.~ - medium: medium || 'video', - feedLinks: { - json: `${webserverUrl}/feeds/${resourceType}.json${queryString}`, - atom: `${webserverUrl}/feeds/${resourceType}.atom${queryString}`, - rss: isPodcast - ? `${webserverUrl}/feeds/podcast/videos.xml${queryString}` - : `${webserverUrl}/feeds/${resourceType}.xml${queryString}` - }, - - ...pick(parameters, [ 'stunServers', 'trackers', 'customXMLNS', 'customTags', 'author', 'person', 'locked' ]) - }) -} - -export function sendFeed (feed: Feed, req: express.Request, res: express.Response) { - const format = req.params.format - - if (format === 'atom' || format === 'atom1') { - return res.send(feed.atom1()).end() - } - - if (format === 'json' || format === 'json1') { - return res.send(feed.json1()).end() - } - - if (format === 'rss' || format === 'rss2') { - return res.send(feed.rss2()).end() - } - - // We're in the ambiguous '.xml' case and we look at the format query parameter - if (req.query.format === 'atom' || req.query.format === 'atom1') { - return res.send(feed.atom1()).end() - } - - return res.send(feed.rss2()).end() -} - -export async function buildFeedMetadata (options: { - videoChannel?: MChannelBannerAccountDefault - account?: MAccountDefault - video?: MVideoFullLight -}) { - const { video, videoChannel, account } = options - - let imageUrl = WEBSERVER.URL + '/client/assets/images/icons/icon-96x96.png' - let accountImageUrl: string - let name: string - let userName: string - let description: string - let email: string - let link: string - let accountLink: string - let user: MUser - - if (videoChannel) { - name = videoChannel.getDisplayName() - description = videoChannel.description - link = videoChannel.getClientUrl() - accountLink = videoChannel.Account.getClientUrl() - - if (videoChannel.Actor.hasImage(ActorImageType.AVATAR)) { - const videoChannelAvatar = getBiggestActorImage(videoChannel.Actor.Avatars) - imageUrl = WEBSERVER.URL + videoChannelAvatar.getStaticPath() - } - - if (videoChannel.Account.Actor.hasImage(ActorImageType.AVATAR)) { - const accountAvatar = getBiggestActorImage(videoChannel.Account.Actor.Avatars) - accountImageUrl = WEBSERVER.URL + accountAvatar.getStaticPath() - } - - user = await UserModel.loadById(videoChannel.Account.userId) - userName = videoChannel.Account.getDisplayName() - } else if (account) { - name = account.getDisplayName() - description = account.description - link = account.getClientUrl() - accountLink = link - - if (account.Actor.hasImage(ActorImageType.AVATAR)) { - const accountAvatar = getBiggestActorImage(account.Actor.Avatars) - imageUrl = WEBSERVER.URL + accountAvatar?.getStaticPath() - accountImageUrl = imageUrl - } - - user = await UserModel.loadById(account.userId) - } else if (video) { - name = video.name - description = video.description - link = video.url - } else { - name = CONFIG.INSTANCE.NAME - description = CONFIG.INSTANCE.DESCRIPTION - link = WEBSERVER.URL - } - - // If the user is local, has a verified email address, and allows it to be publicly displayed - // Return it so the owner can prove ownership of their feed - if (user && !user.pluginAuth && user.emailVerified && user.emailPublic) { - email = user.email - } - - return { name, userName, description, imageUrl, accountImageUrl, email, link, accountLink } -} diff --git a/server/controllers/feeds/shared/index.ts b/server/controllers/feeds/shared/index.ts deleted file mode 100644 index 0136c8477..000000000 --- a/server/controllers/feeds/shared/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './video-feed-utils' -export * from './common-feed-utils' diff --git a/server/controllers/feeds/shared/video-feed-utils.ts b/server/controllers/feeds/shared/video-feed-utils.ts deleted file mode 100644 index b154e04fa..000000000 --- a/server/controllers/feeds/shared/video-feed-utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' -import { CONFIG } from '@server/initializers/config' -import { WEBSERVER } from '@server/initializers/constants' -import { getServerActor } from '@server/models/application/application' -import { getCategoryLabel } from '@server/models/video/formatter' -import { DisplayOnlyForFollowerOptions } from '@server/models/video/sql/video' -import { VideoModel } from '@server/models/video/video' -import { MThumbnail, MUserDefault } from '@server/types/models' -import { VideoInclude } from '@shared/models' - -export async function getVideosForFeeds (options: { - sort: string - nsfw: boolean - isLocal: boolean - include: VideoInclude - - accountId?: number - videoChannelId?: number - displayOnlyForFollower?: DisplayOnlyForFollowerOptions - user?: MUserDefault -}) { - const server = await getServerActor() - - const { data } = await VideoModel.listForApi({ - start: 0, - count: CONFIG.FEEDS.VIDEOS.COUNT, - displayOnlyForFollower: { - actorId: server.id, - orLocalVideos: true - }, - hasFiles: true, - countVideos: false, - - ...options - }) - - return data -} - -export function getCommonVideoFeedAttributes (video: VideoModel) { - const localLink = WEBSERVER.URL + video.getWatchStaticPath() - - const thumbnailModels: MThumbnail[] = [] - if (video.hasPreview()) thumbnailModels.push(video.getPreview()) - thumbnailModels.push(video.getMiniature()) - - return { - title: video.name, - link: localLink, - description: mdToOneLinePlainText(video.getTruncatedDescription()), - content: toSafeHtml(video.description), - - date: video.publishedAt, - nsfw: video.nsfw, - - category: video.category - ? [ { name: getCategoryLabel(video.category) } ] - : undefined, - - thumbnails: thumbnailModels.map(t => ({ - url: WEBSERVER.URL + t.getLocalStaticPath(), - width: t.width, - height: t.height - })) - } -} diff --git a/server/controllers/feeds/video-feeds.ts b/server/controllers/feeds/video-feeds.ts deleted file mode 100644 index e5941be40..000000000 --- a/server/controllers/feeds/video-feeds.ts +++ /dev/null @@ -1,189 +0,0 @@ -import express from 'express' -import { extname } from 'path' -import { Feed } from '@peertube/feed' -import { cacheRouteFactory } from '@server/middlewares' -import { VideoModel } from '@server/models/video/video' -import { VideoInclude } from '@shared/models' -import { buildNSFWFilter } from '../../helpers/express-utils' -import { MIMETYPES, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' -import { - asyncMiddleware, - commonVideosFiltersValidator, - feedsFormatValidator, - setDefaultVideosSort, - setFeedFormatContentType, - feedsAccountOrChannelFiltersValidator, - videosSortValidator, - videoSubscriptionFeedsValidator -} from '../../middlewares' -import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed, sendFeed } from './shared' - -const videoFeedsRouter = express.Router() - -const { middleware: cacheRouteMiddleware } = cacheRouteFactory({ - headerBlacklist: [ 'Content-Type' ] -}) - -// --------------------------------------------------------------------------- - -videoFeedsRouter.get('/videos.:format', - videosSortValidator, - setDefaultVideosSort, - feedsFormatValidator, - setFeedFormatContentType, - cacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS), - commonVideosFiltersValidator, - asyncMiddleware(feedsAccountOrChannelFiltersValidator), - asyncMiddleware(generateVideoFeed) -) - -videoFeedsRouter.get('/subscriptions.:format', - videosSortValidator, - setDefaultVideosSort, - feedsFormatValidator, - setFeedFormatContentType, - cacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS), - commonVideosFiltersValidator, - asyncMiddleware(videoSubscriptionFeedsValidator), - asyncMiddleware(generateVideoFeedForSubscriptions) -) - -// --------------------------------------------------------------------------- - -export { - videoFeedsRouter -} - -// --------------------------------------------------------------------------- - -async function generateVideoFeed (req: express.Request, res: express.Response) { - const account = res.locals.account - const videoChannel = res.locals.videoChannel - - const { name, description, imageUrl, accountImageUrl, link, accountLink } = await buildFeedMetadata({ videoChannel, account }) - - const feed = initFeed({ - name, - description, - link, - isPodcast: false, - imageUrl, - author: { name, link: accountLink, imageUrl: accountImageUrl }, - resourceType: 'videos', - queryString: new URL(WEBSERVER.URL + req.url).search - }) - - const data = await getVideosForFeeds({ - sort: req.query.sort, - nsfw: buildNSFWFilter(res, req.query.nsfw), - isLocal: req.query.isLocal, - include: req.query.include | VideoInclude.FILES, - accountId: account?.id, - videoChannelId: videoChannel?.id - }) - - addVideosToFeed(feed, data) - - // Now the feed generation is done, let's send it! - return sendFeed(feed, req, res) -} - -async function generateVideoFeedForSubscriptions (req: express.Request, res: express.Response) { - const account = res.locals.account - const { name, description, imageUrl, link } = await buildFeedMetadata({ account }) - - const feed = initFeed({ - name, - description, - link, - isPodcast: false, - imageUrl, - resourceType: 'videos', - queryString: new URL(WEBSERVER.URL + req.url).search - }) - - const data = await getVideosForFeeds({ - sort: req.query.sort, - nsfw: buildNSFWFilter(res, req.query.nsfw), - isLocal: req.query.isLocal, - include: req.query.include | VideoInclude.FILES, - displayOnlyForFollower: { - actorId: res.locals.user.Account.Actor.id, - orLocalVideos: false - }, - user: res.locals.user - }) - - addVideosToFeed(feed, data) - - // Now the feed generation is done, let's send it! - return sendFeed(feed, req, res) -} - -// --------------------------------------------------------------------------- - -function addVideosToFeed (feed: Feed, videos: VideoModel[]) { - /** - * Adding video items to the feed object, one at a time - */ - for (const video of videos) { - const formattedVideoFiles = video.getFormattedAllVideoFilesJSON(false) - - const torrents = formattedVideoFiles.map(videoFile => ({ - title: video.name, - url: videoFile.torrentUrl, - size_in_bytes: videoFile.size - })) - - const videoFiles = formattedVideoFiles.map(videoFile => { - return { - type: MIMETYPES.VIDEO.EXT_MIMETYPE[extname(videoFile.fileUrl)], - medium: 'video', - height: videoFile.resolution.id, - fileSize: videoFile.size, - url: videoFile.fileUrl, - framerate: videoFile.fps, - duration: video.duration, - lang: video.language - } - }) - - feed.addItem({ - ...getCommonVideoFeedAttributes(video), - - id: WEBSERVER.URL + video.getWatchStaticPath(), - author: [ - { - name: video.VideoChannel.getDisplayName(), - link: video.VideoChannel.getClientUrl() - } - ], - torrents, - - // Enclosure - video: videoFiles.length !== 0 - ? { - url: videoFiles[0].url, - length: videoFiles[0].fileSize, - type: videoFiles[0].type - } - : undefined, - - // Media RSS - videos: videoFiles, - - embed: { - url: WEBSERVER.URL + video.getEmbedStaticPath(), - allowFullscreen: true - }, - player: { - url: WEBSERVER.URL + video.getWatchStaticPath() - }, - community: { - statistics: { - views: video.views - } - } - }) - } -} diff --git a/server/controllers/feeds/video-podcast-feeds.ts b/server/controllers/feeds/video-podcast-feeds.ts deleted file mode 100644 index fca82ba68..000000000 --- a/server/controllers/feeds/video-podcast-feeds.ts +++ /dev/null @@ -1,313 +0,0 @@ -import express from 'express' -import { extname } from 'path' -import { Feed } from '@peertube/feed' -import { CustomTag, CustomXMLNS, LiveItemStatus } from '@peertube/feed/lib/typings' -import { getBiggestActorImage } from '@server/lib/actor-image' -import { InternalEventEmitter } from '@server/lib/internal-event-emitter' -import { Hooks } from '@server/lib/plugins/hooks' -import { buildPodcastGroupsCache, cacheRouteFactory, videoFeedsPodcastSetCacheKey } from '@server/middlewares' -import { MVideo, MVideoCaptionVideo, MVideoFullLight } from '@server/types/models' -import { sortObjectComparator } from '@shared/core-utils' -import { ActorImageType, VideoFile, VideoInclude, VideoResolution, VideoState } from '@shared/models' -import { buildNSFWFilter } from '../../helpers/express-utils' -import { MIMETYPES, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' -import { asyncMiddleware, setFeedPodcastContentType, videoFeedsPodcastValidator } from '../../middlewares' -import { VideoModel } from '../../models/video/video' -import { VideoCaptionModel } from '../../models/video/video-caption' -import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared' - -const videoPodcastFeedsRouter = express.Router() - -// --------------------------------------------------------------------------- - -const { middleware: podcastCacheRouteMiddleware, instance: podcastApiCache } = cacheRouteFactory({ - headerBlacklist: [ 'Content-Type' ] -}) - -for (const event of ([ 'video-created', 'video-updated', 'video-deleted' ] as const)) { - InternalEventEmitter.Instance.on(event, ({ video }) => { - if (video.remote) return - - podcastApiCache.clearGroupSafe(buildPodcastGroupsCache({ channelId: video.channelId })) - }) -} - -for (const event of ([ 'channel-updated', 'channel-deleted' ] as const)) { - InternalEventEmitter.Instance.on(event, ({ channel }) => { - podcastApiCache.clearGroupSafe(buildPodcastGroupsCache({ channelId: channel.id })) - }) -} - -// --------------------------------------------------------------------------- - -videoPodcastFeedsRouter.get('/podcast/videos.xml', - setFeedPodcastContentType, - videoFeedsPodcastSetCacheKey, - podcastCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS), - asyncMiddleware(videoFeedsPodcastValidator), - asyncMiddleware(generateVideoPodcastFeed) -) - -// --------------------------------------------------------------------------- - -export { - videoPodcastFeedsRouter -} - -// --------------------------------------------------------------------------- - -async function generateVideoPodcastFeed (req: express.Request, res: express.Response) { - const videoChannel = res.locals.videoChannel - - const { name, userName, description, imageUrl, accountImageUrl, email, link, accountLink } = await buildFeedMetadata({ videoChannel }) - - const data = await getVideosForFeeds({ - sort: '-publishedAt', - nsfw: buildNSFWFilter(), - // Prevent podcast feeds from listing videos in other instances - // helps prevent duplicates when they are indexed -- only the author should control them - isLocal: true, - include: VideoInclude.FILES, - videoChannelId: videoChannel?.id - }) - - const customTags: CustomTag[] = await Hooks.wrapObject( - [], - 'filter:feed.podcast.channel.create-custom-tags.result', - { videoChannel } - ) - - const customXMLNS: CustomXMLNS[] = await Hooks.wrapObject( - [], - 'filter:feed.podcast.rss.create-custom-xmlns.result' - ) - - const feed = initFeed({ - name, - description, - link, - isPodcast: true, - imageUrl, - - locked: email - ? { isLocked: true, email } // Default to true because we have no way of offering a redirect yet - : undefined, - - person: [ { name: userName, href: accountLink, img: accountImageUrl } ], - resourceType: 'videos', - queryString: new URL(WEBSERVER.URL + req.url).search, - medium: 'video', - customXMLNS, - customTags - }) - - await addVideosToPodcastFeed(feed, data) - - // Now the feed generation is done, let's send it! - return res.send(feed.podcast()).end() -} - -type PodcastMedia = - { - type: string - length: number - bitrate: number - sources: { uri: string, contentType?: string }[] - title: string - language?: string - } | - { - sources: { uri: string }[] - type: string - title: string - } - -async function generatePodcastItem (options: { - video: VideoModel - liveItem: boolean - media: PodcastMedia[] -}) { - const { video, liveItem, media } = options - - const customTags: CustomTag[] = await Hooks.wrapObject( - [], - 'filter:feed.podcast.video.create-custom-tags.result', - { video, liveItem } - ) - - const account = video.VideoChannel.Account - - const author = { - name: account.getDisplayName(), - href: account.getClientUrl() - } - - const commonAttributes = getCommonVideoFeedAttributes(video) - const guid = liveItem - ? `${video.uuid}_${video.publishedAt.toISOString()}` - : commonAttributes.link - - let personImage: string - - if (account.Actor.hasImage(ActorImageType.AVATAR)) { - const avatar = getBiggestActorImage(account.Actor.Avatars) - personImage = WEBSERVER.URL + avatar.getStaticPath() - } - - return { - guid, - ...commonAttributes, - - trackers: video.getTrackerUrls(), - - author: [ author ], - person: [ - { - ...author, - - img: personImage - } - ], - - media, - - socialInteract: [ - { - uri: video.url, - protocol: 'activitypub', - accountUrl: account.getClientUrl() - } - ], - - customTags - } -} - -async function addVideosToPodcastFeed (feed: Feed, videos: VideoModel[]) { - const captionsGroup = await VideoCaptionModel.listCaptionsOfMultipleVideos(videos.map(v => v.id)) - - for (const video of videos) { - if (!video.isLive) { - await addVODPodcastItem({ feed, video, captionsGroup }) - } else if (video.isLive && video.state !== VideoState.LIVE_ENDED) { - await addLivePodcastItem({ feed, video }) - } - } -} - -async function addVODPodcastItem (options: { - feed: Feed - video: VideoModel - captionsGroup: { [ id: number ]: MVideoCaptionVideo[] } -}) { - const { feed, video, captionsGroup } = options - - const webVideos = video.getFormattedWebVideoFilesJSON(true) - .map(f => buildVODWebVideoFile(video, f)) - .sort(sortObjectComparator('bitrate', 'desc')) - - const streamingPlaylistFiles = buildVODStreamingPlaylists(video) - - // Order matters here, the first media URI will be the "default" - // So web videos are default if enabled - const media = [ ...webVideos, ...streamingPlaylistFiles ] - - const videoCaptions = buildVODCaptions(video, captionsGroup[video.id]) - const item = await generatePodcastItem({ video, liveItem: false, media }) - - feed.addPodcastItem({ ...item, subTitle: videoCaptions }) -} - -async function addLivePodcastItem (options: { - feed: Feed - video: VideoModel -}) { - const { feed, video } = options - - let status: LiveItemStatus - - switch (video.state) { - case VideoState.WAITING_FOR_LIVE: - status = LiveItemStatus.pending - break - case VideoState.PUBLISHED: - status = LiveItemStatus.live - break - } - - const item = await generatePodcastItem({ video, liveItem: true, media: buildLiveStreamingPlaylists(video) }) - - feed.addPodcastLiveItem({ ...item, status, start: video.updatedAt.toISOString() }) -} - -// --------------------------------------------------------------------------- - -function buildVODWebVideoFile (video: MVideo, videoFile: VideoFile) { - const isAudio = videoFile.resolution.id === VideoResolution.H_NOVIDEO - const type = isAudio - ? MIMETYPES.AUDIO.EXT_MIMETYPE[extname(videoFile.fileUrl)] - : MIMETYPES.VIDEO.EXT_MIMETYPE[extname(videoFile.fileUrl)] - - const sources = [ - { uri: videoFile.fileUrl }, - { uri: videoFile.torrentUrl, contentType: 'application/x-bittorrent' } - ] - - if (videoFile.magnetUri) { - sources.push({ uri: videoFile.magnetUri }) - } - - return { - type, - title: videoFile.resolution.label, - length: videoFile.size, - bitrate: videoFile.size / video.duration * 8, - language: video.language, - sources - } -} - -function buildVODStreamingPlaylists (video: MVideoFullLight) { - const hls = video.getHLSPlaylist() - if (!hls) return [] - - return [ - { - type: 'application/x-mpegURL', - title: 'HLS', - sources: [ - { uri: hls.getMasterPlaylistUrl(video) } - ], - language: video.language - } - ] -} - -function buildLiveStreamingPlaylists (video: MVideoFullLight) { - const hls = video.getHLSPlaylist() - - return [ - { - type: 'application/x-mpegURL', - title: `HLS live stream`, - sources: [ - { uri: hls.getMasterPlaylistUrl(video) } - ], - language: video.language - } - ] -} - -function buildVODCaptions (video: MVideo, videoCaptions: MVideoCaptionVideo[]) { - return videoCaptions.map(caption => { - const type = MIMETYPES.VIDEO_CAPTIONS.EXT_MIMETYPE[extname(caption.filename)] - if (!type) return null - - return { - url: caption.getFileUrl(video), - language: caption.language, - type, - rel: 'captions' - } - }).filter(c => c) -} diff --git a/server/controllers/index.ts b/server/controllers/index.ts deleted file mode 100644 index 8a647aff1..000000000 --- a/server/controllers/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from './activitypub' -export * from './api' -export * from './sitemap' -export * from './client' -export * from './download' -export * from './feeds' -export * from './lazy-static' -export * from './misc' -export * from './object-storage-proxy' -export * from './plugins' -export * from './services' -export * from './static' -export * from './tracker' -export * from './well-known' diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts deleted file mode 100644 index dad30365c..000000000 --- a/server/controllers/lazy-static.ts +++ /dev/null @@ -1,128 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { CONFIG } from '@server/initializers/config' -import { HttpStatusCode } from '../../shared/models/http/http-error-codes' -import { FILES_CACHE, LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' -import { - AvatarPermanentFileCache, - VideoCaptionsSimpleFileCache, - VideoMiniaturePermanentFileCache, - VideoPreviewsSimpleFileCache, - VideoStoryboardsSimpleFileCache, - VideoTorrentsSimpleFileCache -} from '../lib/files-cache' -import { asyncMiddleware, handleStaticError } from '../middlewares' - -// --------------------------------------------------------------------------- -// Cache initializations -// --------------------------------------------------------------------------- - -VideoPreviewsSimpleFileCache.Instance.init(CONFIG.CACHE.PREVIEWS.SIZE, FILES_CACHE.PREVIEWS.MAX_AGE) -VideoCaptionsSimpleFileCache.Instance.init(CONFIG.CACHE.VIDEO_CAPTIONS.SIZE, FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE) -VideoTorrentsSimpleFileCache.Instance.init(CONFIG.CACHE.TORRENTS.SIZE, FILES_CACHE.TORRENTS.MAX_AGE) -VideoStoryboardsSimpleFileCache.Instance.init(CONFIG.CACHE.STORYBOARDS.SIZE, FILES_CACHE.STORYBOARDS.MAX_AGE) - -// --------------------------------------------------------------------------- - -const lazyStaticRouter = express.Router() - -lazyStaticRouter.use(cors()) - -lazyStaticRouter.use( - LAZY_STATIC_PATHS.AVATARS + ':filename', - asyncMiddleware(getActorImage), - handleStaticError -) - -lazyStaticRouter.use( - LAZY_STATIC_PATHS.BANNERS + ':filename', - asyncMiddleware(getActorImage), - handleStaticError -) - -lazyStaticRouter.use( - LAZY_STATIC_PATHS.THUMBNAILS + ':filename', - asyncMiddleware(getThumbnail), - handleStaticError -) - -lazyStaticRouter.use( - LAZY_STATIC_PATHS.PREVIEWS + ':filename', - asyncMiddleware(getPreview), - handleStaticError -) - -lazyStaticRouter.use( - LAZY_STATIC_PATHS.STORYBOARDS + ':filename', - asyncMiddleware(getStoryboard), - handleStaticError -) - -lazyStaticRouter.use( - LAZY_STATIC_PATHS.VIDEO_CAPTIONS + ':filename', - asyncMiddleware(getVideoCaption), - handleStaticError -) - -lazyStaticRouter.use( - LAZY_STATIC_PATHS.TORRENTS + ':filename', - asyncMiddleware(getTorrent), - handleStaticError -) - -// --------------------------------------------------------------------------- - -export { - lazyStaticRouter, - getPreview, - getVideoCaption -} - -// --------------------------------------------------------------------------- -const avatarPermanentFileCache = new AvatarPermanentFileCache() - -function getActorImage (req: express.Request, res: express.Response, next: express.NextFunction) { - const filename = req.params.filename - - return avatarPermanentFileCache.lazyServe({ filename, res, next }) -} - -// --------------------------------------------------------------------------- -const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache() - -function getThumbnail (req: express.Request, res: express.Response, next: express.NextFunction) { - const filename = req.params.filename - - return videoMiniaturePermanentFileCache.lazyServe({ filename, res, next }) -} - -// --------------------------------------------------------------------------- - -async function getPreview (req: express.Request, res: express.Response) { - const result = await VideoPreviewsSimpleFileCache.Instance.getFilePath(req.params.filename) - if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() - - return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) -} - -async function getStoryboard (req: express.Request, res: express.Response) { - const result = await VideoStoryboardsSimpleFileCache.Instance.getFilePath(req.params.filename) - if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() - - return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) -} - -async function getVideoCaption (req: express.Request, res: express.Response) { - const result = await VideoCaptionsSimpleFileCache.Instance.getFilePath(req.params.filename) - if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() - - return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) -} - -async function getTorrent (req: express.Request, res: express.Response) { - const result = await VideoTorrentsSimpleFileCache.Instance.getFilePath(req.params.filename) - if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end() - - // Torrents still use the old naming convention (video uuid + .torrent) - return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) -} diff --git a/server/controllers/misc.ts b/server/controllers/misc.ts deleted file mode 100644 index a7dfc7867..000000000 --- a/server/controllers/misc.ts +++ /dev/null @@ -1,210 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { CONFIG, isEmailEnabled } from '@server/initializers/config' -import { serveIndexHTML } from '@server/lib/client-html' -import { ServerConfigManager } from '@server/lib/server-config-manager' -import { HttpStatusCode } from '@shared/models' -import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model' -import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION, ROUTE_CACHE_LIFETIME } from '../initializers/constants' -import { getThemeOrDefault } from '../lib/plugins/theme-utils' -import { apiRateLimiter, asyncMiddleware } from '../middlewares' -import { cacheRoute } from '../middlewares/cache/cache' -import { UserModel } from '../models/user/user' -import { VideoModel } from '../models/video/video' -import { VideoCommentModel } from '../models/video/video-comment' - -const miscRouter = express.Router() - -miscRouter.use(cors()) - -miscRouter.use('/nodeinfo/:version.json', - apiRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO), - asyncMiddleware(generateNodeinfo) -) - -// robots.txt service -miscRouter.get('/robots.txt', - apiRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.ROBOTS), - (_, res: express.Response) => { - res.type('text/plain') - - return res.send(CONFIG.INSTANCE.ROBOTS) - } -) - -miscRouter.all('/teapot', - apiRateLimiter, - getCup, - asyncMiddleware(serveIndexHTML) -) - -// security.txt service -miscRouter.get('/security.txt', - apiRateLimiter, - (_, res: express.Response) => { - return res.redirect(HttpStatusCode.MOVED_PERMANENTLY_301, '/.well-known/security.txt') - } -) - -// --------------------------------------------------------------------------- - -export { - miscRouter -} - -// --------------------------------------------------------------------------- - -async function generateNodeinfo (req: express.Request, res: express.Response) { - const { totalVideos } = await VideoModel.getStats() - const { totalLocalVideoComments } = await VideoCommentModel.getStats() - const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats() - - if (!req.params.version || req.params.version !== '2.0') { - return res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'Nodeinfo schema version not handled' - }) - } - - const json = { - version: '2.0', - software: { - name: 'peertube', - version: PEERTUBE_VERSION - }, - protocols: [ - 'activitypub' - ], - services: { - inbound: [], - outbound: [ - 'atom1.0', - 'rss2.0' - ] - }, - openRegistrations: CONFIG.SIGNUP.ENABLED, - usage: { - users: { - total: totalUsers, - activeMonth: totalMonthlyActiveUsers, - activeHalfyear: totalHalfYearActiveUsers - }, - localPosts: totalVideos, - localComments: totalLocalVideoComments - }, - metadata: { - taxonomy: { - postsName: 'Videos' - }, - nodeName: CONFIG.INSTANCE.NAME, - nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, - nodeConfig: { - search: { - remoteUri: { - users: CONFIG.SEARCH.REMOTE_URI.USERS, - anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS - } - }, - plugin: { - registered: ServerConfigManager.Instance.getRegisteredPlugins() - }, - theme: { - registered: ServerConfigManager.Instance.getRegisteredThemes(), - default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) - }, - email: { - enabled: isEmailEnabled() - }, - contactForm: { - enabled: CONFIG.CONTACT_FORM.ENABLED - }, - transcoding: { - hls: { - enabled: CONFIG.TRANSCODING.HLS.ENABLED - }, - web_videos: { - enabled: CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED - }, - enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('vod') - }, - live: { - enabled: CONFIG.LIVE.ENABLED, - transcoding: { - enabled: CONFIG.LIVE.TRANSCODING.ENABLED, - enabledResolutions: ServerConfigManager.Instance.getEnabledResolutions('live') - } - }, - import: { - videos: { - http: { - enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED - }, - torrent: { - enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED - } - } - }, - autoBlacklist: { - videos: { - ofUsers: { - enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED - } - } - }, - avatar: { - file: { - size: { - max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max - }, - extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME - } - }, - video: { - image: { - extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME, - size: { - max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max - } - }, - file: { - extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME - } - }, - videoCaption: { - file: { - size: { - max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max - }, - extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME - } - }, - user: { - videoQuota: CONFIG.USER.VIDEO_QUOTA, - videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY - }, - trending: { - videos: { - intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS - } - }, - tracker: { - enabled: CONFIG.TRACKER.ENABLED - } - } - } - } as HttpNodeinfoDiasporaSoftwareNsSchema20 - - res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"') - .send(json) - .end() -} - -function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { - res.status(HttpStatusCode.I_AM_A_TEAPOT_418) - res.setHeader('Accept-Additions', 'Non-Dairy;1,Sugar;1') - res.setHeader('Safe', 'if-sepia-awake') - - return next() -} diff --git a/server/controllers/object-storage-proxy.ts b/server/controllers/object-storage-proxy.ts deleted file mode 100644 index d0c59bf93..000000000 --- a/server/controllers/object-storage-proxy.ts +++ /dev/null @@ -1,60 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' -import { proxifyHLS, proxifyWebVideoFile } from '@server/lib/object-storage' -import { - asyncMiddleware, - ensureCanAccessPrivateVideoHLSFiles, - ensureCanAccessVideoPrivateWebVideoFiles, - ensurePrivateObjectStorageProxyIsEnabled, - optionalAuthenticate -} from '@server/middlewares' -import { doReinjectVideoFileToken } from './shared/m3u8-playlist' - -const objectStorageProxyRouter = express.Router() - -objectStorageProxyRouter.use(cors()) - -objectStorageProxyRouter.get( - [ OBJECT_STORAGE_PROXY_PATHS.PRIVATE_WEB_VIDEOS + ':filename', OBJECT_STORAGE_PROXY_PATHS.LEGACY_PRIVATE_WEB_VIDEOS + ':filename' ], - ensurePrivateObjectStorageProxyIsEnabled, - optionalAuthenticate, - asyncMiddleware(ensureCanAccessVideoPrivateWebVideoFiles), - asyncMiddleware(proxifyWebVideoController) -) - -objectStorageProxyRouter.get(OBJECT_STORAGE_PROXY_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename', - ensurePrivateObjectStorageProxyIsEnabled, - optionalAuthenticate, - asyncMiddleware(ensureCanAccessPrivateVideoHLSFiles), - asyncMiddleware(proxifyHLSController) -) - -// --------------------------------------------------------------------------- - -export { - objectStorageProxyRouter -} - -function proxifyWebVideoController (req: express.Request, res: express.Response) { - const filename = req.params.filename - - return proxifyWebVideoFile({ req, res, filename }) -} - -function proxifyHLSController (req: express.Request, res: express.Response) { - const playlist = res.locals.videoStreamingPlaylist - const video = res.locals.onlyVideo - const filename = req.params.filename - - const reinjectVideoFileToken = filename.endsWith('.m3u8') && doReinjectVideoFileToken(req) - - return proxifyHLS({ - req, - res, - playlist, - video, - filename, - reinjectVideoFileToken - }) -} diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts deleted file mode 100644 index f0491b16a..000000000 --- a/server/controllers/plugins.ts +++ /dev/null @@ -1,175 +0,0 @@ -import express from 'express' -import { join } from 'path' -import { logger } from '@server/helpers/logger' -import { CONFIG } from '@server/initializers/config' -import { buildRateLimiter } from '@server/middlewares' -import { optionalAuthenticate } from '@server/middlewares/auth' -import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' -import { HttpStatusCode } from '../../shared/models/http/http-error-codes' -import { PluginType } from '../../shared/models/plugins/plugin.type' -import { isProdInstance } from '../helpers/core-utils' -import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' -import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' -import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins' -import { serveThemeCSSValidator } from '../middlewares/validators/themes' - -const sendFileOptions = { - maxAge: '30 days', - immutable: isProdInstance() -} - -const pluginsRouter = express.Router() - -const pluginsRateLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.PLUGINS.WINDOW_MS, - max: CONFIG.RATES_LIMIT.PLUGINS.MAX -}) - -pluginsRouter.get('/plugins/global.css', - pluginsRateLimiter, - servePluginGlobalCSS -) - -pluginsRouter.get('/plugins/translations/:locale.json', - pluginsRateLimiter, - getPluginTranslations -) - -pluginsRouter.get('/plugins/:pluginName/:pluginVersion/auth/:authName', - pluginsRateLimiter, - getPluginValidator(PluginType.PLUGIN), - getExternalAuthValidator, - handleAuthInPlugin -) - -pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', - pluginsRateLimiter, - getPluginValidator(PluginType.PLUGIN), - pluginStaticDirectoryValidator, - servePluginStaticDirectory -) - -pluginsRouter.get('/plugins/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', - pluginsRateLimiter, - getPluginValidator(PluginType.PLUGIN), - pluginStaticDirectoryValidator, - servePluginClientScripts -) - -pluginsRouter.use('/plugins/:pluginName/router', - pluginsRateLimiter, - getPluginValidator(PluginType.PLUGIN, false), - optionalAuthenticate, - servePluginCustomRoutes -) - -pluginsRouter.use('/plugins/:pluginName/:pluginVersion/router', - pluginsRateLimiter, - getPluginValidator(PluginType.PLUGIN), - optionalAuthenticate, - servePluginCustomRoutes -) - -pluginsRouter.get('/themes/:pluginName/:pluginVersion/static/:staticEndpoint(*)', - pluginsRateLimiter, - getPluginValidator(PluginType.THEME), - pluginStaticDirectoryValidator, - servePluginStaticDirectory -) - -pluginsRouter.get('/themes/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', - pluginsRateLimiter, - getPluginValidator(PluginType.THEME), - pluginStaticDirectoryValidator, - servePluginClientScripts -) - -pluginsRouter.get('/themes/:themeName/:themeVersion/css/:staticEndpoint(*)', - pluginsRateLimiter, - serveThemeCSSValidator, - serveThemeCSSDirectory -) - -// --------------------------------------------------------------------------- - -export { - pluginsRouter -} - -// --------------------------------------------------------------------------- - -function servePluginGlobalCSS (req: express.Request, res: express.Response) { - // Only cache requests that have a ?hash=... query param - const globalCSSOptions = req.query.hash - ? sendFileOptions - : {} - - return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions) -} - -function getPluginTranslations (req: express.Request, res: express.Response) { - const locale = req.params.locale - - if (is18nLocale(locale)) { - const completeLocale = getCompleteLocale(locale) - const json = PluginManager.Instance.getTranslations(completeLocale) - - return res.json(json) - } - - return res.status(HttpStatusCode.NOT_FOUND_404).end() -} - -function servePluginStaticDirectory (req: express.Request, res: express.Response) { - const plugin: RegisteredPlugin = res.locals.registeredPlugin - const staticEndpoint = req.params.staticEndpoint - - const [ directory, ...file ] = staticEndpoint.split('/') - - const staticPath = plugin.staticDirs[directory] - if (!staticPath) return res.status(HttpStatusCode.NOT_FOUND_404).end() - - const filepath = file.join('/') - return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions) -} - -function servePluginCustomRoutes (req: express.Request, res: express.Response, next: express.NextFunction) { - const plugin: RegisteredPlugin = res.locals.registeredPlugin - const router = PluginManager.Instance.getRouter(plugin.npmName) - - if (!router) return res.status(HttpStatusCode.NOT_FOUND_404).end() - - return router(req, res, next) -} - -function servePluginClientScripts (req: express.Request, res: express.Response) { - const plugin: RegisteredPlugin = res.locals.registeredPlugin - const staticEndpoint = req.params.staticEndpoint - - const file = plugin.clientScripts[staticEndpoint] - if (!file) return res.status(HttpStatusCode.NOT_FOUND_404).end() - - return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) -} - -function serveThemeCSSDirectory (req: express.Request, res: express.Response) { - const plugin: RegisteredPlugin = res.locals.registeredPlugin - const staticEndpoint = req.params.staticEndpoint - - if (plugin.css.includes(staticEndpoint) === false) { - return res.status(HttpStatusCode.NOT_FOUND_404).end() - } - - return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) -} - -function handleAuthInPlugin (req: express.Request, res: express.Response) { - const authOptions = res.locals.externalAuth - - try { - logger.debug('Forwarding auth plugin request in %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName) - authOptions.onAuthRequest(req, res) - } catch (err) { - logger.error('Forward request error in auth %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName, { err }) - } -} diff --git a/server/controllers/services.ts b/server/controllers/services.ts deleted file mode 100644 index 0fd63a30f..000000000 --- a/server/controllers/services.ts +++ /dev/null @@ -1,165 +0,0 @@ -import express from 'express' -import { MChannelSummary } from '@server/types/models' -import { escapeHTML } from '@shared/core-utils/renderer' -import { EMBED_SIZE, PREVIEWS_SIZE, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' -import { apiRateLimiter, asyncMiddleware, oembedValidator } from '../middlewares' -import { accountNameWithHostGetValidator } from '../middlewares/validators' -import { forceNumber } from '@shared/core-utils' - -const servicesRouter = express.Router() - -servicesRouter.use('/oembed', - apiRateLimiter, - asyncMiddleware(oembedValidator), - generateOEmbed -) -servicesRouter.use('/redirect/accounts/:accountName', - apiRateLimiter, - asyncMiddleware(accountNameWithHostGetValidator), - redirectToAccountUrl -) - -// --------------------------------------------------------------------------- - -export { - servicesRouter -} - -// --------------------------------------------------------------------------- - -function generateOEmbed (req: express.Request, res: express.Response) { - if (res.locals.videoAll) return generateVideoOEmbed(req, res) - - return generatePlaylistOEmbed(req, res) -} - -function generatePlaylistOEmbed (req: express.Request, res: express.Response) { - const playlist = res.locals.videoPlaylistSummary - - const json = buildOEmbed({ - channel: playlist.VideoChannel, - title: playlist.name, - embedPath: playlist.getEmbedStaticPath() + buildPlayerURLQuery(req.query.url), - previewPath: playlist.getThumbnailStaticPath(), - previewSize: THUMBNAILS_SIZE, - req - }) - - return res.json(json) -} - -function generateVideoOEmbed (req: express.Request, res: express.Response) { - const video = res.locals.videoAll - - const json = buildOEmbed({ - channel: video.VideoChannel, - title: video.name, - embedPath: video.getEmbedStaticPath() + buildPlayerURLQuery(req.query.url), - previewPath: video.getPreviewStaticPath(), - previewSize: PREVIEWS_SIZE, - req - }) - - return res.json(json) -} - -function buildPlayerURLQuery (inputQueryUrl: string) { - const allowedParameters = new Set([ - 'start', - 'stop', - 'loop', - 'autoplay', - 'muted', - 'controls', - 'controlBar', - 'title', - 'api', - 'warningTitle', - 'peertubeLink', - 'p2p', - 'subtitle', - 'bigPlayBackgroundColor', - 'mode', - 'foregroundColor' - ]) - - const params = new URLSearchParams() - - new URL(inputQueryUrl).searchParams.forEach((v, k) => { - if (allowedParameters.has(k)) { - params.append(k, v) - } - }) - - const stringQuery = params.toString() - if (!stringQuery) return '' - - return '?' + stringQuery -} - -function buildOEmbed (options: { - req: express.Request - title: string - channel: MChannelSummary - previewPath: string | null - embedPath: string - previewSize: { - height: number - width: number - } -}) { - const { req, previewSize, previewPath, title, channel, embedPath } = options - - const webserverUrl = WEBSERVER.URL - const maxHeight = forceNumber(req.query.maxheight) - const maxWidth = forceNumber(req.query.maxwidth) - - const embedUrl = webserverUrl + embedPath - const embedTitle = escapeHTML(title) - - let thumbnailUrl = previewPath - ? webserverUrl + previewPath - : undefined - - let embedWidth = EMBED_SIZE.width - if (maxWidth < embedWidth) embedWidth = maxWidth - - let embedHeight = EMBED_SIZE.height - if (maxHeight < embedHeight) embedHeight = maxHeight - - // Our thumbnail is too big for the consumer - if ( - (maxHeight !== undefined && maxHeight < previewSize.height) || - (maxWidth !== undefined && maxWidth < previewSize.width) - ) { - thumbnailUrl = undefined - } - - const html = `` - - const json: any = { - type: 'video', - version: '1.0', - html, - width: embedWidth, - height: embedHeight, - title, - author_name: channel.name, - author_url: channel.Actor.url, - provider_name: 'PeerTube', - provider_url: webserverUrl - } - - if (thumbnailUrl !== undefined) { - json.thumbnail_url = thumbnailUrl - json.thumbnail_width = previewSize.width - json.thumbnail_height = previewSize.height - } - - return json -} - -function redirectToAccountUrl (req: express.Request, res: express.Response, next: express.NextFunction) { - return res.redirect(res.locals.account.Actor.url) -} diff --git a/server/controllers/shared/m3u8-playlist.ts b/server/controllers/shared/m3u8-playlist.ts deleted file mode 100644 index cea5eb5d2..000000000 --- a/server/controllers/shared/m3u8-playlist.ts +++ /dev/null @@ -1,18 +0,0 @@ -import express from 'express' - -function doReinjectVideoFileToken (req: express.Request) { - return req.query.videoFileToken && req.query.reinjectVideoFileToken -} - -function buildReinjectVideoFileTokenQuery (req: express.Request, isMaster: boolean) { - const query = 'videoFileToken=' + req.query.videoFileToken - if (isMaster) { - return query + '&reinjectVideoFileToken=true' - } - return query -} - -export { - doReinjectVideoFileToken, - buildReinjectVideoFileTokenQuery -} diff --git a/server/controllers/sitemap.ts b/server/controllers/sitemap.ts deleted file mode 100644 index 07f4c554e..000000000 --- a/server/controllers/sitemap.ts +++ /dev/null @@ -1,115 +0,0 @@ -import express from 'express' -import { truncate } from 'lodash' -import { ErrorLevel, SitemapStream, streamToPromise } from 'sitemap' -import { logger } from '@server/helpers/logger' -import { getServerActor } from '@server/models/application/application' -import { buildNSFWFilter } from '../helpers/express-utils' -import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' -import { apiRateLimiter, asyncMiddleware } from '../middlewares' -import { cacheRoute } from '../middlewares/cache/cache' -import { AccountModel } from '../models/account/account' -import { VideoModel } from '../models/video/video' -import { VideoChannelModel } from '../models/video/video-channel' - -const sitemapRouter = express.Router() - -sitemapRouter.use('/sitemap.xml', - apiRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP), - asyncMiddleware(getSitemap) -) - -// --------------------------------------------------------------------------- - -export { - sitemapRouter -} - -// --------------------------------------------------------------------------- - -async function getSitemap (req: express.Request, res: express.Response) { - let urls = getSitemapBasicUrls() - - urls = urls.concat(await getSitemapLocalVideoUrls()) - urls = urls.concat(await getSitemapVideoChannelUrls()) - urls = urls.concat(await getSitemapAccountUrls()) - - const sitemapStream = new SitemapStream({ - hostname: WEBSERVER.URL, - errorHandler: (err: Error, level: ErrorLevel) => { - if (level === 'warn') { - logger.warn('Warning in sitemap generation.', { err }) - } else if (level === 'throw') { - logger.error('Error in sitemap generation.', { err }) - - throw err - } - } - }) - - for (const urlObj of urls) { - sitemapStream.write(urlObj) - } - sitemapStream.end() - - const xml = await streamToPromise(sitemapStream) - - res.header('Content-Type', 'application/xml') - res.send(xml) -} - -async function getSitemapVideoChannelUrls () { - const rows = await VideoChannelModel.listLocalsForSitemap('createdAt') - - return rows.map(channel => ({ - url: WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername - })) -} - -async function getSitemapAccountUrls () { - const rows = await AccountModel.listLocalsForSitemap('createdAt') - - return rows.map(channel => ({ - url: WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername - })) -} - -async function getSitemapLocalVideoUrls () { - const serverActor = await getServerActor() - - const { data } = await VideoModel.listForApi({ - start: 0, - count: undefined, - sort: 'createdAt', - displayOnlyForFollower: { - actorId: serverActor.id, - orLocalVideos: true - }, - isLocal: true, - nsfw: buildNSFWFilter(), - countVideos: false - }) - - return data.map(v => ({ - url: WEBSERVER.URL + v.getWatchStaticPath(), - video: [ - { - // Sitemap title should be < 100 characters - title: truncate(v.name, { length: 100, omission: '...' }), - // Sitemap description should be < 2000 characters - description: truncate(v.description || v.name, { length: 2000, omission: '...' }), - player_loc: WEBSERVER.URL + v.getEmbedStaticPath(), - thumbnail_loc: WEBSERVER.URL + v.getMiniatureStaticPath() - } - ] - })) -} - -function getSitemapBasicUrls () { - const paths = [ - '/about/instance', - '/videos/local' - ] - - return paths.map(p => ({ url: WEBSERVER.URL + p })) -} diff --git a/server/controllers/static.ts b/server/controllers/static.ts deleted file mode 100644 index 97caa8292..000000000 --- a/server/controllers/static.ts +++ /dev/null @@ -1,116 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { readFile } from 'fs-extra' -import { join } from 'path' -import { injectQueryToPlaylistUrls } from '@server/lib/hls' -import { - asyncMiddleware, - ensureCanAccessPrivateVideoHLSFiles, - ensureCanAccessVideoPrivateWebVideoFiles, - handleStaticError, - optionalAuthenticate -} from '@server/middlewares' -import { HttpStatusCode } from '@shared/models' -import { CONFIG } from '../initializers/config' -import { DIRECTORIES, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers/constants' -import { buildReinjectVideoFileTokenQuery, doReinjectVideoFileToken } from './shared/m3u8-playlist' - -const staticRouter = express.Router() - -// Cors is very important to let other servers access torrent and video files -staticRouter.use(cors()) - -// --------------------------------------------------------------------------- -// Web videos/Classic videos -// --------------------------------------------------------------------------- - -const privateWebVideoStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AUTH === true - ? [ optionalAuthenticate, asyncMiddleware(ensureCanAccessVideoPrivateWebVideoFiles) ] - : [] - -staticRouter.use( - [ STATIC_PATHS.PRIVATE_WEB_VIDEOS, STATIC_PATHS.LEGACY_PRIVATE_WEB_VIDEOS ], - ...privateWebVideoStaticMiddlewares, - express.static(DIRECTORIES.VIDEOS.PRIVATE, { fallthrough: false }), - handleStaticError -) -staticRouter.use( - [ STATIC_PATHS.WEB_VIDEOS, STATIC_PATHS.LEGACY_WEB_VIDEOS ], - express.static(DIRECTORIES.VIDEOS.PUBLIC, { fallthrough: false }), - handleStaticError -) - -staticRouter.use( - STATIC_PATHS.REDUNDANCY, - express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }), - handleStaticError -) - -// --------------------------------------------------------------------------- -// HLS -// --------------------------------------------------------------------------- - -const privateHLSStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AUTH === true - ? [ optionalAuthenticate, asyncMiddleware(ensureCanAccessPrivateVideoHLSFiles) ] - : [] - -staticRouter.use( - STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistName.m3u8', - ...privateHLSStaticMiddlewares, - asyncMiddleware(servePrivateM3U8) -) - -staticRouter.use( - STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, - ...privateHLSStaticMiddlewares, - express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, { fallthrough: false }), - handleStaticError -) -staticRouter.use( - STATIC_PATHS.STREAMING_PLAYLISTS.HLS, - express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, { fallthrough: false }), - handleStaticError -) - -// FIXME: deprecated in v6, to remove -const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR -staticRouter.use( - STATIC_PATHS.THUMBNAILS, - express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE.SERVER, fallthrough: false }), - handleStaticError -) - -// --------------------------------------------------------------------------- - -export { - staticRouter -} - -// --------------------------------------------------------------------------- - -async function servePrivateM3U8 (req: express.Request, res: express.Response) { - const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.playlistName + '.m3u8') - const filename = req.params.playlistName + '.m3u8' - - let playlistContent: string - - try { - playlistContent = await readFile(path, 'utf-8') - } catch (err) { - if (err.message.includes('ENOENT')) { - return res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'File not found' - }) - } - - throw err - } - - // Inject token in playlist so players that cannot alter the HTTP request can still watch the video - const transformedContent = doReinjectVideoFileToken(req) - ? injectQueryToPlaylistUrls(playlistContent, buildReinjectVideoFileTokenQuery(req, filename.endsWith('master.m3u8'))) - : playlistContent - - return res.set('content-type', 'application/vnd.apple.mpegurl').send(transformedContent).end() -} diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts deleted file mode 100644 index 9a8aa88bc..000000000 --- a/server/controllers/tracker.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Server as TrackerServer } from 'bittorrent-tracker' -import express from 'express' -import { createServer } from 'http' -import { LRUCache } from 'lru-cache' -import proxyAddr from 'proxy-addr' -import { WebSocketServer } from 'ws' -import { logger } from '../helpers/logger' -import { CONFIG } from '../initializers/config' -import { LRU_CACHE, TRACKER_RATE_LIMITS } from '../initializers/constants' -import { VideoFileModel } from '../models/video/video-file' -import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' - -const trackerRouter = express.Router() - -const blockedIPs = new LRUCache({ - max: LRU_CACHE.TRACKER_IPS.MAX_SIZE, - ttl: TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME -}) - -let peersIps = {} -let peersIpInfoHash = {} -runPeersChecker() - -const trackerServer = new TrackerServer({ - http: false, - udp: false, - ws: false, - filter: async function (infoHash, params, cb) { - if (CONFIG.TRACKER.ENABLED === false) { - return cb(new Error('Tracker is disabled on this instance.')) - } - - let ip: string - - if (params.type === 'ws') { - ip = params.ip - } else { - ip = params.httpReq.ip - } - - const key = ip + '-' + infoHash - - peersIps[ip] = peersIps[ip] ? peersIps[ip] + 1 : 1 - peersIpInfoHash[key] = peersIpInfoHash[key] ? peersIpInfoHash[key] + 1 : 1 - - if (CONFIG.TRACKER.REJECT_TOO_MANY_ANNOUNCES && peersIpInfoHash[key] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { - return cb(new Error(`Too many requests (${peersIpInfoHash[key]} of ip ${ip} for torrent ${infoHash}`)) - } - - try { - if (CONFIG.TRACKER.PRIVATE === false) return cb() - - const videoFileExists = await VideoFileModel.doesInfohashExistCached(infoHash) - if (videoFileExists === true) return cb() - - const playlistExists = await VideoStreamingPlaylistModel.doesInfohashExistCached(infoHash) - if (playlistExists === true) return cb() - - cb(new Error(`Unknown infoHash ${infoHash} requested by ip ${ip}`)) - - // Close socket connection and block IP for a few time - if (params.type === 'ws') { - blockedIPs.set(ip, true) - - // setTimeout to wait filter response - setTimeout(() => params.socket.close(), 0) - } - } catch (err) { - logger.error('Error in tracker filter.', { err }) - return cb(err) - } - } -}) - -if (CONFIG.TRACKER.ENABLED !== false) { - trackerServer.on('error', function (err) { - logger.error('Error in tracker.', { err }) - }) - - trackerServer.on('warning', function (err) { - const message = err.message || '' - - if (CONFIG.LOG.LOG_TRACKER_UNKNOWN_INFOHASH === false && message.includes('Unknown infoHash')) { - return - } - - logger.warn('Warning in tracker.', { err }) - }) -} - -const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer) -trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) -trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) - -function createWebsocketTrackerServer (app: express.Application) { - const server = createServer(app) - const wss = new WebSocketServer({ noServer: true }) - - wss.on('connection', function (ws, req) { - ws['ip'] = proxyAddr(req, CONFIG.TRUST_PROXY) - - trackerServer.onWebSocketConnection(ws) - }) - - server.on('upgrade', (request: express.Request, socket, head) => { - if (request.url === '/tracker/socket') { - const ip = proxyAddr(request, CONFIG.TRUST_PROXY) - - if (blockedIPs.has(ip)) { - logger.debug('Blocking IP %s from tracker.', ip) - - socket.write('HTTP/1.1 403 Forbidden\r\n\r\n') - socket.destroy() - return - } - - return wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request)) - } - - // Don't destroy socket, we have Socket.IO too - }) - - return { server, trackerServer } -} - -// --------------------------------------------------------------------------- - -export { - trackerRouter, - createWebsocketTrackerServer -} - -// --------------------------------------------------------------------------- - -function runPeersChecker () { - setInterval(() => { - logger.debug('Checking peers.') - - for (const ip of Object.keys(peersIpInfoHash)) { - if (peersIps[ip] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP) { - logger.warn('Peer %s made abnormal requests (%d).', ip, peersIps[ip]) - } - } - - peersIpInfoHash = {} - peersIps = {} - }, TRACKER_RATE_LIMITS.INTERVAL) -} diff --git a/server/controllers/well-known.ts b/server/controllers/well-known.ts deleted file mode 100644 index 322cf6ea2..000000000 --- a/server/controllers/well-known.ts +++ /dev/null @@ -1,125 +0,0 @@ -import cors from 'cors' -import express from 'express' -import { join } from 'path' -import { asyncMiddleware, buildRateLimiter, handleStaticError, webfingerValidator } from '@server/middlewares' -import { root } from '@shared/core-utils' -import { CONFIG } from '../initializers/config' -import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' -import { cacheRoute } from '../middlewares/cache/cache' - -const wellKnownRouter = express.Router() - -const wellKnownRateLimiter = buildRateLimiter({ - windowMs: CONFIG.RATES_LIMIT.WELL_KNOWN.WINDOW_MS, - max: CONFIG.RATES_LIMIT.WELL_KNOWN.MAX -}) - -wellKnownRouter.use(cors()) - -wellKnownRouter.get('/.well-known/webfinger', - wellKnownRateLimiter, - asyncMiddleware(webfingerValidator), - webfingerController -) - -wellKnownRouter.get('/.well-known/security.txt', - wellKnownRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.SECURITYTXT), - (_, res: express.Response) => { - res.type('text/plain') - return res.send(CONFIG.INSTANCE.SECURITYTXT + CONFIG.INSTANCE.SECURITYTXT_CONTACT) - } -) - -// nodeinfo service -wellKnownRouter.use('/.well-known/nodeinfo', - wellKnownRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.NODEINFO), - (_, res: express.Response) => { - return res.json({ - links: [ - { - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: WEBSERVER.URL + '/nodeinfo/2.0.json' - } - ] - }) - } -) - -// dnt-policy.txt service (see https://www.eff.org/dnt-policy) -wellKnownRouter.use('/.well-known/dnt-policy.txt', - wellKnownRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.DNT_POLICY), - (_, res: express.Response) => { - res.type('text/plain') - - return res.sendFile(join(root(), 'dist/server/static/dnt-policy/dnt-policy-1.0.txt')) - } -) - -// dnt service (see https://www.w3.org/TR/tracking-dnt/#status-resource) -wellKnownRouter.use('/.well-known/dnt/', - wellKnownRateLimiter, - (_, res: express.Response) => { - res.json({ tracking: 'N' }) - } -) - -wellKnownRouter.use('/.well-known/change-password', - wellKnownRateLimiter, - (_, res: express.Response) => { - res.redirect('/my-account/settings') - } -) - -wellKnownRouter.use('/.well-known/host-meta', - wellKnownRateLimiter, - (_, res: express.Response) => { - res.type('application/xml') - - const xml = '\n' + - '\n' + - ` \n` + - '' - - res.send(xml).end() - } -) - -wellKnownRouter.use('/.well-known/', - wellKnownRateLimiter, - cacheRoute(ROUTE_CACHE_LIFETIME.WELL_KNOWN), - express.static(CONFIG.STORAGE.WELL_KNOWN_DIR, { fallthrough: false }), - handleStaticError -) - -// --------------------------------------------------------------------------- - -export { - wellKnownRouter -} - -// --------------------------------------------------------------------------- - -function webfingerController (req: express.Request, res: express.Response) { - const actor = res.locals.actorUrl - - const json = { - subject: req.query.resource, - aliases: [ actor.url ], - links: [ - { - rel: 'self', - type: 'application/activity+json', - href: actor.url - }, - { - rel: 'http://ostatus.org/schema/1.0/subscribe', - template: WEBSERVER.URL + '/remote-interaction?uri={uri}' - } - ] - } - - return res.json(json) -} -- cgit v1.2.3