X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Factor%2Factor.ts;h=a62e6030af835e0f513d39984fda4b4a18f10063;hb=eb66ee88351a93eb68c366cfbe30d35ed7c57b03;hp=5cf6fb8f16810dc3b5692a9023847246c9037211;hpb=7d9ba5c08999c6482f0bc5e0c09c6f55b7724090;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index 5cf6fb8f1..a62e6030a 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts @@ -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)[] = [ '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>> { @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 { + // --------------------------------------------------------------------------- + + 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 { + 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 { - 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(query, options) } static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise { @@ -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 { + static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise { 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 () {