X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Faccount%2Faccount.ts;h=ba1094536f9721e4e6c3e0a248ffc84bf848e016;hb=8424c4026afd7304880a4ce8138a04ffb3d8c938;hp=a79e13880f0f854e908fe8848e3060d49139b7f3;hpb=571389d43b8fc8aaf27e77c06f19b320b08dbbc9;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/account/account.ts b/server/models/account/account.ts index a79e13880..ba1094536 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -1,447 +1,406 @@ -import * as Sequelize from 'sequelize' - -import { - isUserUsernameValid, - isAccountPublicKeyValid, - isAccountUrlValid, - isAccountPrivateKeyValid, - isAccountFollowersCountValid, - isAccountFollowingCountValid, - isAccountInboxValid, - isAccountOutboxValid, - isAccountSharedInboxValid, - isAccountFollowersValid, - isAccountFollowingValid, - activityPubContextify -} from '../../helpers' - -import { addMethodsToModel } from '../utils' import { - AccountInstance, - AccountAttributes, - - AccountMethods -} from './account-interface' - -let Account: Sequelize.Model -let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID -let load: AccountMethods.Load -let loadByUUID: AccountMethods.LoadByUUID -let loadByUrl: AccountMethods.LoadByUrl -let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName -let listOwned: AccountMethods.ListOwned -let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi -let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi -let isOwned: AccountMethods.IsOwned -let toActivityPubObject: AccountMethods.ToActivityPubObject -let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls -let getFollowingUrl: AccountMethods.GetFollowingUrl -let getFollowersUrl: AccountMethods.GetFollowersUrl -let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl - -export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Account = sequelize.define('Account', + AllowNull, + BeforeDestroy, + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + DefaultScope, + ForeignKey, + HasMany, + Is, + Model, + Scopes, + Table, + UpdatedAt +} from 'sequelize-typescript' +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 { 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, 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, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models' +import * as Bluebird from 'bluebird' + +export enum ScopeNames { + SUMMARY = 'SUMMARY' +} + +export type SummaryOptions = { + whereActor?: WhereOptions + withAccountBlockerIds?: number[] +} + +@DefaultScope(() => ({ + include: [ { - uuid: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - allowNull: false, - validate: { - isUUID: 4 - } - }, - name: { - type: DataTypes.STRING, - allowNull: false, - validate: { - usernameValid: value => { - const res = isUserUsernameValid(value) - if (res === false) throw new Error('Username is not valid.') - } - } - }, - url: { - type: DataTypes.STRING, - allowNull: false, - validate: { - urlValid: value => { - const res = isAccountUrlValid(value) - if (res === false) throw new Error('URL is not valid.') - } - } - }, - publicKey: { - type: DataTypes.STRING, - allowNull: false, - validate: { - publicKeyValid: value => { - const res = isAccountPublicKeyValid(value) - if (res === false) throw new Error('Public key is not valid.') - } - } - }, - privateKey: { - type: DataTypes.STRING, - allowNull: false, - validate: { - privateKeyValid: value => { - const res = isAccountPrivateKeyValid(value) - if (res === false) throw new Error('Private key is not valid.') - } - } - }, - followersCount: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - followersCountValid: value => { - const res = isAccountFollowersCountValid(value) - if (res === false) throw new Error('Followers count is not valid.') - } - } - }, - followingCount: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - followersCountValid: value => { - const res = isAccountFollowingCountValid(value) - if (res === false) throw new Error('Following count is not valid.') - } - } - }, - inboxUrl: { - type: DataTypes.STRING, - allowNull: false, - validate: { - inboxUrlValid: value => { - const res = isAccountInboxValid(value) - if (res === false) throw new Error('Inbox URL is not valid.') - } - } - }, - outboxUrl: { - type: DataTypes.STRING, - allowNull: false, - validate: { - outboxUrlValid: value => { - const res = isAccountOutboxValid(value) - if (res === false) throw new Error('Outbox URL is not valid.') - } - } - }, - sharedInboxUrl: { - type: DataTypes.STRING, - allowNull: false, - validate: { - sharedInboxUrlValid: value => { - const res = isAccountSharedInboxValid(value) - if (res === false) throw new Error('Shared inbox URL is not valid.') - } + 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: [ + { + attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], + model: ActorModel.unscoped(), + required: true, + where: whereActor, + include: [ + serverInclude, + + { + model: AvatarModel.unscoped(), + required: false + } + ] } - }, - followersUrl: { - type: DataTypes.STRING, - allowNull: false, - validate: { - followersUrlValid: value => { - const res = isAccountFollowersValid(value) - if (res === false) throw new Error('Followers URL is not valid.') + ] + } + + if (options.withAccountBlockerIds) { + query.include.push({ + attributes: [ 'id' ], + model: AccountBlocklistModel.unscoped(), + as: 'BlockedAccounts', + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds } } - }, - followingUrl: { - type: DataTypes.STRING, - allowNull: false, - validate: { - followingUrlValid: value => { - const res = isAccountFollowingValid(value) - if (res === false) throw new Error('Following URL is not valid.') + }) + + serverInclude.include = [ + { + attributes: [ 'id' ], + model: ServerBlocklistModel.unscoped(), + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds + } } } - } + ] + } + + return query + } +})) +@Table({ + tableName: 'account', + indexes: [ + { + fields: [ 'actorId' ], + unique: true }, { - indexes: [ - { - fields: [ 'name' ] - }, - { - fields: [ 'podId' ] - }, - { - fields: [ 'userId' ], - unique: true - }, - { - fields: [ 'applicationId' ], - unique: true - }, - { - fields: [ 'name', 'podId' ], - unique: true - } - ], - hooks: { afterDestroy } + fields: [ 'applicationId' ] + }, + { + fields: [ 'userId' ] } - ) - - const classMethods = [ - associate, - loadAccountByPodAndUUID, - load, - loadByUUID, - loadLocalAccountByName, - listOwned, - listFollowerUrlsForApi, - listFollowingUrlsForApi - ] - const instanceMethods = [ - isOwned, - toActivityPubObject, - getFollowerSharedInboxUrls, - getFollowingUrl, - getFollowersUrl, - getPublicKeyUrl ] - addMethodsToModel(Account, classMethods, instanceMethods) +}) +export class AccountModel extends Model { - return Account -} + @AllowNull(false) + @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 -function associate (models) { - Account.belongsTo(models.Pod, { + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => ActorModel) + @Column + actorId: number + + @BelongsTo(() => ActorModel, { foreignKey: { - name: 'podId', - allowNull: true + allowNull: false }, onDelete: 'cascade' }) + Actor: ActorModel - Account.belongsTo(models.User, { + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { foreignKey: { - name: 'userId', allowNull: true }, onDelete: 'cascade' }) + User: UserModel + + @ForeignKey(() => ApplicationModel) + @Column + applicationId: number - Account.belongsTo(models.Application, { + @BelongsTo(() => ApplicationModel, { foreignKey: { - name: 'userId', allowNull: true }, onDelete: 'cascade' }) + Application: ApplicationModel - Account.hasMany(models.VideoChannel, { + @HasMany(() => VideoChannelModel, { foreignKey: { - name: 'accountId', allowNull: false }, onDelete: 'cascade', hooks: true }) + VideoChannels: VideoChannelModel[] - Account.hasMany(models.AccountFollower, { + @HasMany(() => VideoPlaylistModel, { foreignKey: { - name: 'accountId', allowNull: false }, - onDelete: 'cascade' + onDelete: 'cascade', + hooks: true }) + VideoPlaylists: VideoPlaylistModel[] - Account.hasMany(models.AccountFollower, { + @HasMany(() => VideoCommentModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade', + hooks: true + }) + VideoComments: VideoCommentModel[] + + @HasMany(() => AccountBlocklistModel, { foreignKey: { name: 'targetAccountId', allowNull: false }, - onDelete: 'cascade' + as: 'BlockedAccounts', + onDelete: 'CASCADE' }) -} + BlockedAccounts: AccountBlocklistModel[] -function afterDestroy (account: AccountInstance) { - if (account.isOwned()) { - const removeVideoAccountToFriendsParams = { - uuid: account.uuid + @BeforeDestroy + static async sendDeleteIfOwned (instance: AccountModel, options) { + if (!instance.Actor) { + instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) as ActorModel } - // FIXME: remove account in followers - // return removeVideoAccountToFriends(removeVideoAccountToFriendsParams) - } - - return undefined -} - -toActivityPubObject = function (this: AccountInstance) { - const type = this.podId ? '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 + await ActorFollowModel.removeFollowsOf(instance.Actor.id, options.transaction) + if (instance.isOwned()) { + return sendDeleteActor(instance.Actor, options.transaction) } - } - - return activityPubContextify(json) -} -isOwned = function (this: AccountInstance) { - return this.podId === null -} + return undefined + } -getFollowerSharedInboxUrls = function (this: AccountInstance) { - const query: Sequelize.FindOptions = { - attributes: [ 'sharedInboxUrl' ], - include: [ - { - model: Account['sequelize'].models.AccountFollower, - where: { - targetAccountId: this.id - } - } - ] + static load (id: number, transaction?: Transaction): Bluebird { + return AccountModel.findByPk(id, { transaction }) } - return Account.findAll(query) - .then(accounts => accounts.map(a => a.sharedInboxUrl)) -} + static loadByNameWithHost (nameWithHost: string): Bluebird { + const [ accountName, host ] = nameWithHost.split('@') -getFollowingUrl = function (this: AccountInstance) { - return this.url + '/followers' -} + if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName) -getFollowersUrl = function (this: AccountInstance) { - return this.url + '/followers' -} + return AccountModel.loadByNameAndHost(accountName, host) + } -getPublicKeyUrl = function (this: AccountInstance) { - return this.url + '#main-key' -} + static loadLocalByName (name: string): Bluebird { + const query = { + where: { + [ Op.or ]: [ + { + userId: { + [ Op.ne ]: null + } + }, + { + applicationId: { + [ Op.ne ]: null + } + } + ] + }, + include: [ + { + model: ActorModel, + required: true, + where: { + preferredUsername: name + } + } + ] + } -// ------------------------------ STATICS ------------------------------ + return AccountModel.findOne(query) + } -listOwned = function () { - const query: Sequelize.FindOptions = { - where: { - podId: null + 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) } - return Account.findAll(query) -} + static loadByUrl (url: string, transaction?: Transaction): Bluebird { + const query = { + include: [ + { + model: ActorModel, + required: true, + where: { + url + } + } + ], + transaction + } -listFollowerUrlsForApi = function (name: string, start: number, count?: number) { - return createListFollowForApiQuery('followers', name, start, count) -} + return AccountModel.findOne(query) + } -listFollowingUrlsForApi = function (name: string, start: number, count?: number) { - return createListFollowForApiQuery('following', name, start, count) -} + static listForApi (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: getSort(sort) + } -load = function (id: number) { - return Account.findById(id) -} + return AccountModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } -loadByUUID = function (uuid: string) { - const query: Sequelize.FindOptions = { - where: { - uuid + static listLocalsForSitemap (sort: string): Bluebird { + const query = { + attributes: [ ], + offset: 0, + order: getSort(sort), + include: [ + { + attributes: [ 'preferredUsername', 'serverId' ], + model: ActorModel.unscoped(), + where: { + serverId: null + } + } + ] } - } - return Account.findOne(query) -} + return AccountModel + .unscoped() + .findAll(query) + } -loadLocalAccountByName = function (name: string) { - const query: Sequelize.FindOptions = { - where: { - name, - userId: { - [Sequelize.Op.ne]: null - } + toFormattedJSON (this: MAccountFormattable): Account { + const actor = this.Actor.toFormattedJSON() + const account = { + id: this.id, + displayName: this.getDisplayName(), + description: this.description, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + userId: this.userId ? this.userId : undefined } + + return Object.assign(actor, account) } - return Account.findOne(query) -} + toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary { + const actor = this.Actor.toFormattedSummaryJSON() -loadByUrl = function (url: string) { - const query: Sequelize.FindOptions = { - where: { - url + return { + id: this.id, + name: actor.name, + displayName: this.getDisplayName(), + url: actor.url, + host: actor.host, + avatar: actor.avatar } } - return Account.findOne(query) -} + toActivityPubObject (this: MAccountAP) { + const obj = this.Actor.toActivityPubObject(this.name) -loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - podId, - uuid - }, - transaction + return Object.assign(obj, { + summary: this.description + }) } - return Account.find(query) -} - -// ------------------------------ UTILS ------------------------------ - -async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count?: number) { - let firstJoin: string - let secondJoin: string - - if (type === 'followers') { - firstJoin = 'targetAccountId' - secondJoin = 'accountId' - } else { - firstJoin = 'accountId' - secondJoin = 'targetAccountId' + isOwned () { + return this.Actor.isOwned() } - const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ] - const tasks: Promise[] = [] - - for (const selection of selections) { - let query = 'SELECT ' + selection + ' FROM "Account" ' + - 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' + - 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' + - 'WHERE "Account"."name" = \'$name\' ' + - 'LIMIT ' + start - - if (count !== undefined) query += ', ' + count - - const options = { - bind: { name }, - type: Sequelize.QueryTypes.SELECT - } - tasks.push(Account['sequelize'].query(query, options)) + isOutdated () { + return this.Actor.isOutdated() } - const [ followers, [ { total } ]] = await Promise.all(tasks) - const urls: string[] = followers.map(f => f.url) + getDisplayName () { + return this.name + } - return { - data: urls, - total: parseInt(total, 10) + isBlocked () { + return this.BlockedAccounts && this.BlockedAccounts.length !== 0 } }