]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video-channel.ts
Update client dependencies
[github/Chocobozzz/PeerTube.git] / server / models / video / video-channel.ts
index 8c4357009dc91a430cde32e3221887c76b71b255..2c6669bcba9262ef9700502b0b186c47f597b0f3 100644 (file)
@@ -1,4 +1,4 @@
-import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize'
+import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
 import {
   AllowNull,
   BeforeDestroy,
@@ -17,14 +17,15 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { setAsUpdated } from '@server/helpers/database-utils'
+import { CONFIG } from '@server/initializers/config'
 import { MAccountActor } from '@server/types/models'
-import { AttributesOnly } from '@shared/core-utils'
+import { pick } from '@shared/core-utils'
+import { AttributesOnly } from '@shared/typescript-utils'
 import { ActivityPubActor } from '../../../shared/models/activitypub'
 import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
 import {
   isVideoChannelDescriptionValid,
-  isVideoChannelNameValid,
+  isVideoChannelDisplayNameValid,
   isVideoChannelSupportValid
 } from '../../helpers/custom-validators/video-channels'
 import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
@@ -41,6 +42,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
 import { ActorFollowModel } from '../actor/actor-follow'
 import { ActorImageModel } from '../actor/actor-image'
 import { ServerModel } from '../server/server'
+import { setAsUpdated } from '../shared'
 import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoPlaylistModel } from './video-playlist'
@@ -58,6 +60,8 @@ export enum ScopeNames {
 type AvailableForListOptions = {
   actorId: number
   search?: string
+  host?: string
+  handles?: string[]
 }
 
 type AvailableWithStatsOptions = {
@@ -83,7 +87,62 @@ export type SummaryOptions = {
     // Only list local channels OR channels that are on an instance followed by actorId
     const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
 
+    const whereActorAnd: WhereOptions[] = [
+      {
+        [Op.or]: [
+          {
+            serverId: null
+          },
+          {
+            serverId: {
+              [Op.in]: Sequelize.literal(inQueryInstanceFollow)
+            }
+          }
+        ]
+      }
+    ]
+
+    let serverRequired = false
+    let whereServer: WhereOptions
+
+    if (options.host && options.host !== WEBSERVER.HOST) {
+      serverRequired = true
+      whereServer = { host: options.host }
+    }
+
+    if (options.host === WEBSERVER.HOST) {
+      whereActorAnd.push({
+        serverId: null
+      })
+    }
+
+    let rootWhere: WhereOptions
+    if (options.handles) {
+      const or: WhereOptions[] = []
+
+      for (const handle of options.handles || []) {
+        const [ preferredUsername, host ] = handle.split('@')
+
+        if (!host || host === WEBSERVER.HOST) {
+          or.push({
+            '$Actor.preferredUsername$': preferredUsername,
+            '$Actor.serverId$': null
+          })
+        } else {
+          or.push({
+            '$Actor.preferredUsername$': preferredUsername,
+            '$Actor.Server.host$': host
+          })
+        }
+      }
+
+      rootWhere = {
+        [Op.or]: or
+      }
+    }
+
     return {
+      where: rootWhere,
       include: [
         {
           attributes: {
@@ -91,18 +150,19 @@ export type SummaryOptions = {
           },
           model: ActorModel,
           where: {
-            [Op.or]: [
-              {
-                serverId: null
-              },
-              {
-                serverId: {
-                  [Op.in]: Sequelize.literal(inQueryInstanceFollow)
-                }
-              }
-            ]
+            [Op.and]: whereActorAnd
           },
           include: [
+            {
+              model: ServerModel,
+              required: serverRequired,
+              where: whereServer
+            },
+            {
+              model: ActorImageModel,
+              as: 'Avatar',
+              required: false
+            },
             {
               model: ActorImageModel,
               as: 'Banner',
@@ -250,7 +310,7 @@ export type SummaryOptions = {
 export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> {
 
   @AllowNull(false)
-  @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
+  @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'name'))
   @Column
   name: string
 
@@ -291,8 +351,7 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel
   @BelongsTo(() => AccountModel, {
     foreignKey: {
       allowNull: false
-    },
-    hooks: true
+    }
   })
   Account: AccountModel
 
@@ -381,30 +440,6 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
     }
   }
 
-  static listForApi (parameters: {
-    actorId: number
-    start: number
-    count: number
-    sort: string
-  }) {
-    const { actorId } = parameters
-
-    const query = {
-      offset: parameters.start,
-      limit: parameters.count,
-      order: getSort(parameters.sort)
-    }
-
-    return VideoChannelModel
-      .scope({
-        method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
-      })
-      .findAndCountAll(query)
-      .then(({ rows, count }) => {
-        return { total: count, data: rows }
-      })
-  }
-
   static listLocalsForSitemap (sort: string): Promise<MChannelActor[]> {
     const query = {
       attributes: [ ],
@@ -426,26 +461,43 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       .findAll(query)
   }
 
-  static searchForApi (options: {
-    actorId: number
-    search: string
+  static listForApi (parameters: Pick<AvailableForListOptions, 'actorId'> & {
     start: number
     count: number
     sort: string
   }) {
-    const attributesInclude = []
-    const escapedSearch = VideoModel.sequelize.escape(options.search)
-    const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
-    attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
+    const { actorId } = parameters
 
     const query = {
-      attributes: {
-        include: attributesInclude
-      },
-      offset: options.start,
-      limit: options.count,
-      order: getSort(options.sort),
-      where: {
+      offset: parameters.start,
+      limit: parameters.count,
+      order: getSort(parameters.sort)
+    }
+
+    return VideoChannelModel
+      .scope({
+        method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
+      })
+      .findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return { total: count, data: rows }
+      })
+  }
+
+  static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
+    start: number
+    count: number
+    sort: string
+  }) {
+    let attributesInclude: any[] = [ literal('0 as similarity') ]
+    let where: WhereOptions
+
+    if (options.search) {
+      const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
+      const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%')
+      attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ]
+
+      where = {
         [Op.or]: [
           Sequelize.literal(
             'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
@@ -457,9 +509,19 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       }
     }
 
+    const query = {
+      attributes: {
+        include: attributesInclude
+      },
+      offset: options.start,
+      limit: options.count,
+      order: getSort(options.sort),
+      where
+    }
+
     return VideoChannelModel
       .scope({
-        method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ]
+        method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ]
       })
       .findAndCountAll(query)
       .then(({ rows, count }) => {
@@ -467,7 +529,7 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       })
   }
 
-  static listByAccount (options: {
+  static listByAccountForAPI (options: {
     accountId: number
     start: number
     count: number
@@ -522,10 +584,28 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       })
   }
 
-  static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> {
+  static listAllByAccount (accountId: number) {
+    const query = {
+      limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER,
+      include: [
+        {
+          attributes: [],
+          model: AccountModel,
+          where: {
+            id: accountId
+          },
+          required: true
+        }
+      ]
+    }
+
+    return VideoChannelModel.findAll(query)
+  }
+
+  static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> {
     return VideoChannelModel.unscoped()
       .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
-      .findByPk(id)
+      .findByPk(id, { transaction })
   }
 
   static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> {
@@ -693,7 +773,7 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
     return this.Actor.isOutdated()
   }
 
-  setAsUpdated (transaction: Transaction) {
+  setAsUpdated (transaction?: Transaction) {
     return setAsUpdated('videoChannel', this.id, transaction)
   }
 }