video-channel.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import * as express from 'express'
  2. import { Hooks } from '@server/lib/plugins/hooks'
  3. import { getServerActor } from '@server/models/application/application'
  4. import { MChannelAccountDefault } from '@server/types/models'
  5. import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
  6. import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
  7. import { resetSequelizeInstance } from '../../helpers/database-utils'
  8. import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
  9. import { logger } from '../../helpers/logger'
  10. import { getFormattedObjects } from '../../helpers/utils'
  11. import { CONFIG } from '../../initializers/config'
  12. import { MIMETYPES } from '../../initializers/constants'
  13. import { sequelizeTypescript } from '../../initializers/database'
  14. import { setAsyncActorKeys } from '../../lib/activitypub/actor'
  15. import { sendUpdateActor } from '../../lib/activitypub/send'
  16. import { deleteActorAvatarFile, updateActorAvatarFile } from '../../lib/avatar'
  17. import { JobQueue } from '../../lib/job-queue'
  18. import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
  19. import {
  20. asyncMiddleware,
  21. asyncRetryTransactionMiddleware,
  22. authenticate,
  23. commonVideosFiltersValidator,
  24. optionalAuthenticate,
  25. paginationValidator,
  26. setDefaultPagination,
  27. setDefaultSort,
  28. setDefaultVideosSort,
  29. videoChannelsAddValidator,
  30. videoChannelsRemoveValidator,
  31. videoChannelsSortValidator,
  32. videoChannelsUpdateValidator,
  33. videoPlaylistsSortValidator
  34. } from '../../middlewares'
  35. import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators'
  36. import { updateAvatarValidator } from '../../middlewares/validators/avatar'
  37. import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
  38. import { AccountModel } from '../../models/account/account'
  39. import { VideoModel } from '../../models/video/video'
  40. import { VideoChannelModel } from '../../models/video/video-channel'
  41. import { VideoPlaylistModel } from '../../models/video/video-playlist'
  42. import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
  43. const auditLogger = auditLoggerFactory('channels')
  44. const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
  45. const videoChannelRouter = express.Router()
  46. videoChannelRouter.get('/',
  47. paginationValidator,
  48. videoChannelsSortValidator,
  49. setDefaultSort,
  50. setDefaultPagination,
  51. videoChannelsOwnSearchValidator,
  52. asyncMiddleware(listVideoChannels)
  53. )
  54. videoChannelRouter.post('/',
  55. authenticate,
  56. asyncMiddleware(videoChannelsAddValidator),
  57. asyncRetryTransactionMiddleware(addVideoChannel)
  58. )
  59. videoChannelRouter.post('/:nameWithHost/avatar/pick',
  60. authenticate,
  61. reqAvatarFile,
  62. // Check the rights
  63. asyncMiddleware(videoChannelsUpdateValidator),
  64. updateAvatarValidator,
  65. asyncMiddleware(updateVideoChannelAvatar)
  66. )
  67. videoChannelRouter.delete('/:nameWithHost/avatar',
  68. authenticate,
  69. // Check the rights
  70. asyncMiddleware(videoChannelsUpdateValidator),
  71. asyncMiddleware(deleteVideoChannelAvatar)
  72. )
  73. videoChannelRouter.put('/:nameWithHost',
  74. authenticate,
  75. asyncMiddleware(videoChannelsUpdateValidator),
  76. asyncRetryTransactionMiddleware(updateVideoChannel)
  77. )
  78. videoChannelRouter.delete('/:nameWithHost',
  79. authenticate,
  80. asyncMiddleware(videoChannelsRemoveValidator),
  81. asyncRetryTransactionMiddleware(removeVideoChannel)
  82. )
  83. videoChannelRouter.get('/:nameWithHost',
  84. asyncMiddleware(videoChannelsNameWithHostValidator),
  85. asyncMiddleware(getVideoChannel)
  86. )
  87. videoChannelRouter.get('/:nameWithHost/video-playlists',
  88. asyncMiddleware(videoChannelsNameWithHostValidator),
  89. paginationValidator,
  90. videoPlaylistsSortValidator,
  91. setDefaultSort,
  92. setDefaultPagination,
  93. commonVideoPlaylistFiltersValidator,
  94. asyncMiddleware(listVideoChannelPlaylists)
  95. )
  96. videoChannelRouter.get('/:nameWithHost/videos',
  97. asyncMiddleware(videoChannelsNameWithHostValidator),
  98. paginationValidator,
  99. videosSortValidator,
  100. setDefaultVideosSort,
  101. setDefaultPagination,
  102. optionalAuthenticate,
  103. commonVideosFiltersValidator,
  104. asyncMiddleware(listVideoChannelVideos)
  105. )
  106. // ---------------------------------------------------------------------------
  107. export {
  108. videoChannelRouter
  109. }
  110. // ---------------------------------------------------------------------------
  111. async function listVideoChannels (req: express.Request, res: express.Response) {
  112. const serverActor = await getServerActor()
  113. const resultList = await VideoChannelModel.listForApi({
  114. actorId: serverActor.id,
  115. start: req.query.start,
  116. count: req.query.count,
  117. sort: req.query.sort
  118. })
  119. return res.json(getFormattedObjects(resultList.data, resultList.total))
  120. }
  121. async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
  122. const avatarPhysicalFile = req.files['avatarfile'][0]
  123. const videoChannel = res.locals.videoChannel
  124. const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
  125. const avatar = await updateActorAvatarFile(videoChannel, avatarPhysicalFile)
  126. auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
  127. return res
  128. .json({
  129. avatar: avatar.toFormattedJSON()
  130. })
  131. .end()
  132. }
  133. async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
  134. const videoChannel = res.locals.videoChannel
  135. await deleteActorAvatarFile(videoChannel)
  136. return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
  137. }
  138. async function addVideoChannel (req: express.Request, res: express.Response) {
  139. const videoChannelInfo: VideoChannelCreate = req.body
  140. const videoChannelCreated = await sequelizeTypescript.transaction(async t => {
  141. const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
  142. return createLocalVideoChannel(videoChannelInfo, account, t)
  143. })
  144. setAsyncActorKeys(videoChannelCreated.Actor)
  145. .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.url, { err }))
  146. auditLogger.create(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelCreated.toFormattedJSON()))
  147. logger.info('Video channel %s created.', videoChannelCreated.Actor.url)
  148. return res.json({
  149. videoChannel: {
  150. id: videoChannelCreated.id
  151. }
  152. }).end()
  153. }
  154. async function updateVideoChannel (req: express.Request, res: express.Response) {
  155. const videoChannelInstance = res.locals.videoChannel
  156. const videoChannelFieldsSave = videoChannelInstance.toJSON()
  157. const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
  158. const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
  159. let doBulkVideoUpdate = false
  160. try {
  161. await sequelizeTypescript.transaction(async t => {
  162. const sequelizeOptions = {
  163. transaction: t
  164. }
  165. if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName
  166. if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description
  167. if (videoChannelInfoToUpdate.support !== undefined) {
  168. const oldSupportField = videoChannelInstance.support
  169. videoChannelInstance.support = videoChannelInfoToUpdate.support
  170. if (videoChannelInfoToUpdate.bulkVideosSupportUpdate === true && oldSupportField !== videoChannelInfoToUpdate.support) {
  171. doBulkVideoUpdate = true
  172. await VideoModel.bulkUpdateSupportField(videoChannelInstance, t)
  173. }
  174. }
  175. const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault
  176. await sendUpdateActor(videoChannelInstanceUpdated, t)
  177. auditLogger.update(
  178. getAuditIdFromRes(res),
  179. new VideoChannelAuditView(videoChannelInstanceUpdated.toFormattedJSON()),
  180. oldVideoChannelAuditKeys
  181. )
  182. logger.info('Video channel %s updated.', videoChannelInstance.Actor.url)
  183. })
  184. } catch (err) {
  185. logger.debug('Cannot update the video channel.', { err })
  186. // Force fields we want to update
  187. // If the transaction is retried, sequelize will think the object has not changed
  188. // So it will skip the SQL request, even if the last one was ROLLBACKed!
  189. resetSequelizeInstance(videoChannelInstance, videoChannelFieldsSave)
  190. throw err
  191. }
  192. res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
  193. // Don't process in a transaction, and after the response because it could be long
  194. if (doBulkVideoUpdate) {
  195. await federateAllVideosOfChannel(videoChannelInstance)
  196. }
  197. }
  198. async function removeVideoChannel (req: express.Request, res: express.Response) {
  199. const videoChannelInstance = res.locals.videoChannel
  200. await sequelizeTypescript.transaction(async t => {
  201. await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t)
  202. await videoChannelInstance.destroy({ transaction: t })
  203. auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
  204. logger.info('Video channel %s deleted.', videoChannelInstance.Actor.url)
  205. })
  206. return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
  207. }
  208. async function getVideoChannel (req: express.Request, res: express.Response) {
  209. const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id)
  210. if (videoChannelWithVideos.isOutdated()) {
  211. JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } })
  212. }
  213. return res.json(videoChannelWithVideos.toFormattedJSON())
  214. }
  215. async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
  216. const serverActor = await getServerActor()
  217. const resultList = await VideoPlaylistModel.listForApi({
  218. followerActorId: serverActor.id,
  219. start: req.query.start,
  220. count: req.query.count,
  221. sort: req.query.sort,
  222. videoChannelId: res.locals.videoChannel.id,
  223. type: req.query.playlistType
  224. })
  225. return res.json(getFormattedObjects(resultList.data, resultList.total))
  226. }
  227. async function listVideoChannelVideos (req: express.Request, res: express.Response) {
  228. const videoChannelInstance = res.locals.videoChannel
  229. const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
  230. const countVideos = getCountVideos(req)
  231. const apiOptions = await Hooks.wrapObject({
  232. followerActorId,
  233. start: req.query.start,
  234. count: req.query.count,
  235. sort: req.query.sort,
  236. includeLocalVideos: true,
  237. categoryOneOf: req.query.categoryOneOf,
  238. licenceOneOf: req.query.licenceOneOf,
  239. languageOneOf: req.query.languageOneOf,
  240. tagsOneOf: req.query.tagsOneOf,
  241. tagsAllOf: req.query.tagsAllOf,
  242. filter: req.query.filter,
  243. nsfw: buildNSFWFilter(res, req.query.nsfw),
  244. withFiles: false,
  245. videoChannelId: videoChannelInstance.id,
  246. user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
  247. countVideos
  248. }, 'filter:api.video-channels.videos.list.params')
  249. const resultList = await Hooks.wrapPromiseFun(
  250. VideoModel.listForApi,
  251. apiOptions,
  252. 'filter:api.video-channels.videos.list.result'
  253. )
  254. return res.json(getFormattedObjects(resultList.data, resultList.total))
  255. }