X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Faccount%2Faccount.ts;h=c72f9c63df3c418a8953f57b5025fbb95a1d460a;hb=b49f22d8f9a52ab75fd38db2d377249eb58fa678;hp=d6758fa109dd144e80747a47ea8a59038bde7f33;hpb=3fd3ab2d34d512b160a5e6084d7609be7b4f4452;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/account/account.ts b/server/models/account/account.ts index d6758fa10..c72f9c63d 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -1,126 +1,156 @@ -import { join } from 'path' -import * as Sequelize from 'sequelize' +import { FindOptions, Includeable, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize' import { - AfterDestroy, AllowNull, + BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, Default, + DefaultScope, ForeignKey, HasMany, Is, - IsUUID, Model, + Scopes, Table, UpdatedAt } from 'sequelize-typescript' -import { Avatar } from '../../../shared/models/avatars/avatar.model' -import { activityPubContextify } from '../../helpers' +import { ModelCache } from '@server/models/model-cache' +import { Account, AccountSummary } from '../../../shared/models/actors' +import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' +import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' +import { sendDeleteActor } from '../../lib/activitypub/send' import { - isAccountFollowersCountValid, - isAccountFollowingCountValid, - isAccountPrivateKeyValid, - isAccountPublicKeyValid, - isActivityPubUrlValid -} from '../../helpers/custom-validators/activitypub' -import { isUserUsernameValid } from '../../helpers/custom-validators/users' -import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' -import { sendDeleteAccount } from '../../lib/activitypub/send' + MAccount, + MAccountActor, + MAccountAP, + MAccountDefault, + MAccountFormattable, + MAccountSummaryFormattable, + MChannelActor +} from '../../types/models' +import { ActorModel } from '../activitypub/actor' +import { ActorFollowModel } from '../activitypub/actor-follow' import { ApplicationModel } from '../application/application' import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' -import { throwIfNotValid } from '../utils' +import { ServerBlocklistModel } from '../server/server-blocklist' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from '../video/video' import { VideoChannelModel } from '../video/video-channel' -import { AccountFollowModel } from './account-follow' +import { VideoCommentModel } from '../video/video-comment' +import { VideoPlaylistModel } from '../video/video-playlist' +import { AccountBlocklistModel } from './account-blocklist' import { UserModel } from './user' +export enum ScopeNames { + SUMMARY = 'SUMMARY' +} + +export type SummaryOptions = { + actorRequired?: boolean // Default: true + whereActor?: WhereOptions + withAccountBlockerIds?: number[] +} + +@DefaultScope(() => ({ + include: [ + { + model: ActorModel, // Default scope includes avatar and server + required: true + } + ] +})) +@Scopes(() => ({ + [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { + const whereActor = options.whereActor || undefined + + const serverInclude: IncludeOptions = { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + + const queryInclude: Includeable[] = [ + { + attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], + model: ActorModel.unscoped(), + required: options.actorRequired ?? true, + where: whereActor, + include: [ + serverInclude, + + { + model: AvatarModel.unscoped(), + required: false + } + ] + } + ] + + const query: FindOptions = { + attributes: [ 'id', 'name', 'actorId' ] + } + + if (options.withAccountBlockerIds) { + queryInclude.push({ + attributes: [ 'id' ], + model: AccountBlocklistModel.unscoped(), + as: 'BlockedAccounts', + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds + } + } + }) + + serverInclude.include = [ + { + attributes: [ 'id' ], + model: ServerBlocklistModel.unscoped(), + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds + } + } + } + ] + } + + query.include = queryInclude + + return query + } +})) @Table({ tableName: 'account', indexes: [ { - fields: [ 'name' ] - }, - { - fields: [ 'serverId' ] - }, - { - fields: [ 'userId' ], + fields: [ 'actorId' ], unique: true }, { - fields: [ 'applicationId' ], - unique: true + fields: [ 'applicationId' ] }, { - fields: [ 'name', 'serverId', 'applicationId' ], - unique: true + fields: [ 'userId' ] } ] }) -export class AccountModel extends Model { - - @AllowNull(false) - @Default(DataType.UUIDV4) - @IsUUID(4) - @Column(DataType.UUID) - uuid: string +export class AccountModel extends Model { @AllowNull(false) - @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name')) @Column name: string - @AllowNull(false) - @Is('AccountUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - url: string - - @AllowNull(true) - @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPublicKeyValid, 'public key')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max)) - publicKey: string - @AllowNull(true) - @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPrivateKeyValid, 'private key')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max)) - privateKey: string - - @AllowNull(false) - @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowersCountValid, 'followers count')) - @Column - followersCount: number - - @AllowNull(false) - @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowingCountValid, 'following count')) - @Column - followingCount: number - - @AllowNull(false) - @Is('AccountInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - inboxUrl: string - - @AllowNull(false) - @Is('AccountOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - outboxUrl: string - - @AllowNull(false) - @Is('AccountSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - sharedInboxUrl: string - - @AllowNull(false) - @Is('AccountFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - followersUrl: string - - @AllowNull(false) - @Is('AccountFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url')) - @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) - followingUrl: string + @Default(null) + @Is('AccountDescription', value => throwIfNotValid(value, isAccountDescriptionValid, 'description', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max)) + description: string @CreatedAt createdAt: Date @@ -128,29 +158,17 @@ export class AccountModel extends Model { @UpdatedAt updatedAt: Date - @ForeignKey(() => AvatarModel) + @ForeignKey(() => ActorModel) @Column - avatarId: number + actorId: number - @BelongsTo(() => AvatarModel, { + @BelongsTo(() => ActorModel, { foreignKey: { - allowNull: true - }, - onDelete: 'cascade' - }) - Avatar: AvatarModel - - @ForeignKey(() => ServerModel) - @Column - serverId: number - - @BelongsTo(() => ServerModel, { - foreignKey: { - allowNull: true + allowNull: false }, onDelete: 'cascade' }) - Server: ServerModel + Actor: ActorModel @ForeignKey(() => UserModel) @Column @@ -185,93 +203,119 @@ export class AccountModel extends Model { }) VideoChannels: VideoChannelModel[] - @HasMany(() => AccountFollowModel, { + @HasMany(() => VideoPlaylistModel, { foreignKey: { - name: 'accountId', allowNull: false }, - onDelete: 'cascade' + onDelete: 'cascade', + hooks: true }) - AccountFollowing: AccountFollowModel[] + VideoPlaylists: VideoPlaylistModel[] - @HasMany(() => AccountFollowModel, { + @HasMany(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade', + hooks: true + }) + VideoComments: VideoCommentModel[] + + @HasMany(() => AccountBlocklistModel, { foreignKey: { name: 'targetAccountId', allowNull: false }, - as: 'followers', - onDelete: 'cascade' + as: 'BlockedAccounts', + onDelete: 'CASCADE' }) - AccountFollowers: AccountFollowModel[] + BlockedAccounts: AccountBlocklistModel[] + + @BeforeDestroy + static async sendDeleteIfOwned (instance: AccountModel, options) { + if (!instance.Actor) { + instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) + } + + await ActorFollowModel.removeFollowsOf(instance.Actor.id, options.transaction) - @AfterDestroy - static sendDeleteIfOwned (instance: AccountModel) { if (instance.isOwned()) { - return sendDeleteAccount(instance, undefined) + return sendDeleteActor(instance.Actor, options.transaction) } return undefined } - static loadApplication () { - return AccountModel.findOne({ - include: [ - { - model: ApplicationModel, - required: true - } - ] - }) + static load (id: number, transaction?: Transaction): Promise { + return AccountModel.findByPk(id, { transaction }) } - static load (id: number) { - return AccountModel.findById(id) - } + static loadByNameWithHost (nameWithHost: string): Promise { + const [ accountName, host ] = nameWithHost.split('@') - static loadByUUID (uuid: string) { - const query = { - where: { - uuid - } - } + if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName) - return AccountModel.findOne(query) + return AccountModel.loadByNameAndHost(accountName, host) } - static loadLocalByName (name: string) { - const query = { - where: { - name, - [ Sequelize.Op.or ]: [ - { - userId: { - [ Sequelize.Op.ne ]: null + static loadLocalByName (name: string): Promise { + const fun = () => { + const query = { + where: { + [Op.or]: [ + { + userId: { + [Op.ne]: null + } + }, + { + applicationId: { + [Op.ne]: null + } } - }, + ] + }, + include: [ { - applicationId: { - [ Sequelize.Op.ne ]: null + model: ActorModel, + required: true, + where: { + preferredUsername: name } } ] } + + return AccountModel.findOne(query) } - return AccountModel.findOne(query) + return ModelCache.Instance.doCache({ + cacheType: 'local-account-name', + key: name, + fun, + // The server actor never change, so we can easily cache it + whitelist: () => name === SERVER_ACTOR_NAME + }) } - static loadByNameAndHost (name: string, host: string) { + static loadByNameAndHost (name: string, host: string): Promise { const query = { - where: { - name - }, include: [ { - model: ServerModel, + model: ActorModel, required: true, where: { - host - } + preferredUsername: name + }, + include: [ + { + model: ServerModel, + required: true, + where: { + host + } + } + ] } ] } @@ -279,122 +323,139 @@ export class AccountModel extends Model { return AccountModel.findOne(query) } - static loadByUrl (url: string, transaction?: Sequelize.Transaction) { + static loadByUrl (url: string, transaction?: Transaction): Promise { const query = { - where: { - url - }, + include: [ + { + model: ActorModel, + required: true, + where: { + url + } + } + ], transaction } return AccountModel.findOne(query) } - static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { + static listForApi (start: number, count: number, sort: string) { const query = { - where: { - followersUrl: { - [ Sequelize.Op.in ]: followersUrls - } - }, - transaction + offset: start, + limit: count, + order: getSort(sort) } - return AccountModel.findAll(query) + return AccountModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) } - toFormattedJSON () { - let host = CONFIG.WEBSERVER.HOST - let score: number - let avatar: Avatar = null - - if (this.Avatar) { - avatar = { - path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), - createdAt: this.Avatar.createdAt, - updatedAt: this.Avatar.updatedAt - } + static loadAccountIdFromVideo (videoId: number): Promise { + const query = { + include: [ + { + attributes: [ 'id', 'accountId' ], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id', 'channelId' ], + model: VideoModel.unscoped(), + where: { + id: videoId + } + } + ] + } + ] } - if (this.Server) { - host = this.Server.host - score = this.Server.score + return AccountModel.findOne(query) + } + + static listLocalsForSitemap (sort: string): Promise { + const query = { + attributes: [ ], + offset: 0, + order: getSort(sort), + include: [ + { + attributes: [ 'preferredUsername', 'serverId' ], + model: ActorModel.unscoped(), + where: { + serverId: null + } + } + ] } - return { + return AccountModel + .unscoped() + .findAll(query) + } + + getClientUrl () { + return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier() + } + + toFormattedJSON (this: MAccountFormattable): Account { + const actor = this.Actor.toFormattedJSON() + const account = { id: this.id, - uuid: this.uuid, - host, - score, - name: this.name, - followingCount: this.followingCount, - followersCount: this.followersCount, + displayName: this.getDisplayName(), + description: this.description, createdAt: this.createdAt, updatedAt: this.updatedAt, - avatar + userId: this.userId ? this.userId : undefined } + + return Object.assign(actor, account) } - toActivityPubObject () { - const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' - - const json = { - type, - id: this.url, - following: this.getFollowingUrl(), - followers: this.getFollowersUrl(), - inbox: this.inboxUrl, - outbox: this.outboxUrl, - preferredUsername: this.name, - url: this.url, - name: this.name, - endpoints: { - sharedInbox: this.sharedInboxUrl - }, - uuid: this.uuid, - publicKey: { - id: this.getPublicKeyUrl(), - owner: this.url, - publicKeyPem: this.publicKey - } + toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { + const actor = this.Actor.toFormattedSummaryJSON() + + return { + id: this.id, + name: actor.name, + displayName: this.getDisplayName(), + url: actor.url, + host: actor.host, + avatar: actor.avatar } + } + + toActivityPubObject (this: MAccountAP) { + const obj = this.Actor.toActivityPubObject(this.name) - return activityPubContextify(json) + return Object.assign(obj, { + summary: this.description + }) } isOwned () { - return this.serverId === null + return this.Actor.isOwned() } - getFollowerSharedInboxUrls (t: Sequelize.Transaction) { - const query = { - attributes: [ 'sharedInboxUrl' ], - include: [ - { - model: AccountFollowModel, - required: true, - as: 'followers', - where: { - targetAccountId: this.id - } - } - ], - transaction: t - } - - return AccountModel.findAll(query) - .then(accounts => accounts.map(a => a.sharedInboxUrl)) + isOutdated () { + return this.Actor.isOutdated() } - getFollowingUrl () { - return this.url + '/following' + getDisplayName () { + return this.name } - getFollowersUrl () { - return this.url + '/followers' + getLocalUrl (this: MAccountActor | MChannelActor) { + return WEBSERVER.URL + `/accounts/` + this.Actor.preferredUsername } - getPublicKeyUrl () { - return this.url + '#main-key' + isBlocked () { + return this.BlockedAccounts && this.BlockedAccounts.length !== 0 } }