]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/actor/actor.ts
Refactor table attributes
[github/Chocobozzz/PeerTube.git] / server / models / actor / actor.ts
index 5cf6fb8f16810dc3b5692a9023847246c9037211..a62e6030af835e0f513d39984fda4b4a18f10063 100644 (file)
@@ -1,6 +1,4 @@
-import { values } from 'lodash'
-import { extname } from 'path'
-import { literal, Op, Transaction } from 'sequelize'
+import { literal, Op, QueryTypes, Transaction } from 'sequelize'
 import {
   AllowNull,
   BelongsTo,
@@ -17,10 +15,12 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { activityPubContextify } from '@server/lib/activitypub/context'
+import { getBiggestActorImage } from '@server/lib/actor-image'
 import { ModelCache } from '@server/models/model-cache'
-import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
-import { ActorImage } from '../../../shared/models/actors/actor-image.model'
-import { activityPubContextify } from '../../helpers/activitypub'
+import { forceNumber, getLowercaseExtension } from '@shared/core-utils'
+import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
+import { AttributesOnly } from '@shared/typescript-utils'
 import {
   isActorFollowersCountValid,
   isActorFollowingCountValid,
@@ -42,17 +42,20 @@ import {
   MActorAccountChannelId,
   MActorAPAccount,
   MActorAPChannel,
+  MActorFollowersUrl,
   MActorFormattable,
   MActorFull,
   MActorHost,
+  MActorId,
   MActorServer,
   MActorSummaryFormattable,
   MActorUrl,
   MActorWithInboxes
 } from '../../types/models'
 import { AccountModel } from '../account/account'
+import { getServerActor } from '../application/application'
 import { ServerModel } from '../server/server'
-import { isOutdated, throwIfNotValid } from '../utils'
+import { buildSQLAttributes, isOutdated, throwIfNotValid } from '../utils'
 import { VideoModel } from '../video/video'
 import { VideoChannelModel } from '../video/video-channel'
 import { ActorFollowModel } from './actor-follow'
@@ -62,7 +65,7 @@ enum ScopeNames {
   FULL = 'FULL'
 }
 
-export const unusedActorAttributesForAPI = [
+export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] = [
   'publicKey',
   'privateKey',
   'inboxUrl',
@@ -80,7 +83,7 @@ export const unusedActorAttributesForAPI = [
     },
     {
       model: ActorImageModel,
-      as: 'Avatar',
+      as: 'Avatars',
       required: false
     }
   ]
@@ -108,12 +111,12 @@ export const unusedActorAttributesForAPI = [
       },
       {
         model: ActorImageModel,
-        as: 'Avatar',
+        as: 'Avatars',
         required: false
       },
       {
         model: ActorImageModel,
-        as: 'Banner',
+        as: 'Banners',
         required: false
       }
     ]
@@ -151,18 +154,15 @@ export const unusedActorAttributesForAPI = [
     {
       fields: [ 'serverId' ]
     },
-    {
-      fields: [ 'avatarId' ]
-    },
     {
       fields: [ 'followersUrl' ]
     }
   ]
 })
-export class ActorModel extends Model {
+export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
 
   @AllowNull(false)
-  @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
+  @Column(DataType.ENUM(...Object.values(ACTIVITY_PUB_ACTOR_TYPES)))
   type: ActivityPubActorType
 
   @AllowNull(false)
@@ -230,35 +230,31 @@ export class ActorModel extends Model {
   @UpdatedAt
   updatedAt: Date
 
-  @ForeignKey(() => ActorImageModel)
-  @Column
-  avatarId: number
-
-  @ForeignKey(() => ActorImageModel)
-  @Column
-  bannerId: number
-
-  @BelongsTo(() => ActorImageModel, {
+  @HasMany(() => ActorImageModel, {
+    as: 'Avatars',
+    onDelete: 'cascade',
+    hooks: true,
     foreignKey: {
-      name: 'avatarId',
-      allowNull: true
+      allowNull: false
     },
-    as: 'Avatar',
-    onDelete: 'set null',
-    hooks: true
+    scope: {
+      type: ActorImageType.AVATAR
+    }
   })
-  Avatar: ActorImageModel
+  Avatars: ActorImageModel[]
 
-  @BelongsTo(() => ActorImageModel, {
+  @HasMany(() => ActorImageModel, {
+    as: 'Banners',
+    onDelete: 'cascade',
+    hooks: true,
     foreignKey: {
-      name: 'bannerId',
-      allowNull: true
+      allowNull: false
     },
-    as: 'Banner',
-    onDelete: 'set null',
-    hooks: true
+    scope: {
+      type: ActorImageType.BANNER
+    }
   })
-  Banner: ActorImageModel
+  Banners: ActorImageModel[]
 
   @HasMany(() => ActorFollowModel, {
     foreignKey: {
@@ -310,7 +306,31 @@ export class ActorModel extends Model {
   })
   VideoChannel: VideoChannelModel
 
-  static load (id: number): Promise<MActor> {
+  // ---------------------------------------------------------------------------
+
+  static getSQLAttributes (tableName: string, aliasPrefix = '') {
+    return buildSQLAttributes({
+      model: this,
+      tableName,
+      aliasPrefix
+    })
+  }
+
+  static getSQLAPIAttributes (tableName: string, aliasPrefix = '') {
+    return buildSQLAttributes({
+      model: this,
+      tableName,
+      aliasPrefix,
+      excludeAttributes: unusedActorAttributesForAPI
+    })
+  }
+
+  // ---------------------------------------------------------------------------
+
+  static async load (id: number): Promise<MActor> {
+    const actorServer = await getServerActor()
+    if (id === actorServer.id) return actorServer
+
     return ActorModel.unscoped().findByPk(id)
   }
 
@@ -318,48 +338,21 @@ export class ActorModel extends Model {
     return ActorModel.scope(ScopeNames.FULL).findByPk(id)
   }
 
-  static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
-    const query = {
-      include: [
-        {
-          attributes: [ 'id' ],
-          model: AccountModel.unscoped(),
-          required: true,
-          include: [
-            {
-              attributes: [ 'id' ],
-              model: VideoChannelModel.unscoped(),
-              required: true,
-              include: [
-                {
-                  attributes: [ 'id' ],
-                  model: VideoModel.unscoped(),
-                  required: true,
-                  where: {
-                    id: videoId
-                  }
-                }
-              ]
-            }
-          ]
-        }
-      ],
+  static loadAccountActorFollowerUrlByVideoId (videoId: number, transaction: Transaction) {
+    const query = `SELECT "actor"."id" AS "id", "actor"."followersUrl" AS "followersUrl" ` +
+                  `FROM "actor" ` +
+                  `INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
+                  `INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
+                  `INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
+
+    const options = {
+      type: QueryTypes.SELECT as QueryTypes.SELECT,
+      replacements: { videoId },
+      plain: true as true,
       transaction
     }
 
-    return ActorModel.unscoped().findOne(query)
-  }
-
-  static isActorUrlExist (url: string) {
-    const query = {
-      raw: true,
-      where: {
-        url
-      }
-    }
-
-    return ActorModel.unscoped().findOne(query)
-      .then(a => !!a)
+    return ActorModel.sequelize.query<MActorId & MActorFollowersUrl>(query, options)
   }
 
   static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
@@ -385,8 +378,7 @@ export class ActorModel extends Model {
         transaction
       }
 
-      return ActorModel.scope(ScopeNames.FULL)
-                       .findOne(query)
+      return ActorModel.scope(ScopeNames.FULL).findOne(query)
     }
 
     return ModelCache.Instance.doCache({
@@ -409,8 +401,7 @@ export class ActorModel extends Model {
         transaction
       }
 
-      return ActorModel.unscoped()
-                       .findOne(query)
+      return ActorModel.unscoped().findOne(query)
     }
 
     return ModelCache.Instance.doCache({
@@ -476,7 +467,7 @@ export class ActorModel extends Model {
   }
 
   static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
-    const sanitizedOfId = parseInt(ofId + '', 10)
+    const sanitizedOfId = forceNumber(ofId)
     const where = { id: sanitizedOfId }
 
     let columnToUpdate: string
@@ -491,11 +482,11 @@ export class ActorModel extends Model {
     }
 
     return ActorModel.update({
-      [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
+      [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId} AND "state" = 'accepted')`)
     }, { where, transaction })
   }
 
-  static loadAccountActorByVideoId (videoId: number): Promise<MActor> {
+  static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
     const query = {
       include: [
         {
@@ -519,7 +510,8 @@ export class ActorModel extends Model {
             }
           ]
         }
-      ]
+      ],
+      transaction
     }
 
     return ActorModel.unscoped().findOne(query)
@@ -530,63 +522,58 @@ export class ActorModel extends Model {
   }
 
   toFormattedSummaryJSON (this: MActorSummaryFormattable) {
-    let avatar: ActorImage = null
-    if (this.Avatar) {
-      avatar = this.Avatar.toFormattedJSON()
-    }
-
     return {
       url: this.url,
       name: this.preferredUsername,
       host: this.getHost(),
-      avatar
+      avatars: (this.Avatars || []).map(a => a.toFormattedJSON()),
+
+      // TODO: remove, deprecated in 4.2
+      avatar: this.hasImage(ActorImageType.AVATAR)
+        ? this.Avatars[0].toFormattedJSON()
+        : undefined
     }
   }
 
   toFormattedJSON (this: MActorFormattable) {
-    const base = this.toFormattedSummaryJSON()
-
-    let banner: ActorImage = null
-    if (this.Banner) {
-      banner = this.Banner.toFormattedJSON()
-    }
+    return {
+      ...this.toFormattedSummaryJSON(),
 
-    return Object.assign(base, {
       id: this.id,
       hostRedundancyAllowed: this.getRedundancyAllowed(),
       followingCount: this.followingCount,
       followersCount: this.followersCount,
-      banner,
-      createdAt: this.getCreatedAt()
-    })
+      createdAt: this.getCreatedAt(),
+
+      banners: (this.Banners || []).map(b => b.toFormattedJSON()),
+
+      // TODO: remove, deprecated in 4.2
+      banner: this.hasImage(ActorImageType.BANNER)
+        ? this.Banners[0].toFormattedJSON()
+        : undefined
+    }
   }
 
   toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
     let icon: ActivityIconObject
+    let icons: ActivityIconObject[]
     let image: ActivityIconObject
 
-    if (this.avatarId) {
-      const extension = extname(this.Avatar.filename)
-
-      icon = {
-        type: 'Image',
-        mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
-        height: this.Avatar.height,
-        width: this.Avatar.width,
-        url: this.getAvatarUrl()
-      }
+    if (this.hasImage(ActorImageType.AVATAR)) {
+      icon = getBiggestActorImage(this.Avatars).toActivityPubObject()
+      icons = this.Avatars.map(a => a.toActivityPubObject())
     }
 
-    if (this.bannerId) {
-      const banner = (this as MActorAPChannel).Banner
-      const extension = extname(banner.filename)
+    if (this.hasImage(ActorImageType.BANNER)) {
+      const banner = getBiggestActorImage((this as MActorAPChannel).Banners)
+      const extension = getLowercaseExtension(banner.filename)
 
       image = {
         type: 'Image',
         mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
         height: banner.height,
         width: banner.width,
-        url: this.getBannerUrl()
+        url: ActorImageModel.getImageUrl(banner)
       }
     }
 
@@ -610,11 +597,14 @@ export class ActorModel extends Model {
         publicKeyPem: this.publicKey
       },
       published: this.getCreatedAt().toISOString(),
+
       icon,
+      icons,
+
       image
     }
 
-    return activityPubContextify(json)
+    return activityPubContextify(json, 'Actor')
   }
 
   getFollowerSharedInboxUrls (t: Transaction) {
@@ -675,16 +665,12 @@ export class ActorModel extends Model {
     return this.Server ? this.Server.redundancyAllowed : false
   }
 
-  getAvatarUrl () {
-    if (!this.avatarId) return undefined
-
-    return WEBSERVER.URL + this.Avatar.getStaticPath()
-  }
-
-  getBannerUrl () {
-    if (!this.bannerId) return undefined
+  hasImage (type: ActorImageType) {
+    const images = type === ActorImageType.AVATAR
+      ? this.Avatars
+      : this.Banners
 
-    return WEBSERVER.URL + this.Banner.getStaticPath()
+    return Array.isArray(images) && images.length !== 0
   }
 
   isOutdated () {