diff options
Diffstat (limited to 'server/controllers/api/video-channel.ts')
-rw-r--r-- | server/controllers/api/video-channel.ts | 431 |
1 files changed, 0 insertions, 431 deletions
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 @@ | |||
1 | import express from 'express' | ||
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | ||
3 | import { Hooks } from '@server/lib/plugins/hooks' | ||
4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | ||
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { MChannelBannerAccountDefault } from '@server/types/models' | ||
7 | import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models' | ||
8 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | ||
9 | import { resetSequelizeInstance } from '../../helpers/database-utils' | ||
10 | import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | ||
11 | import { logger } from '../../helpers/logger' | ||
12 | import { getFormattedObjects } from '../../helpers/utils' | ||
13 | import { MIMETYPES } from '../../initializers/constants' | ||
14 | import { sequelizeTypescript } from '../../initializers/database' | ||
15 | import { sendUpdateActor } from '../../lib/activitypub/send' | ||
16 | import { JobQueue } from '../../lib/job-queue' | ||
17 | import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor' | ||
18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | ||
19 | import { | ||
20 | apiRateLimiter, | ||
21 | asyncMiddleware, | ||
22 | asyncRetryTransactionMiddleware, | ||
23 | authenticate, | ||
24 | commonVideosFiltersValidator, | ||
25 | ensureCanManageChannelOrAccount, | ||
26 | optionalAuthenticate, | ||
27 | paginationValidator, | ||
28 | setDefaultPagination, | ||
29 | setDefaultSort, | ||
30 | setDefaultVideosSort, | ||
31 | videoChannelsAddValidator, | ||
32 | videoChannelsRemoveValidator, | ||
33 | videoChannelsSortValidator, | ||
34 | videoChannelsUpdateValidator, | ||
35 | videoPlaylistsSortValidator | ||
36 | } from '../../middlewares' | ||
37 | import { | ||
38 | ensureChannelOwnerCanUpload, | ||
39 | ensureIsLocalChannel, | ||
40 | videoChannelImportVideosValidator, | ||
41 | videoChannelsFollowersSortValidator, | ||
42 | videoChannelsListValidator, | ||
43 | videoChannelsNameWithHostValidator, | ||
44 | videosSortValidator | ||
45 | } from '../../middlewares/validators' | ||
46 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' | ||
47 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | ||
48 | import { AccountModel } from '../../models/account/account' | ||
49 | import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter' | ||
50 | import { VideoModel } from '../../models/video/video' | ||
51 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
52 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | ||
53 | |||
54 | const auditLogger = auditLoggerFactory('channels') | ||
55 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) | ||
56 | const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) | ||
57 | |||
58 | const videoChannelRouter = express.Router() | ||
59 | |||
60 | videoChannelRouter.use(apiRateLimiter) | ||
61 | |||
62 | videoChannelRouter.get('/', | ||
63 | paginationValidator, | ||
64 | videoChannelsSortValidator, | ||
65 | setDefaultSort, | ||
66 | setDefaultPagination, | ||
67 | videoChannelsListValidator, | ||
68 | asyncMiddleware(listVideoChannels) | ||
69 | ) | ||
70 | |||
71 | videoChannelRouter.post('/', | ||
72 | authenticate, | ||
73 | asyncMiddleware(videoChannelsAddValidator), | ||
74 | asyncRetryTransactionMiddleware(addVideoChannel) | ||
75 | ) | ||
76 | |||
77 | videoChannelRouter.post('/:nameWithHost/avatar/pick', | ||
78 | authenticate, | ||
79 | reqAvatarFile, | ||
80 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
81 | ensureIsLocalChannel, | ||
82 | ensureCanManageChannelOrAccount, | ||
83 | updateAvatarValidator, | ||
84 | asyncMiddleware(updateVideoChannelAvatar) | ||
85 | ) | ||
86 | |||
87 | videoChannelRouter.post('/:nameWithHost/banner/pick', | ||
88 | authenticate, | ||
89 | reqBannerFile, | ||
90 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
91 | ensureIsLocalChannel, | ||
92 | ensureCanManageChannelOrAccount, | ||
93 | updateBannerValidator, | ||
94 | asyncMiddleware(updateVideoChannelBanner) | ||
95 | ) | ||
96 | |||
97 | videoChannelRouter.delete('/:nameWithHost/avatar', | ||
98 | authenticate, | ||
99 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
100 | ensureIsLocalChannel, | ||
101 | ensureCanManageChannelOrAccount, | ||
102 | asyncMiddleware(deleteVideoChannelAvatar) | ||
103 | ) | ||
104 | |||
105 | videoChannelRouter.delete('/:nameWithHost/banner', | ||
106 | authenticate, | ||
107 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
108 | ensureIsLocalChannel, | ||
109 | ensureCanManageChannelOrAccount, | ||
110 | asyncMiddleware(deleteVideoChannelBanner) | ||
111 | ) | ||
112 | |||
113 | videoChannelRouter.put('/:nameWithHost', | ||
114 | authenticate, | ||
115 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
116 | ensureIsLocalChannel, | ||
117 | ensureCanManageChannelOrAccount, | ||
118 | videoChannelsUpdateValidator, | ||
119 | asyncRetryTransactionMiddleware(updateVideoChannel) | ||
120 | ) | ||
121 | |||
122 | videoChannelRouter.delete('/:nameWithHost', | ||
123 | authenticate, | ||
124 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
125 | ensureIsLocalChannel, | ||
126 | ensureCanManageChannelOrAccount, | ||
127 | asyncMiddleware(videoChannelsRemoveValidator), | ||
128 | asyncRetryTransactionMiddleware(removeVideoChannel) | ||
129 | ) | ||
130 | |||
131 | videoChannelRouter.get('/:nameWithHost', | ||
132 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
133 | asyncMiddleware(getVideoChannel) | ||
134 | ) | ||
135 | |||
136 | videoChannelRouter.get('/:nameWithHost/video-playlists', | ||
137 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
138 | paginationValidator, | ||
139 | videoPlaylistsSortValidator, | ||
140 | setDefaultSort, | ||
141 | setDefaultPagination, | ||
142 | commonVideoPlaylistFiltersValidator, | ||
143 | asyncMiddleware(listVideoChannelPlaylists) | ||
144 | ) | ||
145 | |||
146 | videoChannelRouter.get('/:nameWithHost/videos', | ||
147 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
148 | paginationValidator, | ||
149 | videosSortValidator, | ||
150 | setDefaultVideosSort, | ||
151 | setDefaultPagination, | ||
152 | optionalAuthenticate, | ||
153 | commonVideosFiltersValidator, | ||
154 | asyncMiddleware(listVideoChannelVideos) | ||
155 | ) | ||
156 | |||
157 | videoChannelRouter.get('/:nameWithHost/followers', | ||
158 | authenticate, | ||
159 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
160 | ensureCanManageChannelOrAccount, | ||
161 | paginationValidator, | ||
162 | videoChannelsFollowersSortValidator, | ||
163 | setDefaultSort, | ||
164 | setDefaultPagination, | ||
165 | asyncMiddleware(listVideoChannelFollowers) | ||
166 | ) | ||
167 | |||
168 | videoChannelRouter.post('/:nameWithHost/import-videos', | ||
169 | authenticate, | ||
170 | asyncMiddleware(videoChannelsNameWithHostValidator), | ||
171 | asyncMiddleware(videoChannelImportVideosValidator), | ||
172 | ensureIsLocalChannel, | ||
173 | ensureCanManageChannelOrAccount, | ||
174 | asyncMiddleware(ensureChannelOwnerCanUpload), | ||
175 | asyncMiddleware(importVideosInChannel) | ||
176 | ) | ||
177 | |||
178 | // --------------------------------------------------------------------------- | ||
179 | |||
180 | export { | ||
181 | videoChannelRouter | ||
182 | } | ||
183 | |||
184 | // --------------------------------------------------------------------------- | ||
185 | |||
186 | async function listVideoChannels (req: express.Request, res: express.Response) { | ||
187 | const serverActor = await getServerActor() | ||
188 | |||
189 | const apiOptions = await Hooks.wrapObject({ | ||
190 | actorId: serverActor.id, | ||
191 | start: req.query.start, | ||
192 | count: req.query.count, | ||
193 | sort: req.query.sort | ||
194 | }, 'filter:api.video-channels.list.params') | ||
195 | |||
196 | const resultList = await Hooks.wrapPromiseFun( | ||
197 | VideoChannelModel.listForApi, | ||
198 | apiOptions, | ||
199 | 'filter:api.video-channels.list.result' | ||
200 | ) | ||
201 | |||
202 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
203 | } | ||
204 | |||
205 | async function updateVideoChannelBanner (req: express.Request, res: express.Response) { | ||
206 | const bannerPhysicalFile = req.files['bannerfile'][0] | ||
207 | const videoChannel = res.locals.videoChannel | ||
208 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | ||
209 | |||
210 | const banners = await updateLocalActorImageFiles(videoChannel, bannerPhysicalFile, ActorImageType.BANNER) | ||
211 | |||
212 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | ||
213 | |||
214 | return res.json({ | ||
215 | banners: banners.map(b => b.toFormattedJSON()) | ||
216 | }) | ||
217 | } | ||
218 | |||
219 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | ||
220 | const avatarPhysicalFile = req.files['avatarfile'][0] | ||
221 | const videoChannel = res.locals.videoChannel | ||
222 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) | ||
223 | |||
224 | const avatars = await updateLocalActorImageFiles(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR) | ||
225 | auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) | ||
226 | |||
227 | return res.json({ | ||
228 | avatars: avatars.map(a => a.toFormattedJSON()) | ||
229 | }) | ||
230 | } | ||
231 | |||
232 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { | ||
233 | const videoChannel = res.locals.videoChannel | ||
234 | |||
235 | await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) | ||
236 | |||
237 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
238 | } | ||
239 | |||
240 | async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { | ||
241 | const videoChannel = res.locals.videoChannel | ||
242 | |||
243 | await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) | ||
244 | |||
245 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
246 | } | ||
247 | |||
248 | async function addVideoChannel (req: express.Request, res: express.Response) { | ||
249 | const videoChannelInfo: VideoChannelCreate = req.body | ||
250 | |||
251 | const videoChannelCreated = await sequelizeTypescript.transaction(async t => { | ||
252 | const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) | ||
253 | |||
254 | return createLocalVideoChannel(videoChannelInfo, account, t) | ||
255 | }) | ||
256 | |||
257 | const payload = { actorId: videoChannelCreated.actorId } | ||
258 | await JobQueue.Instance.createJob({ type: 'actor-keys', payload }) | ||
259 | |||
260 | auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON())) | ||
261 | logger.info('Video channel %s created.', videoChannelCreated.Actor.url) | ||
262 | |||
263 | Hooks.runAction('action:api.video-channel.created', { videoChannel: videoChannelCreated, req, res }) | ||
264 | |||
265 | return res.json({ | ||
266 | videoChannel: { | ||
267 | id: videoChannelCreated.id | ||
268 | } | ||
269 | }) | ||
270 | } | ||
271 | |||
272 | async function updateVideoChannel (req: express.Request, res: express.Response) { | ||
273 | const videoChannelInstance = res.locals.videoChannel | ||
274 | const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()) | ||
275 | const videoChannelInfoToUpdate = req.body as VideoChannelUpdate | ||
276 | let doBulkVideoUpdate = false | ||
277 | |||
278 | try { | ||
279 | await sequelizeTypescript.transaction(async t => { | ||
280 | if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName | ||
281 | if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description | ||
282 | |||
283 | if (videoChannelInfoToUpdate.support !== undefined) { | ||
284 | const oldSupportField = videoChannelInstance.support | ||
285 | videoChannelInstance.support = videoChannelInfoToUpdate.support | ||
286 | |||
287 | if (videoChannelInfoToUpdate.bulkVideosSupportUpdate === true && oldSupportField !== videoChannelInfoToUpdate.support) { | ||
288 | doBulkVideoUpdate = true | ||
289 | await VideoModel.bulkUpdateSupportField(videoChannelInstance, t) | ||
290 | } | ||
291 | } | ||
292 | |||
293 | const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault | ||
294 | await sendUpdateActor(videoChannelInstanceUpdated, t) | ||
295 | |||
296 | auditLogger.update( | ||
297 | getAuditIdFromRes(res), | ||
298 | new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()), | ||
299 | oldVideoChannelAuditKeys | ||
300 | ) | ||
301 | |||
302 | Hooks.runAction('action:api.video-channel.updated', { videoChannel: videoChannelInstanceUpdated, req, res }) | ||
303 | |||
304 | logger.info('Video channel %s updated.', videoChannelInstance.Actor.url) | ||
305 | }) | ||
306 | } catch (err) { | ||
307 | logger.debug('Cannot update the video channel.', { err }) | ||
308 | |||
309 | // If the transaction is retried, sequelize will think the object has not changed | ||
310 | // So we need to restore the previous fields | ||
311 | await resetSequelizeInstance(videoChannelInstance) | ||
312 | |||
313 | throw err | ||
314 | } | ||
315 | |||
316 | res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
317 | |||
318 | // Don't process in a transaction, and after the response because it could be long | ||
319 | if (doBulkVideoUpdate) { | ||
320 | await federateAllVideosOfChannel(videoChannelInstance) | ||
321 | } | ||
322 | } | ||
323 | |||
324 | async function removeVideoChannel (req: express.Request, res: express.Response) { | ||
325 | const videoChannelInstance = res.locals.videoChannel | ||
326 | |||
327 | await sequelizeTypescript.transaction(async t => { | ||
328 | await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t) | ||
329 | |||
330 | await videoChannelInstance.destroy({ transaction: t }) | ||
331 | |||
332 | Hooks.runAction('action:api.video-channel.deleted', { videoChannel: videoChannelInstance, req, res }) | ||
333 | |||
334 | auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())) | ||
335 | logger.info('Video channel %s deleted.', videoChannelInstance.Actor.url) | ||
336 | }) | ||
337 | |||
338 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
339 | } | ||
340 | |||
341 | async function getVideoChannel (req: express.Request, res: express.Response) { | ||
342 | const id = res.locals.videoChannel.id | ||
343 | const videoChannel = await Hooks.wrapObject(res.locals.videoChannel, 'filter:api.video-channel.get.result', { id }) | ||
344 | |||
345 | if (videoChannel.isOutdated()) { | ||
346 | JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } }) | ||
347 | } | ||
348 | |||
349 | return res.json(videoChannel.toFormattedJSON()) | ||
350 | } | ||
351 | |||
352 | async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { | ||
353 | const serverActor = await getServerActor() | ||
354 | |||
355 | const resultList = await VideoPlaylistModel.listForApi({ | ||
356 | followerActorId: serverActor.id, | ||
357 | start: req.query.start, | ||
358 | count: req.query.count, | ||
359 | sort: req.query.sort, | ||
360 | videoChannelId: res.locals.videoChannel.id, | ||
361 | type: req.query.playlistType | ||
362 | }) | ||
363 | |||
364 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
365 | } | ||
366 | |||
367 | async function listVideoChannelVideos (req: express.Request, res: express.Response) { | ||
368 | const serverActor = await getServerActor() | ||
369 | |||
370 | const videoChannelInstance = res.locals.videoChannel | ||
371 | |||
372 | const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res) | ||
373 | ? null | ||
374 | : { | ||
375 | actorId: serverActor.id, | ||
376 | orLocalVideos: true | ||
377 | } | ||
378 | |||
379 | const countVideos = getCountVideos(req) | ||
380 | const query = pickCommonVideoQuery(req.query) | ||
381 | |||
382 | const apiOptions = await Hooks.wrapObject({ | ||
383 | ...query, | ||
384 | |||
385 | displayOnlyForFollower, | ||
386 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
387 | videoChannelId: videoChannelInstance.id, | ||
388 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, | ||
389 | countVideos | ||
390 | }, 'filter:api.video-channels.videos.list.params') | ||
391 | |||
392 | const resultList = await Hooks.wrapPromiseFun( | ||
393 | VideoModel.listForApi, | ||
394 | apiOptions, | ||
395 | 'filter:api.video-channels.videos.list.result' | ||
396 | ) | ||
397 | |||
398 | return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) | ||
399 | } | ||
400 | |||
401 | async function listVideoChannelFollowers (req: express.Request, res: express.Response) { | ||
402 | const channel = res.locals.videoChannel | ||
403 | |||
404 | const resultList = await ActorFollowModel.listFollowersForApi({ | ||
405 | actorIds: [ channel.actorId ], | ||
406 | start: req.query.start, | ||
407 | count: req.query.count, | ||
408 | sort: req.query.sort, | ||
409 | search: req.query.search, | ||
410 | state: 'accepted' | ||
411 | }) | ||
412 | |||
413 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
414 | } | ||
415 | |||
416 | async function importVideosInChannel (req: express.Request, res: express.Response) { | ||
417 | const { externalChannelUrl } = req.body as VideosImportInChannelCreate | ||
418 | |||
419 | await JobQueue.Instance.createJob({ | ||
420 | type: 'video-channel-import', | ||
421 | payload: { | ||
422 | externalChannelUrl, | ||
423 | videoChannelId: res.locals.videoChannel.id, | ||
424 | partOfChannelSyncId: res.locals.videoChannelSync?.id | ||
425 | } | ||
426 | }) | ||
427 | |||
428 | logger.info('Video import job for channel "%s" with url "%s" created.', res.locals.videoChannel.name, externalChannelUrl) | ||
429 | |||
430 | return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() | ||
431 | } | ||