Add subscriptions endpoints to REST API
[oweals/peertube.git] / server / models / video / video-channel.ts
index 6d70f2aa2e77f23b4570bc474491fa3945a2ed29..0273fab13c2958856b6eb5f3718444df2881f83e 100644 (file)
-import * as Sequelize from 'sequelize'
-
-import { isVideoChannelNameValid, isVideoChannelDescriptionValid } from '../../helpers'
-
-import { addMethodsToModel, getSort } from '../utils'
 import {
-  VideoChannelInstance,
-  VideoChannelAttributes,
-
-  VideoChannelMethods
-} from './video-channel-interface'
-import { sendDeleteVideoChannel } from '../../lib/activitypub/send-request'
-import { isVideoChannelUrlValid } from '../../helpers/custom-validators/video-channels'
-import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
-
-let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
-let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
-let toActivityPubObject: VideoChannelMethods.ToActivityPubObject
-let isOwned: VideoChannelMethods.IsOwned
-let countByAccount: VideoChannelMethods.CountByAccount
-let listOwned: VideoChannelMethods.ListOwned
-let listForApi: VideoChannelMethods.ListForApi
-let listByAccount: VideoChannelMethods.ListByAccount
-let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
-let loadByUUID: VideoChannelMethods.LoadByUUID
-let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
-let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
-let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID
-let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
-let loadByUrl: VideoChannelMethods.LoadByUrl
-let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl
-
-export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
-  VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel',
+  AllowNull,
+  BeforeDestroy,
+  BelongsTo,
+  Column,
+  CreatedAt,
+  DataType,
+  Default,
+  DefaultScope,
+  ForeignKey,
+  HasMany,
+  Is,
+  Model,
+  Scopes,
+  Table,
+  UpdatedAt
+} from 'sequelize-typescript'
+import { ActivityPubActor } from '../../../shared/models/activitypub'
+import { VideoChannel } from '../../../shared/models/videos'
+import {
+  isVideoChannelDescriptionValid,
+  isVideoChannelNameValid,
+  isVideoChannelSupportValid
+} from '../../helpers/custom-validators/video-channels'
+import { sendDeleteActor } from '../../lib/activitypub/send'
+import { AccountModel } from '../account/account'
+import { ActorModel } from '../activitypub/actor'
+import { getSort, throwIfNotValid } from '../utils'
+import { VideoModel } from './video'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
+import { AvatarModel } from '../avatar/avatar'
+
+enum ScopeNames {
+  WITH_ACCOUNT = 'WITH_ACCOUNT',
+  WITH_ACTOR = 'WITH_ACTOR',
+  WITH_VIDEOS = 'WITH_VIDEOS'
+}
+
+@DefaultScope({
+  include: [
     {
-      uuid: {
-        type: DataTypes.UUID,
-        defaultValue: DataTypes.UUIDV4,
-        allowNull: false,
-        validate: {
-          isUUID: 4
-        }
-      },
-      name: {
-        type: DataTypes.STRING,
-        allowNull: false,
-        validate: {
-          nameValid: value => {
-            const res = isVideoChannelNameValid(value)
-            if (res === false) throw new Error('Video channel name is not valid.')
-          }
-        }
-      },
-      description: {
-        type: DataTypes.STRING,
-        allowNull: true,
-        validate: {
-          descriptionValid: value => {
-            const res = isVideoChannelDescriptionValid(value)
-            if (res === false) throw new Error('Video channel description is not valid.')
-          }
-        }
-      },
-      remote: {
-        type: DataTypes.BOOLEAN,
-        allowNull: false,
-        defaultValue: false
-      },
-      url: {
-        type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max),
-        allowNull: false,
-        validate: {
-          urlValid: value => {
-            const res = isVideoChannelUrlValid(value)
-            if (res === false) throw new Error('Video channel URL is not valid.')
+      model: () => ActorModel,
+      required: true
+    }
+  ]
+})
+@Scopes({
+  [ScopeNames.WITH_ACCOUNT]: {
+    include: [
+      {
+        model: () => AccountModel.unscoped(),
+        required: true,
+        include: [
+          {
+            model: () => ActorModel.unscoped(),
+            required: true,
+            include: [
+              {
+                model: () => AvatarModel.unscoped(),
+                required: false
+              }
+            ]
           }
-        }
+        ]
       }
+    ]
+  },
+  [ScopeNames.WITH_VIDEOS]: {
+    include: [
+      () => VideoModel
+    ]
+  },
+  [ScopeNames.WITH_ACTOR]: {
+    include: [
+      () => ActorModel
+    ]
+  }
+})
+@Table({
+  tableName: 'videoChannel',
+  indexes: [
+    {
+      fields: [ 'accountId' ]
     },
     {
-      indexes: [
-        {
-          fields: [ 'accountId' ]
-        }
-      ],
-      hooks: {
-        afterDestroy
-      }
+      fields: [ 'actorId' ]
     }
-  )
-
-  const classMethods = [
-    associate,
-
-    listForApi,
-    listByAccount,
-    listOwned,
-    loadByIdAndAccount,
-    loadAndPopulateAccount,
-    loadByUUIDAndPopulateAccount,
-    loadByUUID,
-    loadByHostAndUUID,
-    loadAndPopulateAccountAndVideos,
-    countByAccount,
-    loadByUrl,
-    loadByUUIDOrUrl
-  ]
-  const instanceMethods = [
-    isOwned,
-    toFormattedJSON,
-    toActivityPubObject
   ]
-  addMethodsToModel(VideoChannel, classMethods, instanceMethods)
+})
+export class VideoChannelModel extends Model<VideoChannelModel> {
 
-  return VideoChannel
-}
+  @AllowNull(false)
+  @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
+  @Column
+  name: string
 
-// ------------------------------ METHODS ------------------------------
+  @AllowNull(true)
+  @Default(null)
+  @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description'))
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max))
+  description: string
 
-isOwned = function (this: VideoChannelInstance) {
-  return this.remote === false
-}
+  @AllowNull(true)
+  @Default(null)
+  @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support'))
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max))
+  support: string
 
-toFormattedJSON = function (this: VideoChannelInstance) {
-  const json = {
-    id: this.id,
-    uuid: this.uuid,
-    name: this.name,
-    description: this.description,
-    isLocal: this.isOwned(),
-    createdAt: this.createdAt,
-    updatedAt: this.updatedAt
-  }
+  @CreatedAt
+  createdAt: Date
 
-  if (this.Account !== undefined) {
-    json['owner'] = {
-      name: this.Account.name,
-      uuid: this.Account.uuid
-    }
-  }
+  @UpdatedAt
+  updatedAt: Date
 
-  if (Array.isArray(this.Videos)) {
-    json['videos'] = this.Videos.map(v => v.toFormattedJSON())
-  }
+  @ForeignKey(() => ActorModel)
+  @Column
+  actorId: number
 
-  return json
-}
-
-toActivityPubObject = function (this: VideoChannelInstance) {
-  const json = {
-    type: 'VideoChannel' as 'VideoChannel',
-    id: this.url,
-    uuid: this.uuid,
-    content: this.description,
-    name: this.name,
-    published: this.createdAt,
-    updated: this.updatedAt
-  }
-
-  return json
-}
+  @BelongsTo(() => ActorModel, {
+    foreignKey: {
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
+  Actor: ActorModel
 
-// ------------------------------ STATICS ------------------------------
+  @ForeignKey(() => AccountModel)
+  @Column
+  accountId: number
 
-function associate (models) {
-  VideoChannel.belongsTo(models.Account, {
+  @BelongsTo(() => AccountModel, {
     foreignKey: {
-      name: 'accountId',
       allowNull: false
     },
-    onDelete: 'CASCADE'
+    hooks: true
   })
+  Account: AccountModel
 
-  VideoChannel.hasMany(models.Video, {
+  @HasMany(() => VideoModel, {
     foreignKey: {
       name: 'channelId',
       allowNull: false
     },
-    onDelete: 'CASCADE'
+    onDelete: 'CASCADE',
+    hooks: true
   })
-}
-
-function afterDestroy (videoChannel: VideoChannelInstance) {
-  if (videoChannel.isOwned()) {
-    return sendDeleteVideoChannel(videoChannel, undefined)
-  }
-
-  return undefined
-}
+  Videos: VideoModel[]
 
-countByAccount = function (accountId: number) {
-  const query = {
-    where: {
-      accountId
+  @BeforeDestroy
+  static async sendDeleteIfOwned (instance: VideoChannelModel, options) {
+    if (!instance.Actor) {
+      instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) as ActorModel
     }
-  }
 
-  return VideoChannel.count(query)
-}
+    if (instance.Actor.isOwned()) {
+      return sendDeleteActor(instance.Actor, options.transaction)
+    }
 
-listOwned = function () {
-  const query = {
-    where: {
-      remote: false
-    },
-    include: [ VideoChannel['sequelize'].models.Account ]
+    return undefined
   }
 
-  return VideoChannel.findAll(query)
-}
-
-listForApi = function (start: number, count: number, sort: string) {
-  const query = {
-    offset: start,
-    limit: count,
-    order: [ getSort(sort) ],
-    include: [
-      {
-        model: VideoChannel['sequelize'].models.Account,
-        required: true,
-        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
+  static countByAccount (accountId: number) {
+    const query = {
+      where: {
+        accountId
       }
-    ]
-  }
-
-  return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
-    return { total: count, data: rows }
-  })
-}
+    }
 
-listByAccount = function (accountId: number) {
-  const query = {
-    order: [ getSort('createdAt') ],
-    include: [
-      {
-        model: VideoChannel['sequelize'].models.Account,
-        where: {
-          id: accountId
-        },
-        required: true,
-        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
-      }
-    ]
+    return VideoChannelModel.count(query)
   }
 
-  return VideoChannel.findAndCountAll(query).then(({ rows, count }) => {
-    return { total: count, data: rows }
-  })
-}
-
-loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
-  const query: Sequelize.FindOptions<VideoChannelAttributes> = {
-    where: {
-      uuid
+  static listForApi (start: number, count: number, sort: string) {
+    const query = {
+      offset: start,
+      limit: count,
+      order: getSort(sort)
     }
+
+    return VideoChannelModel
+      .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
+      .findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return { total: count, data: rows }
+      })
   }
 
-  if (t !== undefined) query.transaction = t
+  static listByAccount (accountId: number) {
+    const query = {
+      order: getSort('createdAt'),
+      include: [
+        {
+          model: AccountModel,
+          where: {
+            id: accountId
+          },
+          required: true
+        }
+      ]
+    }
 
-  return VideoChannel.findOne(query)
-}
+    return VideoChannelModel
+      .findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return { total: count, data: rows }
+      })
+  }
 
-loadByUrl = function (url: string, t?: Sequelize.Transaction) {
-  const query: Sequelize.FindOptions<VideoChannelAttributes> = {
-    where: {
-      url
+  static loadByIdAndAccount (id: number, accountId: number) {
+    const options = {
+      where: {
+        id,
+        accountId
+      }
     }
-  }
 
-  if (t !== undefined) query.transaction = t
+    return VideoChannelModel
+      .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
+      .findOne(options)
+  }
 
-  return VideoChannel.findOne(query)
-}
+  static loadAndPopulateAccount (id: number) {
+    return VideoChannelModel
+      .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
+      .findById(id)
+  }
 
-loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) {
-  const query: Sequelize.FindOptions<VideoChannelAttributes> = {
-    where: {
-      [Sequelize.Op.or]: [
-        { uuid },
-        { url }
+  static loadByUUIDAndPopulateAccount (uuid: string) {
+    const options = {
+      include: [
+        {
+          model: ActorModel,
+          required: true,
+          where: {
+            uuid
+          }
+        }
       ]
     }
-  }
 
-  if (t !== undefined) query.transaction = t
-
-  return VideoChannel.findOne(query)
-}
-
-loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
-  const query: Sequelize.FindOptions<VideoChannelAttributes> = {
-    where: {
-      uuid
-    },
-    include: [
-      {
-        model: VideoChannel['sequelize'].models.Account,
-        include: [
-          {
-            model: VideoChannel['sequelize'].models.Pod,
-            required: true,
-            where: {
-              host: fromHost
-            }
-          }
-        ]
-      }
-    ]
+    return VideoChannelModel
+      .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
+      .findOne(options)
   }
 
-  if (t !== undefined) query.transaction = t
-
-  return VideoChannel.findOne(query)
-}
+  static loadAndPopulateAccountAndVideos (id: number) {
+    const options = {
+      include: [
+        VideoModel
+      ]
+    }
 
-loadByIdAndAccount = function (id: number, accountId: number) {
-  const options = {
-    where: {
-      id,
-      accountId
-    },
-    include: [
-      {
-        model: VideoChannel['sequelize'].models.Account,
-        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
-      }
-    ]
+    return VideoChannelModel
+      .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
+      .findById(id, options)
   }
 
-  return VideoChannel.findOne(options)
-}
+  static loadLocalByName (name: string) {
+    const query = {
+      include: [
+        {
+          model: ActorModel,
+          required: true,
+          where: {
+            preferredUsername: name,
+            serverId: null
+          }
+        }
+      ]
+    }
 
-loadAndPopulateAccount = function (id: number) {
-  const options = {
-    include: [
-      {
-        model: VideoChannel['sequelize'].models.Account,
-        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
-      }
-    ]
+    return VideoChannelModel.findOne(query)
   }
 
-  return VideoChannel.findById(id, options)
-}
+  toFormattedJSON (): VideoChannel {
+    const actor = this.Actor.toFormattedJSON()
+    const videoChannel = {
+      id: this.id,
+      displayName: this.getDisplayName(),
+      description: this.description,
+      support: this.support,
+      isLocal: this.Actor.isOwned(),
+      createdAt: this.createdAt,
+      updatedAt: this.updatedAt,
+      ownerAccount: undefined
+    }
 
-loadByUUIDAndPopulateAccount = function (uuid: string) {
-  const options = {
-    where: {
-      uuid
-    },
-    include: [
-      {
-        model: VideoChannel['sequelize'].models.Account,
-        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
-      }
-    ]
+    if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
+
+    return Object.assign(actor, videoChannel)
   }
 
-  return VideoChannel.findOne(options)
-}
+  toActivityPubObject (): ActivityPubActor {
+    const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
 
-loadAndPopulateAccountAndVideos = function (id: number) {
-  const options = {
-    include: [
-      {
-        model: VideoChannel['sequelize'].models.Account,
-        include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ]
-      },
-      VideoChannel['sequelize'].models.Video
-    ]
+    return Object.assign(obj, {
+      summary: this.description,
+      support: this.support,
+      attributedTo: [
+        {
+          type: 'Person' as 'Person',
+          id: this.Account.Actor.url
+        }
+      ]
+    })
   }
 
-  return VideoChannel.findById(id, options)
+  getDisplayName () {
+    return this.name
+  }
 }