X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Faccount%2Faccount.ts;h=466d6258e1cdafce825adab256c83c38bb92aa6f;hb=d95d15598847c7f020aa056e7e6e0c02d2bbf732;hp=4930681279e1424066c8910b7ac64fd5052b11fb;hpb=265ba139ebf56bbdc1c65f6ea4f367774c691fc0;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 493068127..466d6258e 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -1,57 +1,146 @@ -import * as Sequelize from 'sequelize' import { - AfterDestroy, AllowNull, + BeforeDestroy, BelongsTo, Column, CreatedAt, + DataType, + Default, DefaultScope, ForeignKey, HasMany, Is, Model, + Scopes, Table, UpdatedAt } from 'sequelize-typescript' -import { Account } from '../../../shared/models/actors' -import { isUserUsernameValid } from '../../helpers/custom-validators/users' +import { Account, AccountSummary } from '../../../shared/models/actors' +import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' import { sendDeleteActor } from '../../lib/activitypub/send' import { ActorModel } from '../activitypub/actor' import { ApplicationModel } from '../application/application' -import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' import { getSort, throwIfNotValid } from '../utils' import { VideoChannelModel } from '../video/video-channel' +import { VideoCommentModel } from '../video/video-comment' import { UserModel } from './user' +import { AvatarModel } from '../avatar/avatar' +import { VideoPlaylistModel } from '../video/video-playlist' +import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' +import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize' +import { AccountBlocklistModel } from './account-blocklist' +import { ServerBlocklistModel } from '../server/server-blocklist' +import { ActorFollowModel } from '../activitypub/actor-follow' +import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable, MAccount } from '../../types/models' +import * as Bluebird from 'bluebird' +import { ModelCache } from '@server/models/model-cache' +import { VideoModel } from '../video/video' + +export enum ScopeNames { + SUMMARY = 'SUMMARY' +} -@DefaultScope({ +export type SummaryOptions = { + whereActor?: WhereOptions + withAccountBlockerIds?: number[] +} + +@DefaultScope(() => ({ include: [ { - model: () => ActorModel, - required: true, + 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 query: FindOptions = { + attributes: [ 'id', 'name' ], include: [ { - model: () => ServerModel, - required: false - }, + attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], + model: ActorModel.unscoped(), + required: true, + where: whereActor, + include: [ + serverInclude, + + { + model: AvatarModel.unscoped(), + required: false + } + ] + } + ] + } + + if (options.withAccountBlockerIds) { + query.include.push({ + attributes: [ 'id' ], + model: AccountBlocklistModel.unscoped(), + as: 'BlockedAccounts', + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds + } + } + }) + + serverInclude.include = [ { - model: () => AvatarModel, - required: false + attributes: [ 'id' ], + model: ServerBlocklistModel.unscoped(), + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds + } + } } ] } - ] -}) + + return query + } +})) @Table({ - tableName: 'account' + tableName: 'account', + indexes: [ + { + fields: [ 'actorId' ], + unique: true + }, + { + fields: [ 'applicationId' ] + }, + { + fields: [ 'userId' ] + } + ] }) export class AccountModel extends Model { @AllowNull(false) - @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name')) @Column name: string + @AllowNull(true) + @Default(null) + @Is('AccountDescription', value => throwIfNotValid(value, isAccountDescriptionValid, 'description', true)) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max)) + description: string + @CreatedAt createdAt: Date @@ -92,7 +181,7 @@ export class AccountModel extends Model { }, onDelete: 'cascade' }) - Account: ApplicationModel + Application: ApplicationModel @HasMany(() => VideoChannelModel, { foreignKey: { @@ -103,58 +192,126 @@ export class AccountModel extends Model { }) VideoChannels: VideoChannelModel[] - @AfterDestroy - static sendDeleteIfOwned (instance: AccountModel) { + @HasMany(() => VideoPlaylistModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade', + hooks: true + }) + VideoPlaylists: VideoPlaylistModel[] + + @HasMany(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade', + hooks: true + }) + VideoComments: VideoCommentModel[] + + @HasMany(() => AccountBlocklistModel, { + foreignKey: { + name: 'targetAccountId', + allowNull: false + }, + as: 'BlockedAccounts', + onDelete: 'CASCADE' + }) + 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) if (instance.isOwned()) { - return sendDeleteActor(instance.Actor, undefined) + return sendDeleteActor(instance.Actor, options.transaction) } return undefined } - static load (id: number) { - return AccountModel.findById(id) + static load (id: number, transaction?: Transaction): Bluebird { + return AccountModel.findByPk(id, { transaction }) } - static loadByUUID (uuid: string) { - const query = { - include: [ - { - model: ActorModel, - required: true, - where: { - uuid - } - } - ] - } + static loadByNameWithHost (nameWithHost: string): Bluebird { + const [ accountName, host ] = nameWithHost.split('@') - return AccountModel.findOne(query) + if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName) + + 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): Bluebird { + 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 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): Bluebird { + const query = { + include: [ + { + model: ActorModel, + required: true, + where: { + preferredUsername: name + }, + include: [ + { + model: ServerModel, + required: true, + where: { + host + } + } + ] + } + ] } return AccountModel.findOne(query) } - static loadByUrl (url: string, transaction?: Sequelize.Transaction) { + static loadByUrl (url: string, transaction?: Transaction): Bluebird { const query = { include: [ { @@ -175,7 +332,7 @@ export class AccountModel extends Model { const query = { offset: start, limit: count, - order: [ getSort(sort) ] + order: getSort(sort) } return AccountModel.findAndCountAll(query) @@ -187,24 +344,102 @@ export class AccountModel extends Model { }) } - toFormattedJSON (): Account { + static loadAccountIdFromVideo (videoId: number): Bluebird { + const query = { + include: [ + { + attributes: [ 'id', 'accountId' ], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [ 'id', 'channelId' ], + model: VideoModel.unscoped(), + where: { + id: videoId + } + } + ] + } + ] + } + + return AccountModel.findOne(query) + } + + static listLocalsForSitemap (sort: string): Bluebird { + const query = { + attributes: [ ], + offset: 0, + order: getSort(sort), + include: [ + { + attributes: [ 'preferredUsername', 'serverId' ], + model: ActorModel.unscoped(), + where: { + serverId: null + } + } + ] + } + + 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, - name: this.Actor.preferredUsername, - displayName: this.name, + displayName: this.getDisplayName(), + description: this.description, createdAt: this.createdAt, - updatedAt: this.updatedAt + updatedAt: this.updatedAt, + userId: this.userId ? this.userId : undefined } return Object.assign(actor, account) } - toActivityPubObject () { - return this.Actor.toActivityPubObject(this.name, 'Account') + 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 Object.assign(obj, { + summary: this.description + }) } isOwned () { return this.Actor.isOwned() } + + isOutdated () { + return this.Actor.isOutdated() + } + + getDisplayName () { + return this.name + } + + isBlocked () { + return this.BlockedAccounts && this.BlockedAccounts.length !== 0 + } }