]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/video/video-channel.ts
Implement avatar miniatures (#4639)
[github/Chocobozzz/PeerTube.git] / server / models / video / video-channel.ts
index 9f04a57c64025f11baa6e9d02fee6f4ef1208141..410fd6d3f777c4e15c2bb0f509eb2012eb0210de 100644 (file)
@@ -17,18 +17,21 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { CONFIG } from '@server/initializers/config'
 import { MAccountActor } from '@server/types/models'
-import { AttributesOnly, pick } 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'
 import { sendDeleteActor } from '../../lib/activitypub/send'
 import {
+  MChannel,
   MChannelActor,
   MChannelAP,
   MChannelBannerAccountDefault,
@@ -60,6 +63,7 @@ type AvailableForListOptions = {
   search?: string
   host?: string
   handles?: string[]
+  forCount?: boolean
 }
 
 type AvailableWithStatsOptions = {
@@ -114,70 +118,91 @@ export type SummaryOptions = {
       })
     }
 
-    let rootWhere: WhereOptions
-    if (options.handles) {
-      const or: WhereOptions[] = []
+    if (Array.isArray(options.handles) && options.handles.length !== 0) {
+      const or: string[] = []
 
       for (const handle of options.handles || []) {
         const [ preferredUsername, host ] = handle.split('@')
 
-        if (!host) {
-          or.push({
-            '$Actor.preferredUsername$': preferredUsername,
-            '$Actor.serverId$': null
-          })
+        if (!host || host === WEBSERVER.HOST) {
+          or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`)
         } else {
-          or.push({
-            '$Actor.preferredUsername$': preferredUsername,
-            '$Actor.Server.host$': host
-          })
+          or.push(
+            `(` +
+              `"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` +
+              `AND "host" = ${VideoChannelModel.sequelize.escape(host)}` +
+            `)`
+          )
         }
       }
 
-      rootWhere = {
-        [Op.or]: or
-      }
+      whereActorAnd.push({
+        id: {
+          [Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`)
+        }
+      })
+    }
+
+    const channelInclude: Includeable[] = []
+    const accountInclude: Includeable[] = []
+
+    if (options.forCount !== true) {
+      accountInclude.push({
+        model: ServerModel,
+        required: false
+      })
+
+      accountInclude.push({
+        model: ActorImageModel,
+        as: 'Avatars',
+        required: false
+      })
+
+      channelInclude.push({
+        model: ActorImageModel,
+        as: 'Avatars',
+        required: false
+      })
+
+      channelInclude.push({
+        model: ActorImageModel,
+        as: 'Banners',
+        required: false
+      })
+    }
+
+    if (options.forCount !== true || serverRequired) {
+      channelInclude.push({
+        model: ServerModel,
+        duplicating: false,
+        required: serverRequired,
+        where: whereServer
+      })
     }
 
     return {
-      where: rootWhere,
       include: [
         {
           attributes: {
             exclude: unusedActorAttributesForAPI
           },
-          model: ActorModel,
+          model: ActorModel.unscoped(),
           where: {
             [Op.and]: whereActorAnd
           },
-          include: [
-            {
-              model: ServerModel,
-              required: serverRequired,
-              where: whereServer
-            },
-            {
-              model: ActorImageModel,
-              as: 'Avatar',
-              required: false
-            },
-            {
-              model: ActorImageModel,
-              as: 'Banner',
-              required: false
-            }
-          ]
+          include: channelInclude
         },
         {
-          model: AccountModel,
+          model: AccountModel.unscoped(),
           required: true,
           include: [
             {
               attributes: {
                 exclude: unusedActorAttributesForAPI
               },
-              model: ActorModel, // Default scope includes avatar and server
-              required: true
+              model: ActorModel.unscoped(),
+              required: true,
+              include: accountInclude
             }
           ]
         }
@@ -187,7 +212,7 @@ export type SummaryOptions = {
   [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
     const include: Includeable[] = [
       {
-        attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
+        attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ],
         model: ActorModel.unscoped(),
         required: options.actorRequired ?? true,
         include: [
@@ -197,8 +222,8 @@ export type SummaryOptions = {
             required: false
           },
           {
-            model: ActorImageModel.unscoped(),
-            as: 'Avatar',
+            model: ActorImageModel,
+            as: 'Avatars',
             required: false
           }
         ]
@@ -243,7 +268,7 @@ export type SummaryOptions = {
           {
             model: ActorImageModel,
             required: false,
-            as: 'Banner'
+            as: 'Banners'
           }
         ]
       }
@@ -308,7 +333,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
 
@@ -472,14 +497,14 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       order: getSort(parameters.sort)
     }
 
-    return VideoChannelModel
-      .scope({
-        method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
-      })
-      .findAndCountAll(query)
-      .then(({ rows, count }) => {
-        return { total: count, data: rows }
-      })
+    const getScope = (forCount: boolean) => {
+      return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] }
+    }
+
+    return Promise.all([
+      VideoChannelModel.scope(getScope(true)).count(),
+      VideoChannelModel.scope(getScope(false)).findAll(query)
+    ]).then(([ total, data ]) => ({ total, data }))
   }
 
   static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
@@ -517,17 +542,25 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       where
     }
 
-    return VideoChannelModel
-      .scope({
-        method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ]
-      })
-      .findAndCountAll(query)
-      .then(({ rows, count }) => {
-        return { total: count, data: rows }
-      })
+    const getScope = (forCount: boolean) => {
+      return {
+        method: [
+          ScopeNames.FOR_API, {
+            ...pick(options, [ 'actorId', 'host', 'handles' ]),
+
+            forCount
+          } as AvailableForListOptions
+        ]
+      }
+    }
+
+    return Promise.all([
+      VideoChannelModel.scope(getScope(true)).count(query),
+      VideoChannelModel.scope(getScope(false)).findAll(query)
+    ]).then(([ total, data ]) => ({ total, data }))
   }
 
-  static listByAccount (options: {
+  static listByAccountForAPI (options: {
     accountId: number
     start: number
     count: number
@@ -550,20 +583,26 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       }
       : null
 
-    const query = {
-      offset: options.start,
-      limit: options.count,
-      order: getSort(options.sort),
-      include: [
-        {
-          model: AccountModel,
-          where: {
-            id: options.accountId
-          },
-          required: true
-        }
-      ],
-      where
+    const getQuery = (forCount: boolean) => {
+      const accountModel = forCount
+        ? AccountModel.unscoped()
+        : AccountModel
+
+      return {
+        offset: options.start,
+        limit: options.count,
+        order: getSort(options.sort),
+        include: [
+          {
+            model: accountModel,
+            where: {
+              id: options.accountId
+            },
+            required: true
+          }
+        ],
+        where
+      }
     }
 
     const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
@@ -574,12 +613,28 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       })
     }
 
-    return VideoChannelModel
-      .scope(scopes)
-      .findAndCountAll(query)
-      .then(({ rows, count }) => {
-        return { total: count, data: rows }
-      })
+    return Promise.all([
+      VideoChannelModel.scope(scopes).count(getQuery(true)),
+      VideoChannelModel.scope(scopes).findAll(getQuery(false))
+    ]).then(([ total, data ]) => ({ total, data }))
+  }
+
+  static listAllByAccount (accountId: number): Promise<MChannel[]> {
+    const query = {
+      limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER,
+      include: [
+        {
+          attributes: [],
+          model: AccountModel.unscoped(),
+          where: {
+            id: accountId
+          },
+          required: true
+        }
+      ]
+    }
+
+    return VideoChannelModel.findAll(query)
   }
 
   static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> {
@@ -601,7 +656,7 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
             {
               model: ActorImageModel,
               required: false,
-              as: 'Banner'
+              as: 'Banners'
             }
           ]
         }
@@ -635,7 +690,7 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
             {
               model: ActorImageModel,
               required: false,
-              as: 'Banner'
+              as: 'Banners'
             }
           ]
         }
@@ -665,7 +720,7 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
             {
               model: ActorImageModel,
               required: false,
-              as: 'Banner'
+              as: 'Banners'
             }
           ]
         }
@@ -686,6 +741,9 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       displayName: this.getDisplayName(),
       url: actor.url,
       host: actor.host,
+      avatars: actor.avatars,
+
+      // TODO: remove, deprecated in 4.2
       avatar: actor.avatar
     }
   }
@@ -716,9 +774,16 @@ ON              "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
       support: this.support,
       isLocal: this.Actor.isOwned(),
       updatedAt: this.updatedAt,
+
       ownerAccount: undefined,
+
       videosCount,
-      viewsPerDay
+      viewsPerDay,
+
+      avatars: actor.avatars,
+
+      // TODO: remove, deprecated in 4.2
+      avatar: actor.avatar
     }
 
     if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
@@ -753,7 +818,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)
   }
 }