diff options
-rw-r--r-- | config/default.yaml | 2 | ||||
-rw-r--r-- | server/controllers/api/videos/live.ts | 3 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 2 | ||||
-rw-r--r-- | server/lib/moderation.ts | 11 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-live.ts | 30 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test/main.js | 25 | ||||
-rw-r--r-- | server/tests/plugins/action-hooks.ts | 43 | ||||
-rw-r--r-- | server/tests/plugins/filter-hooks.ts | 23 | ||||
-rw-r--r-- | shared/models/plugins/server-hook.model.ts | 6 |
9 files changed, 123 insertions, 22 deletions
diff --git a/config/default.yaml b/config/default.yaml index af16f081f..120f03d5d 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -262,7 +262,7 @@ live: | |||
262 | # PeerTube will transcode segments in a video file | 262 | # PeerTube will transcode segments in a video file |
263 | # If the user daily/total quota is reached, PeerTube will stop the live | 263 | # If the user daily/total quota is reached, PeerTube will stop the live |
264 | # /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay | 264 | # /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay |
265 | allow_replay: true | 265 | allow_replay: false |
266 | 266 | ||
267 | rtmp: | 267 | rtmp: |
268 | port: 1935 | 268 | port: 1935 |
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index f980c7730..d438b6f3a 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts | |||
@@ -5,6 +5,7 @@ import { CONFIG } from '@server/initializers/config' | |||
5 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' | 5 | import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' |
6 | import { getVideoActivityPubUrl } from '@server/lib/activitypub/url' | 6 | import { getVideoActivityPubUrl } from '@server/lib/activitypub/url' |
7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
8 | import { Hooks } from '@server/lib/plugins/hooks' | ||
8 | import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' | 9 | import { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' |
9 | import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live' | 10 | import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator } from '@server/middlewares/validators/videos/video-live' |
10 | import { VideoLiveModel } from '@server/models/video/video-live' | 11 | import { VideoLiveModel } from '@server/models/video/video-live' |
@@ -128,6 +129,8 @@ async function addLiveVideo (req: express.Request, res: express.Response) { | |||
128 | return { videoCreated } | 129 | return { videoCreated } |
129 | }) | 130 | }) |
130 | 131 | ||
132 | Hooks.runAction('action:api.live-video.created', { video: videoCreated }) | ||
133 | |||
131 | return res.json({ | 134 | return res.json({ |
132 | video: { | 135 | video: { |
133 | id: videoCreated.id, | 136 | id: videoCreated.id, |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index 9a2914cc5..ef9377e43 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -118,7 +118,7 @@ class LiveManager { | |||
118 | } | 118 | } |
119 | 119 | ||
120 | run () { | 120 | run () { |
121 | logger.info('Running RTMP server.') | 121 | logger.info('Running RTMP server on port %d', config.rtmp.port) |
122 | 122 | ||
123 | this.rtmpServer = new NodeRtmpServer(config) | 123 | this.rtmpServer = new NodeRtmpServer(config) |
124 | this.rtmpServer.run() | 124 | this.rtmpServer.run() |
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index 0ef26d53d..0ace2d021 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts | |||
@@ -20,7 +20,7 @@ import { | |||
20 | import { ActivityCreate } from '../../shared/models/activitypub' | 20 | import { ActivityCreate } from '../../shared/models/activitypub' |
21 | import { VideoObject } from '../../shared/models/activitypub/objects' | 21 | import { VideoObject } from '../../shared/models/activitypub/objects' |
22 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' | 22 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' |
23 | import { VideoCreate, VideoImportCreate } from '../../shared/models/videos' | 23 | import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' |
24 | import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' | 24 | import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model' |
25 | import { UserModel } from '../models/account/user' | 25 | import { UserModel } from '../models/account/user' |
26 | import { ActorModel } from '../models/activitypub/actor' | 26 | import { ActorModel } from '../models/activitypub/actor' |
@@ -43,6 +43,13 @@ function isLocalVideoAccepted (object: { | |||
43 | return { accepted: true } | 43 | return { accepted: true } |
44 | } | 44 | } |
45 | 45 | ||
46 | function isLocalLiveVideoAccepted (object: { | ||
47 | liveVideoBody: LiveVideoCreate | ||
48 | user: UserModel | ||
49 | }): AcceptResult { | ||
50 | return { accepted: true } | ||
51 | } | ||
52 | |||
46 | function isLocalVideoThreadAccepted (_object: { | 53 | function isLocalVideoThreadAccepted (_object: { |
47 | commentBody: VideoCommentCreate | 54 | commentBody: VideoCommentCreate |
48 | video: VideoModel | 55 | video: VideoModel |
@@ -175,6 +182,8 @@ function createAccountAbuse (options: { | |||
175 | } | 182 | } |
176 | 183 | ||
177 | export { | 184 | export { |
185 | isLocalLiveVideoAccepted, | ||
186 | |||
178 | isLocalVideoAccepted, | 187 | isLocalVideoAccepted, |
179 | isLocalVideoThreadAccepted, | 188 | isLocalVideoThreadAccepted, |
180 | isRemoteVideoAccepted, | 189 | isRemoteVideoAccepted, |
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index cbc48fe93..ff92db910 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts | |||
@@ -11,6 +11,8 @@ import { CONFIG } from '../../../initializers/config' | |||
11 | import { areValidationErrors } from '../utils' | 11 | import { areValidationErrors } from '../utils' |
12 | import { getCommonVideoEditAttributes } from './videos' | 12 | import { getCommonVideoEditAttributes } from './videos' |
13 | import { VideoModel } from '@server/models/video/video' | 13 | import { VideoModel } from '@server/models/video/video' |
14 | import { Hooks } from '@server/lib/plugins/hooks' | ||
15 | import { isLocalLiveVideoAccepted } from '@server/lib/moderation' | ||
14 | 16 | ||
15 | const videoLiveGetValidator = [ | 17 | const videoLiveGetValidator = [ |
16 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | 18 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), |
@@ -97,6 +99,8 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
97 | } | 99 | } |
98 | } | 100 | } |
99 | 101 | ||
102 | if (!await isLiveVideoAccepted(req, res)) return cleanUpReqFiles(req) | ||
103 | |||
100 | return next() | 104 | return next() |
101 | } | 105 | } |
102 | ]) | 106 | ]) |
@@ -137,3 +141,29 @@ export { | |||
137 | videoLiveUpdateValidator, | 141 | videoLiveUpdateValidator, |
138 | videoLiveGetValidator | 142 | videoLiveGetValidator |
139 | } | 143 | } |
144 | |||
145 | // --------------------------------------------------------------------------- | ||
146 | |||
147 | async function isLiveVideoAccepted (req: express.Request, res: express.Response) { | ||
148 | // Check we accept this video | ||
149 | const acceptParameters = { | ||
150 | liveVideoBody: req.body, | ||
151 | user: res.locals.oauth.token.User | ||
152 | } | ||
153 | const acceptedResult = await Hooks.wrapFun( | ||
154 | isLocalLiveVideoAccepted, | ||
155 | acceptParameters, | ||
156 | 'filter:api.live-video.create.accept.result' | ||
157 | ) | ||
158 | |||
159 | if (!acceptedResult || acceptedResult.accepted !== true) { | ||
160 | logger.info('Refused local live video.', { acceptedResult, acceptParameters }) | ||
161 | |||
162 | res.status(403) | ||
163 | .json({ error: acceptedResult.errorMessage || 'Refused local live video' }) | ||
164 | |||
165 | return false | ||
166 | } | ||
167 | |||
168 | return true | ||
169 | } | ||
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index a45e98fb5..322c0610c 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -7,6 +7,8 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
7 | 'action:api.video.uploaded', | 7 | 'action:api.video.uploaded', |
8 | 'action:api.video.viewed', | 8 | 'action:api.video.viewed', |
9 | 9 | ||
10 | 'action:api.live-video.created', | ||
11 | |||
10 | 'action:api.video-thread.created', | 12 | 'action:api.video-thread.created', |
11 | 'action:api.video-comment-reply.created', | 13 | 'action:api.video-comment-reply.created', |
12 | 'action:api.video-comment.deleted', | 14 | 'action:api.video-comment.deleted', |
@@ -46,15 +48,22 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
46 | } | 48 | } |
47 | }) | 49 | }) |
48 | 50 | ||
49 | registerHook({ | 51 | for (const hook of [ 'filter:api.video.upload.accept.result', 'filter:api.live-video.create.accept.result' ]) { |
50 | target: 'filter:api.video.upload.accept.result', | 52 | registerHook({ |
51 | handler: ({ accepted }, { videoBody }) => { | 53 | target: hook, |
52 | if (!accepted) return { accepted: false } | 54 | handler: ({ accepted }, { videoBody, liveVideoBody }) => { |
53 | if (videoBody.name.indexOf('bad word') !== -1) return { accepted: false, errorMessage: 'bad word' } | 55 | if (!accepted) return { accepted: false } |
54 | 56 | ||
55 | return { accepted: true } | 57 | const name = videoBody |
56 | } | 58 | ? videoBody.name |
57 | }) | 59 | : liveVideoBody.name |
60 | |||
61 | if (name.indexOf('bad word') !== -1) return { accepted: false, errorMessage: 'bad word' } | ||
62 | |||
63 | return { accepted: true } | ||
64 | } | ||
65 | }) | ||
66 | } | ||
58 | 67 | ||
59 | registerHook({ | 68 | registerHook({ |
60 | target: 'filter:api.video.pre-import-url.accept.result', | 69 | target: 'filter:api.video.pre-import-url.accept.result', |
diff --git a/server/tests/plugins/action-hooks.ts b/server/tests/plugins/action-hooks.ts index ca57a4b51..ac9f2cea5 100644 --- a/server/tests/plugins/action-hooks.ts +++ b/server/tests/plugins/action-hooks.ts | |||
@@ -1,18 +1,12 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { | 4 | import { ServerHookName, VideoPrivacy } from '@shared/models' |
5 | cleanupTests, | ||
6 | flushAndRunMultipleServers, | ||
7 | killallServers, | ||
8 | reRunServer, | ||
9 | ServerInfo, | ||
10 | waitUntilLog | ||
11 | } from '../../../shared/extra-utils/server/servers' | ||
12 | import { | 5 | import { |
13 | addVideoCommentReply, | 6 | addVideoCommentReply, |
14 | addVideoCommentThread, | 7 | addVideoCommentThread, |
15 | blockUser, | 8 | blockUser, |
9 | createLive, | ||
16 | createUser, | 10 | createUser, |
17 | deleteVideoComment, | 11 | deleteVideoComment, |
18 | getPluginTestPath, | 12 | getPluginTestPath, |
@@ -20,6 +14,7 @@ import { | |||
20 | registerUser, | 14 | registerUser, |
21 | removeUser, | 15 | removeUser, |
22 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
17 | setDefaultVideoChannel, | ||
23 | unblockUser, | 18 | unblockUser, |
24 | updateUser, | 19 | updateUser, |
25 | updateVideo, | 20 | updateVideo, |
@@ -27,13 +22,21 @@ import { | |||
27 | userLogin, | 22 | userLogin, |
28 | viewVideo | 23 | viewVideo |
29 | } from '../../../shared/extra-utils' | 24 | } from '../../../shared/extra-utils' |
25 | import { | ||
26 | cleanupTests, | ||
27 | flushAndRunMultipleServers, | ||
28 | killallServers, | ||
29 | reRunServer, | ||
30 | ServerInfo, | ||
31 | waitUntilLog | ||
32 | } from '../../../shared/extra-utils/server/servers' | ||
30 | 33 | ||
31 | describe('Test plugin action hooks', function () { | 34 | describe('Test plugin action hooks', function () { |
32 | let servers: ServerInfo[] | 35 | let servers: ServerInfo[] |
33 | let videoUUID: string | 36 | let videoUUID: string |
34 | let threadId: number | 37 | let threadId: number |
35 | 38 | ||
36 | function checkHook (hook: string) { | 39 | function checkHook (hook: ServerHookName) { |
37 | return waitUntilLog(servers[0], 'Run hook ' + hook) | 40 | return waitUntilLog(servers[0], 'Run hook ' + hook) |
38 | } | 41 | } |
39 | 42 | ||
@@ -42,6 +45,7 @@ describe('Test plugin action hooks', function () { | |||
42 | 45 | ||
43 | servers = await flushAndRunMultipleServers(2) | 46 | servers = await flushAndRunMultipleServers(2) |
44 | await setAccessTokensToServers(servers) | 47 | await setAccessTokensToServers(servers) |
48 | await setDefaultVideoChannel(servers) | ||
45 | 49 | ||
46 | await installPlugin({ | 50 | await installPlugin({ |
47 | url: servers[0].url, | 51 | url: servers[0].url, |
@@ -51,7 +55,11 @@ describe('Test plugin action hooks', function () { | |||
51 | 55 | ||
52 | killallServers([ servers[0] ]) | 56 | killallServers([ servers[0] ]) |
53 | 57 | ||
54 | await reRunServer(servers[0]) | 58 | await reRunServer(servers[0], { |
59 | live: { | ||
60 | enabled: true | ||
61 | } | ||
62 | }) | ||
55 | }) | 63 | }) |
56 | 64 | ||
57 | describe('Application hooks', function () { | 65 | describe('Application hooks', function () { |
@@ -81,6 +89,21 @@ describe('Test plugin action hooks', function () { | |||
81 | }) | 89 | }) |
82 | }) | 90 | }) |
83 | 91 | ||
92 | describe('Live hooks', function () { | ||
93 | |||
94 | it('Should run action:api.live-video.created', async function () { | ||
95 | const attributes = { | ||
96 | name: 'live', | ||
97 | privacy: VideoPrivacy.PUBLIC, | ||
98 | channelId: servers[0].videoChannel.id | ||
99 | } | ||
100 | |||
101 | await createLive(servers[0].url, servers[0].accessToken, attributes) | ||
102 | |||
103 | await checkHook('action:api.live-video.created') | ||
104 | }) | ||
105 | }) | ||
106 | |||
84 | describe('Comments hooks', function () { | 107 | describe('Comments hooks', function () { |
85 | it('Should run action:api.video-thread.created', async function () { | 108 | it('Should run action:api.video-thread.created', async function () { |
86 | const res = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'thread') | 109 | const res = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'thread') |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index 4d354b68e..9939b8e6e 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -6,6 +6,7 @@ import { ServerConfig } from '@shared/models' | |||
6 | import { | 6 | import { |
7 | addVideoCommentReply, | 7 | addVideoCommentReply, |
8 | addVideoCommentThread, | 8 | addVideoCommentThread, |
9 | createLive, | ||
9 | doubleFollow, | 10 | doubleFollow, |
10 | getConfig, | 11 | getConfig, |
11 | getPluginTestPath, | 12 | getPluginTestPath, |
@@ -19,6 +20,7 @@ import { | |||
19 | registerUser, | 20 | registerUser, |
20 | setAccessTokensToServers, | 21 | setAccessTokensToServers, |
21 | setDefaultVideoChannel, | 22 | setDefaultVideoChannel, |
23 | updateCustomSubConfig, | ||
22 | updateVideo, | 24 | updateVideo, |
23 | uploadVideo, | 25 | uploadVideo, |
24 | waitJobs | 26 | waitJobs |
@@ -61,6 +63,17 @@ describe('Test plugin filter hooks', function () { | |||
61 | 63 | ||
62 | const res = await getVideosList(servers[0].url) | 64 | const res = await getVideosList(servers[0].url) |
63 | videoUUID = res.body.data[0].uuid | 65 | videoUUID = res.body.data[0].uuid |
66 | |||
67 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
68 | live: { enabled: true }, | ||
69 | signup: { enabled: true }, | ||
70 | import: { | ||
71 | videos: { | ||
72 | http: { enabled: true }, | ||
73 | torrent: { enabled: true } | ||
74 | } | ||
75 | } | ||
76 | }) | ||
64 | }) | 77 | }) |
65 | 78 | ||
66 | it('Should run filter:api.videos.list.params', async function () { | 79 | it('Should run filter:api.videos.list.params', async function () { |
@@ -87,6 +100,16 @@ describe('Test plugin filter hooks', function () { | |||
87 | await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video with bad word' }, 403) | 100 | await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video with bad word' }, 403) |
88 | }) | 101 | }) |
89 | 102 | ||
103 | it('Should run filter:api.live-video.create.accept.result', async function () { | ||
104 | const attributes = { | ||
105 | name: 'video with bad word', | ||
106 | privacy: VideoPrivacy.PUBLIC, | ||
107 | channelId: servers[0].videoChannel.id | ||
108 | } | ||
109 | |||
110 | await createLive(servers[0].url, servers[0].accessToken, attributes, 403) | ||
111 | }) | ||
112 | |||
90 | it('Should run filter:api.video.pre-import-url.accept.result', async function () { | 113 | it('Should run filter:api.video.pre-import-url.accept.result', async function () { |
91 | const baseAttributes = { | 114 | const baseAttributes = { |
92 | name: 'normal title', | 115 | name: 'normal title', |
diff --git a/shared/models/plugins/server-hook.model.ts b/shared/models/plugins/server-hook.model.ts index 5f812904f..6609bc893 100644 --- a/shared/models/plugins/server-hook.model.ts +++ b/shared/models/plugins/server-hook.model.ts | |||
@@ -9,9 +9,10 @@ export const serverFilterHookObject = { | |||
9 | // Used to get detailed video information (video watch page for example) | 9 | // Used to get detailed video information (video watch page for example) |
10 | 'filter:api.video.get.result': true, | 10 | 'filter:api.video.get.result': true, |
11 | 11 | ||
12 | // Filter the result of the accept upload, import via torrent or url functions | 12 | // Filter the result of the accept upload/live, import via torrent/url functions |
13 | // If this function returns false then the upload is aborted with an error | 13 | // If this function returns false then the upload is aborted with an error |
14 | 'filter:api.video.upload.accept.result': true, | 14 | 'filter:api.video.upload.accept.result': true, |
15 | 'filter:api.live-video.create.accept.result': true, | ||
15 | 'filter:api.video.pre-import-url.accept.result': true, | 16 | 'filter:api.video.pre-import-url.accept.result': true, |
16 | 'filter:api.video.pre-import-torrent.accept.result': true, | 17 | 'filter:api.video.pre-import-torrent.accept.result': true, |
17 | 'filter:api.video.post-import-url.accept.result': true, | 18 | 'filter:api.video.post-import-url.accept.result': true, |
@@ -54,6 +55,9 @@ export const serverActionHookObject = { | |||
54 | // Fired when a local video is viewed | 55 | // Fired when a local video is viewed |
55 | 'action:api.video.viewed': true, | 56 | 'action:api.video.viewed': true, |
56 | 57 | ||
58 | // Fired when a live video is created | ||
59 | 'action:api.live-video.created': true, | ||
60 | |||
57 | // Fired when a thread is created | 61 | // Fired when a thread is created |
58 | 'action:api.video-thread.created': true, | 62 | 'action:api.video-thread.created': true, |
59 | // Fired when a reply to a thread is created | 63 | // Fired when a reply to a thread is created |