]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video.ts
Send account activitypub update events
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
index 9e26f9bbedb2c34568fd7fb814524e433eb7d9e4..c4b716cd21e68013adbc4f1d0b167f9012d53036 100644 (file)
@@ -5,77 +5,124 @@ import * as parseTorrent from 'parse-torrent'
 import { join } from 'path'
 import * as Sequelize from 'sequelize'
 import {
-  AfterDestroy,
-  AllowNull,
-  BelongsTo,
-  BelongsToMany,
-  Column,
-  CreatedAt,
-  DataType,
-  Default,
-  ForeignKey,
-  HasMany,
-  IFindOptions,
-  Is,
-  IsInt,
-  IsUUID,
-  Min,
-  Model,
-  Table,
-  UpdatedAt
+  AfterDestroy, AllowNull, BelongsTo, BelongsToMany, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, IFindOptions, Is,
+  IsInt, IsUUID, Min, Model, Scopes, Table, UpdatedAt
 } from 'sequelize-typescript'
 import { IIncludeOptions } from 'sequelize-typescript/lib/interfaces/IIncludeOptions'
 import { VideoPrivacy, VideoResolution } from '../../../shared'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
+import { Video, VideoDetails } from '../../../shared/models/videos'
+import { activityPubCollection } from '../../helpers/activitypub'
+import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
+import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
+import { isBooleanValid } from '../../helpers/custom-validators/misc'
 import {
-  activityPubCollection,
-  createTorrentPromise,
-  generateImageFromVideoFile,
-  getVideoFileHeight,
-  logger,
-  renamePromise,
-  statPromise,
-  transcode,
-  unlinkPromise,
-  writeFilePromise
-} from '../../helpers'
-import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
-import {
-  isVideoCategoryValid,
-  isVideoDescriptionValid,
-  isVideoDurationValid,
-  isVideoLanguageValid,
-  isVideoLicenceValid,
-  isVideoNameValid,
-  isVideoNSFWValid,
+  isVideoCategoryValid, isVideoDescriptionValid, isVideoDurationValid, isVideoLanguageValid, isVideoLicenceValid, isVideoNameValid,
   isVideoPrivacyValid
 } from '../../helpers/custom-validators/videos'
+import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils'
+import { logger } from '../../helpers/logger'
 import {
-  API_VERSION,
-  CONFIG,
-  CONSTRAINTS_FIELDS,
-  PREVIEWS_SIZE,
-  REMOTE_SCHEME,
-  STATIC_PATHS,
-  THUMBNAILS_SIZE,
-  VIDEO_CATEGORIES,
-  VIDEO_LANGUAGES,
-  VIDEO_LICENCES,
-  VIDEO_PRIVACIES
+  API_VERSION, CONFIG, CONSTRAINTS_FIELDS, PREVIEWS_SIZE, REMOTE_SCHEME, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_CATEGORIES,
+  VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES
 } from '../../initializers'
 import { getAnnounceActivityPubUrl } from '../../lib/activitypub'
-import { sendDeleteVideo } from '../../lib/index'
+import { sendDeleteVideo } from '../../lib/activitypub/send'
 import { AccountModel } from '../account/account'
 import { AccountVideoRateModel } from '../account/account-video-rate'
+import { ActorModel } from '../activitypub/actor'
 import { ServerModel } from '../server/server'
 import { getSort, throwIfNotValid } from '../utils'
 import { TagModel } from './tag'
 import { VideoAbuseModel } from './video-abuse'
 import { VideoChannelModel } from './video-channel'
+import { VideoCommentModel } from './video-comment'
 import { VideoFileModel } from './video-file'
 import { VideoShareModel } from './video-share'
 import { VideoTagModel } from './video-tag'
 
+enum ScopeNames {
+  AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
+  WITH_ACCOUNT = 'WITH_ACCOUNT',
+  WITH_TAGS = 'WITH_TAGS',
+  WITH_FILES = 'WITH_FILES',
+  WITH_SHARES = 'WITH_SHARES',
+  WITH_RATES = 'WITH_RATES',
+  WITH_COMMENTS = 'WITH_COMMENTS'
+}
+
+@Scopes({
+  [ScopeNames.AVAILABLE_FOR_LIST]: {
+    where: {
+      id: {
+        [Sequelize.Op.notIn]: Sequelize.literal(
+          '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
+        )
+      },
+      privacy: VideoPrivacy.PUBLIC
+    }
+  },
+  [ScopeNames.WITH_ACCOUNT]: {
+    include: [
+      {
+        model: () => VideoChannelModel,
+        required: true,
+        include: [
+          {
+            model: () => AccountModel,
+            required: true,
+            include: [
+              {
+                model: () => ActorModel,
+                required: true,
+                include: [
+                  {
+                    model: () => ServerModel,
+                    required: false
+                  }
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  [ScopeNames.WITH_TAGS]: {
+    include: [ () => TagModel ]
+  },
+  [ScopeNames.WITH_FILES]: {
+    include: [
+      {
+        model: () => VideoFileModel,
+        required: true
+      }
+    ]
+  },
+  [ScopeNames.WITH_SHARES]: {
+    include: [
+      {
+        model: () => VideoShareModel,
+        include: [ () => ActorModel ]
+      }
+    ]
+  },
+  [ScopeNames.WITH_RATES]: {
+    include: [
+      {
+        model: () => AccountVideoRateModel,
+        include: [ () => AccountModel ]
+      }
+    ]
+  },
+  [ScopeNames.WITH_COMMENTS]: {
+    include: [
+      {
+        model: () => VideoCommentModel
+      }
+    ]
+  }
+})
 @Table({
   tableName: 'video',
   indexes: [
@@ -139,7 +186,7 @@ export class VideoModel extends Model<VideoModel> {
   privacy: number
 
   @AllowNull(false)
-  @Is('VideoNSFW', value => throwIfNotValid(value, isVideoNSFWValid, 'NSFW boolean'))
+  @Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
   @Column
   nsfw: boolean
 
@@ -184,6 +231,10 @@ export class VideoModel extends Model<VideoModel> {
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
   url: string
 
+  @AllowNull(false)
+  @Column
+  commentsEnabled: boolean
+
   @CreatedAt
   createdAt: Date
 
@@ -196,7 +247,7 @@ export class VideoModel extends Model<VideoModel> {
 
   @BelongsTo(() => VideoChannelModel, {
     foreignKey: {
-      allowNull: false
+      allowNull: true
     },
     onDelete: 'cascade'
   })
@@ -245,6 +296,15 @@ export class VideoModel extends Model<VideoModel> {
   })
   AccountVideoRates: AccountVideoRateModel[]
 
+  @HasMany(() => VideoCommentModel, {
+    foreignKey: {
+      name: 'videoId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
+  VideoComments: VideoCommentModel[]
+
   @AfterDestroy
   static removeFilesAndSendDelete (instance: VideoModel) {
     const tasks = []
@@ -273,21 +333,18 @@ export class VideoModel extends Model<VideoModel> {
   }
 
   static list () {
-    const query = {
-      include: [ VideoFileModel ]
-    }
-
-    return VideoModel.findAll(query)
+    return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
   }
 
-  static listAllAndSharedByAccountForOutbox (accountId: number, start: number, count: number) {
+  static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
     function getRawQuery (select: string) {
       const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
         'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
-        'WHERE "VideoChannel"."accountId" = ' + accountId
+        'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
+        'WHERE "Account"."actorId" = ' + actorId
       const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
         'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
-        'WHERE "VideoShare"."accountId" = ' + accountId
+        'WHERE "VideoShare"."actorId" = ' + actorId
 
       return `(${queryVideo}) UNION (${queryVideoShare})`
     }
@@ -317,11 +374,16 @@ export class VideoModel extends Model<VideoModel> {
                 }
               },
               {
-                accountId
+                actorId
               }
             ]
           },
-          include: [ AccountModel ]
+          include: [
+            {
+              model: ActorModel,
+              required: true
+            }
+          ]
         },
         {
           model: VideoChannelModel,
@@ -338,7 +400,8 @@ export class VideoModel extends Model<VideoModel> {
           include: [ AccountModel ]
         },
         VideoFileModel,
-        TagModel
+        TagModel,
+        VideoCommentModel
       ]
     }
 
@@ -363,10 +426,9 @@ export class VideoModel extends Model<VideoModel> {
 
   static listUserVideosForApi (userId: number, start: number, count: number, sort: string) {
     const query = {
-      distinct: true,
       offset: start,
       limit: count,
-      order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ],
+      order: [ getSort(sort) ],
       include: [
         {
           model: VideoChannelModel,
@@ -380,8 +442,7 @@ export class VideoModel extends Model<VideoModel> {
               required: true
             }
           ]
-        },
-        TagModel
+        }
       ]
     }
 
@@ -395,50 +456,30 @@ export class VideoModel extends Model<VideoModel> {
 
   static listForApi (start: number, count: number, sort: string) {
     const query = {
-      distinct: true,
       offset: start,
       limit: count,
-      order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ],
-      include: [
-        {
-          model: VideoChannelModel,
-          required: true,
-          include: [
-            {
-              model: AccountModel,
-              required: true,
-              include: [
-                {
-                  model: ServerModel,
-                  required: false
-                }
-              ]
-            }
-          ]
-        },
-        TagModel
-      ],
-      where: this.createBaseVideosWhere()
+      order: [ getSort(sort) ]
     }
 
-    return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
-      return {
-        data: rows,
-        total: count
-      }
-    })
+    return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST, ScopeNames.WITH_ACCOUNT ])
+      .findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return {
+          data: rows,
+          total: count
+        }
+      })
   }
 
   static load (id: number) {
     return VideoModel.findById(id)
   }
 
-  static loadByUUID (uuid: string, t?: Sequelize.Transaction) {
+  static loadByUrl (url: string, t?: Sequelize.Transaction) {
     const query: IFindOptions<VideoModel> = {
       where: {
-        uuid
-      },
-      include: [ VideoFileModel ]
+        url
+      }
     }
 
     if (t !== undefined) query.transaction = t
@@ -450,19 +491,12 @@ export class VideoModel extends Model<VideoModel> {
     const query: IFindOptions<VideoModel> = {
       where: {
         url
-      },
-      include: [
-        VideoFileModel,
-        {
-          model: VideoChannelModel,
-          include: [ AccountModel ]
-        }
-      ]
+      }
     }
 
     if (t !== undefined) query.transaction = t
 
-    return VideoModel.findOne(query)
+    return VideoModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_FILES ]).findOne(query)
   }
 
   static loadByUUIDOrURL (uuid: string, url: string, t?: Sequelize.Transaction) {
@@ -472,42 +506,34 @@ export class VideoModel extends Model<VideoModel> {
           { uuid },
           { url }
         ]
-      },
-      include: [ VideoFileModel ]
+      }
     }
 
     if (t !== undefined) query.transaction = t
 
-    return VideoModel.findOne(query)
+    return VideoModel.scope(ScopeNames.WITH_FILES).findOne(query)
   }
 
   static loadAndPopulateAccountAndServerAndTags (id: number) {
     const options = {
-      order: [ [ 'Tags', 'name', 'ASC' ] ],
-      include: [
-        {
-          model: VideoChannelModel,
-          include: [
-            {
-              model: AccountModel,
-              include: [ { model: ServerModel, required: false } ]
-            }
-          ]
-        },
-        {
-          model: AccountVideoRateModel,
-          include: [ AccountModel ]
-        },
-        {
-          model: VideoShareModel,
-          include: [ AccountModel ]
-        },
-        TagModel,
-        VideoFileModel
-      ]
+      order: [ [ 'Tags', 'name', 'ASC' ] ]
     }
 
-    return VideoModel.findById(id, options)
+    return VideoModel
+      .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT ])
+      .findById(id, options)
+  }
+
+  static loadByUUID (uuid: string) {
+    const options = {
+      where: {
+        uuid
+      }
+    }
+
+    return VideoModel
+      .scope([ ScopeNames.WITH_FILES ])
+      .findOne(options)
   }
 
   static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) {
@@ -515,31 +541,32 @@ export class VideoModel extends Model<VideoModel> {
       order: [ [ 'Tags', 'name', 'ASC' ] ],
       where: {
         uuid
-      },
-      include: [
-        {
-          model: VideoChannelModel,
-          include: [
-            {
-              model: AccountModel,
-              include: [ { model: ServerModel, required: false } ]
-            }
-          ]
-        },
-        {
-          model: AccountVideoRateModel,
-          include: [ AccountModel ]
-        },
-        {
-          model: VideoShareModel,
-          include: [ AccountModel ]
-        },
-        TagModel,
-        VideoFileModel
-      ]
+      }
+    }
+
+    return VideoModel
+      .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT ])
+      .findOne(options)
+  }
+
+  static loadAndPopulateAll (id: number) {
+    const options = {
+      order: [ [ 'Tags', 'name', 'ASC' ] ],
+      where: {
+        id
+      }
     }
 
-    return VideoModel.findOne(options)
+    return VideoModel
+      .scope([
+        ScopeNames.WITH_RATES,
+        ScopeNames.WITH_SHARES,
+        ScopeNames.WITH_TAGS,
+        ScopeNames.WITH_FILES,
+        ScopeNames.WITH_ACCOUNT,
+        ScopeNames.WITH_COMMENTS
+      ])
+      .findOne(options)
   }
 
   static searchAndPopulateAccountAndServerAndTags (value: string, start: number, count: number, sort: string) {
@@ -550,7 +577,13 @@ export class VideoModel extends Model<VideoModel> {
 
     const accountInclude: IIncludeOptions = {
       model: AccountModel,
-      include: [ serverInclude ]
+      include: [
+        {
+          model: ActorModel,
+          required: true,
+          include: [ serverInclude ]
+        }
+      ]
     }
 
     const videoChannelInclude: IIncludeOptions = {
@@ -564,11 +597,11 @@ export class VideoModel extends Model<VideoModel> {
     }
 
     const query: IFindOptions<VideoModel> = {
-      distinct: true,
-      where: this.createBaseVideosWhere(),
+      distinct: true, // Because we have tags
       offset: start,
       limit: count,
-      order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ]
+      order: [ getSort(sort) ],
+      where: {}
     }
 
     // TODO: search on tags too
@@ -595,23 +628,13 @@ export class VideoModel extends Model<VideoModel> {
       videoChannelInclude, tagInclude
     ]
 
-    return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
-      return {
-        data: rows,
-        total: count
-      }
-    })
-  }
-
-  private static createBaseVideosWhere () {
-    return {
-      id: {
-        [Sequelize.Op.notIn]: VideoModel.sequelize.literal(
-          '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
-        )
-      },
-      privacy: VideoPrivacy.PUBLIC
-    }
+    return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST ])
+      .findAndCountAll(query).then(({ rows, count }) => {
+        return {
+          data: rows,
+          total: count
+        }
+      })
   }
 
   getOriginalFile () {
@@ -707,8 +730,8 @@ export class VideoModel extends Model<VideoModel> {
   toFormattedJSON () {
     let serverHost
 
-    if (this.VideoChannel.Account.Server) {
-      serverHost = this.VideoChannel.Account.Server.host
+    if (this.VideoChannel.Account.Actor.Server) {
+      serverHost = this.VideoChannel.Account.Actor.Server.host
     } else {
       // It means it's our video
       serverHost = CONFIG.WEBSERVER.HOST
@@ -733,13 +756,12 @@ export class VideoModel extends Model<VideoModel> {
       views: this.views,
       likes: this.likes,
       dislikes: this.dislikes,
-      tags: map<TagModel, string>(this.Tags, 'name'),
       thumbnailPath: this.getThumbnailPath(),
       previewPath: this.getPreviewPath(),
       embedPath: this.getEmbedPath(),
       createdAt: this.createdAt,
       updatedAt: this.updatedAt
-    }
+    } as Video
   }
 
   toFormattedDetailsJSON () {
@@ -755,6 +777,8 @@ export class VideoModel extends Model<VideoModel> {
       descriptionPath: this.getDescriptionPath(),
       channel: this.VideoChannel.toFormattedJSON(),
       account: this.VideoChannel.Account.toFormattedJSON(),
+      tags: map<TagModel, string>(this.Tags, 'name'),
+      commentsEnabled: this.commentsEnabled,
       files: []
     }
 
@@ -779,7 +803,7 @@ export class VideoModel extends Model<VideoModel> {
         return -1
       })
 
-    return Object.assign(formattedJson, detailsJson)
+    return Object.assign(formattedJson, detailsJson) as VideoDetails
   }
 
   toActivityPubObject (): VideoTorrentObject {
@@ -824,9 +848,9 @@ export class VideoModel extends Model<VideoModel> {
 
       for (const rate of this.AccountVideoRates) {
         if (rate.type === 'like') {
-          likes.push(rate.Account.url)
+          likes.push(rate.Account.Actor.url)
         } else if (rate.type === 'dislike') {
-          dislikes.push(rate.Account.url)
+          dislikes.push(rate.Account.Actor.url)
         }
       }
 
@@ -839,13 +863,24 @@ export class VideoModel extends Model<VideoModel> {
       const shares: string[] = []
 
       for (const videoShare of this.VideoShares) {
-        const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account)
+        const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Actor)
         shares.push(shareUrl)
       }
 
       sharesObject = activityPubCollection(shares)
     }
 
+    let commentsObject
+    if (Array.isArray(this.VideoComments)) {
+      const comments: string[] = []
+
+      for (const videoComment of this.VideoComments) {
+        comments.push(videoComment.url)
+      }
+
+      commentsObject = activityPubCollection(comments)
+    }
+
     const url = []
     for (const file of this.VideoFiles) {
       url.push({
@@ -891,6 +926,7 @@ export class VideoModel extends Model<VideoModel> {
       language,
       views: this.views,
       nsfw: this.nsfw,
+      commentsEnabled: this.commentsEnabled,
       published: this.createdAt.toISOString(),
       updated: this.updatedAt.toISOString(),
       mediaType: 'text/markdown',
@@ -905,7 +941,14 @@ export class VideoModel extends Model<VideoModel> {
       url,
       likes: likesObject,
       dislikes: dislikesObject,
-      shares: sharesObject
+      shares: sharesObject,
+      comments: commentsObject,
+      attributedTo: [
+        {
+          type: 'Group',
+          id: this.VideoChannel.Actor.url
+        }
+      ]
     }
   }
 
@@ -1049,8 +1092,8 @@ export class VideoModel extends Model<VideoModel> {
       baseUrlHttp = CONFIG.WEBSERVER.URL
       baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
     } else {
-      baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Server.host
-      baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Server.host
+      baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
+      baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
     }
 
     return { baseUrlHttp, baseUrlWs }