X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Factivitypub%2Factor.ts;h=09d96b24dc29fd028d6328fba8e0ddbe2a1faa75;hb=f479685678406a5df864d89615b33d29085ebfc6;hp=4a466441cd98a73c034ace260279a884d38aeb34;hpb=97567dd81f508dd6295ac4d73d849aa2ce0a6549;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 4a466441c..09d96b24d 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -1,26 +1,25 @@ import { values } from 'lodash' import { extname } from 'path' -import * as Sequelize from 'sequelize' +import { literal, Op, Transaction } from 'sequelize' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, - Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, - IsUUID, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' -import { ActivityPubActorType } from '../../../shared/models/activitypub' -import { Avatar } from '../../../shared/models/avatars/avatar.model' +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 { isActorFollowersCountValid, @@ -30,14 +29,26 @@ import { isActorPublicKeyValid } from '../../helpers/custom-validators/activitypub/actor' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' +import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' +import { + MActor, + MActorAccountChannelId, + MActorAP, + MActorFormattable, + MActorFull, + MActorHost, + MActorServer, + MActorSummaryFormattable, + MActorUrl, + MActorWithInboxes +} from '../../types/models' import { AccountModel } from '../account/account' -import { AvatarModel } from '../avatar/avatar' +import { ActorImageModel } from '../account/actor-image' import { ServerModel } from '../server/server' import { isOutdated, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' import { ActorFollowModel } from './actor-follow' -import { VideoModel } from '../video/video' enum ScopeNames { FULL = 'FULL' @@ -51,7 +62,6 @@ export const unusedActorAttributesForAPI = [ 'sharedInboxUrl', 'followersUrl', 'followingUrl', - 'url', 'createdAt', 'updatedAt' ] @@ -63,7 +73,8 @@ export const unusedActorAttributesForAPI = [ required: false }, { - model: AvatarModel, + model: ActorImageModel, + as: 'Avatar', required: false } ] @@ -90,7 +101,8 @@ export const unusedActorAttributesForAPI = [ required: false }, { - model: AvatarModel, + model: ActorImageModel, + as: 'Avatar', required: false } ] @@ -105,7 +117,19 @@ export const unusedActorAttributesForAPI = [ }, { fields: [ 'preferredUsername', 'serverId' ], - unique: true + unique: true, + where: { + serverId: { + [Op.ne]: null + } + } + }, + { + fields: [ 'preferredUsername' ], + unique: true, + where: { + serverId: null + } }, { fields: [ 'inboxUrl', 'sharedInboxUrl' ] @@ -119,27 +143,17 @@ export const unusedActorAttributesForAPI = [ { fields: [ 'avatarId' ] }, - { - fields: [ 'uuid' ], - unique: true - }, { fields: [ 'followersUrl' ] } ] }) -export class ActorModel extends Model { +export class ActorModel extends Model { @AllowNull(false) @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) type: ActivityPubActorType - @AllowNull(false) - @Default(DataType.UUIDV4) - @IsUUID(4) - @Column(DataType.UUID) - uuid: string - @AllowNull(false) @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username')) @Column @@ -175,23 +189,23 @@ export class ActorModel extends Model { @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) inboxUrl: string - @AllowNull(false) - @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url')) + @AllowNull(true) + @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) outboxUrl: string - @AllowNull(false) - @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url')) + @AllowNull(true) + @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) sharedInboxUrl: string - @AllowNull(false) - @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url')) + @AllowNull(true) + @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) followersUrl: string - @AllowNull(false) - @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url')) + @AllowNull(true) + @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) followingUrl: string @@ -201,18 +215,35 @@ export class ActorModel extends Model { @UpdatedAt updatedAt: Date - @ForeignKey(() => AvatarModel) + @ForeignKey(() => ActorImageModel) @Column avatarId: number - @BelongsTo(() => AvatarModel, { + @ForeignKey(() => ActorImageModel) + @Column + bannerId: number + + @BelongsTo(() => ActorImageModel, { foreignKey: { + name: 'avatarId', allowNull: true }, + as: 'Avatar', onDelete: 'set null', hooks: true }) - Avatar: AvatarModel + Avatar: ActorImageModel + + @BelongsTo(() => ActorImageModel, { + foreignKey: { + name: 'bannerId', + allowNull: true + }, + as: 'Banner', + onDelete: 'set null', + hooks: true + }) + Banner: ActorImageModel @HasMany(() => ActorFollowModel, { foreignKey: { @@ -264,11 +295,15 @@ export class ActorModel extends Model { }) VideoChannel: VideoChannelModel - static load (id: number) { + static load (id: number): Promise { return ActorModel.unscoped().findByPk(id) } - static loadAccountActorByVideoId (videoId: number, transaction: Sequelize.Transaction) { + static loadFull (id: number): Promise { + return ActorModel.scope(ScopeNames.FULL).findByPk(id) + } + + static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise { const query = { include: [ { @@ -312,11 +347,11 @@ export class ActorModel extends Model { .then(a => !!a) } - static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { + static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise { const query = { where: { followersUrl: { - [ Sequelize.Op.in ]: followersUrls + [Op.in]: followersUrls } }, transaction @@ -325,19 +360,54 @@ export class ActorModel extends Model { return ActorModel.scope(ScopeNames.FULL).findAll(query) } - static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction) { - const query = { - where: { - preferredUsername, - serverId: null - }, - transaction + static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise { + const fun = () => { + const query = { + where: { + preferredUsername, + serverId: null + }, + transaction + } + + return ActorModel.scope(ScopeNames.FULL) + .findOne(query) } - return ActorModel.scope(ScopeNames.FULL).findOne(query) + return ModelCache.Instance.doCache({ + cacheType: 'local-actor-name', + key: preferredUsername, + // The server actor never change, so we can easily cache it + whitelist: () => preferredUsername === SERVER_ACTOR_NAME, + fun + }) + } + + static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise { + const fun = () => { + const query = { + attributes: [ 'url' ], + where: { + preferredUsername, + serverId: null + }, + transaction + } + + return ActorModel.unscoped() + .findOne(query) + } + + return ModelCache.Instance.doCache({ + cacheType: 'local-actor-name', + key: preferredUsername, + // The server actor never change, so we can easily cache it + whitelist: () => preferredUsername === SERVER_ACTOR_NAME, + fun + }) } - static loadByNameAndHost (preferredUsername: string, host: string) { + static loadByNameAndHost (preferredUsername: string, host: string): Promise { const query = { where: { preferredUsername @@ -356,7 +426,7 @@ export class ActorModel extends Model { return ActorModel.scope(ScopeNames.FULL).findOne(query) } - static loadByUrl (url: string, transaction?: Sequelize.Transaction) { + static loadByUrl (url: string, transaction?: Transaction): Promise { const query = { where: { url @@ -379,7 +449,7 @@ export class ActorModel extends Model { return ActorModel.unscoped().findOne(query) } - static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction) { + static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise { const query = { where: { url @@ -390,49 +460,93 @@ export class ActorModel extends Model { return ActorModel.scope(ScopeNames.FULL).findOne(query) } - static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) { - return ActorModel.increment(column, { - by, - where: { - id - } - }) + static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) { + const sanitizedOfId = parseInt(ofId + '', 10) + const where = { id: sanitizedOfId } + + let columnToUpdate: string + let columnOfCount: string + + if (type === 'followers') { + columnToUpdate = 'followersCount' + columnOfCount = 'targetActorId' + } else { + columnToUpdate = 'followingCount' + columnOfCount = 'actorId' + } + + return ActorModel.update({ + [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`) + }, { where, transaction }) + } + + static loadAccountActorByVideoId (videoId: number): Promise { + const query = { + include: [ + { + attributes: [ 'id' ], + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id', 'accountId' ], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id', 'channelId' ], + model: VideoModel.unscoped(), + where: { + id: videoId + } + } + ] + } + ] + } + ] + } + + return ActorModel.unscoped().findOne(query) } - toFormattedJSON () { - let avatar: Avatar = null + getSharedInbox (this: MActorWithInboxes) { + return this.sharedInboxUrl || this.inboxUrl + } + + toFormattedSummaryJSON (this: MActorSummaryFormattable) { + let avatar: ActorImage = null if (this.Avatar) { avatar = this.Avatar.toFormattedJSON() } return { - id: this.id, url: this.url, - uuid: this.uuid, name: this.preferredUsername, host: this.getHost(), + avatar + } + } + + toFormattedJSON (this: MActorFormattable) { + const base = this.toFormattedSummaryJSON() + + return Object.assign(base, { + id: this.id, hostRedundancyAllowed: this.getRedundancyAllowed(), followingCount: this.followingCount, followersCount: this.followersCount, - avatar, createdAt: this.createdAt, updatedAt: this.updatedAt - } + }) } - toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') { - let activityPubType - if (type === 'Account') { - activityPubType = 'Person' as 'Person' - } else if (type === 'Application') { - activityPubType = 'Application' as 'Application' - } else { // VideoChannel - activityPubType = 'Group' as 'Group' - } + toActivityPubObject (this: MActorAP, name: string) { + let icon: ActivityIconObject - let icon = undefined if (this.avatarId) { const extension = extname(this.Avatar.filename) + icon = { type: 'Image', mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', @@ -441,7 +555,7 @@ export class ActorModel extends Model { } const json = { - type: activityPubType, + type: this.type, id: this.url, following: this.getFollowingUrl(), followers: this.getFollowersUrl(), @@ -454,7 +568,6 @@ export class ActorModel extends Model { endpoints: { sharedInbox: this.sharedInboxUrl }, - uuid: this.uuid, publicKey: { id: this.getPublicKeyUrl(), owner: this.url, @@ -466,7 +579,7 @@ export class ActorModel extends Model { return activityPubContextify(json) } - getFollowerSharedInboxUrls (t: Sequelize.Transaction) { + getFollowerSharedInboxUrls (t: Transaction) { const query = { attributes: [ 'sharedInboxUrl' ], include: [ @@ -508,7 +621,7 @@ export class ActorModel extends Model { return this.serverId === null } - getWebfingerUrl () { + getWebfingerUrl (this: MActorServer) { return 'acct:' + this.preferredUsername + '@' + this.getHost() } @@ -516,7 +629,7 @@ export class ActorModel extends Model { return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername } - getHost () { + getHost (this: MActorHost) { return this.Server ? this.Server.host : WEBSERVER.HOST } @@ -527,7 +640,7 @@ export class ActorModel extends Model { getAvatarUrl () { if (!this.avatarId) return undefined - return WEBSERVER.URL + this.Avatar.getWebserverPath() + return WEBSERVER.URL + this.Avatar.getStaticPath() } isOutdated () {