X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Factor%2Factor-follow.ts;h=71ce9fa6fb2887d298cecd999ac6e2bfb6e9c692;hb=3f0ceab06e5320f62f593c49daa30d963dbc36f9;hp=4c5f376202b7fbb207b03bad70ef7d11beb0316e;hpb=7d9ba5c08999c6482f0bc5e0c09c6f55b7724090;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 4c5f37620..71ce9fa6f 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts @@ -1,5 +1,5 @@ -import { difference, values } from 'lodash' -import { IncludeOptions, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize' +import { difference } from 'lodash' +import { Attributes, FindOptions, Includeable, IncludeOptions, Op, QueryTypes, Transaction, WhereAttributeHash } from 'sequelize' import { AfterCreate, AfterDestroy, @@ -19,25 +19,30 @@ import { UpdatedAt } from 'sequelize-typescript' import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' +import { afterCommitIfTransaction } from '@server/helpers/database-utils' import { getServerActor } from '@server/models/application/application' -import { VideoModel } from '@server/models/video/video' import { + MActor, + MActorFollowActors, MActorFollowActorsDefault, MActorFollowActorsDefaultSubscription, MActorFollowFollowingHost, MActorFollowFormattable, MActorFollowSubscriptions } from '@server/types/models' -import { ActivityPubActorType } from '@shared/models' +import { AttributesOnly } from '@shared/typescript-utils' import { FollowState } from '../../../shared/models/actors' import { ActorFollow } from '../../../shared/models/actors/follow.model' import { logger } from '../../helpers/logger' -import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' +import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants' import { AccountModel } from '../account/account' import { ServerModel } from '../server/server' -import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' +import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared' +import { doesExist } from '../shared/query' import { VideoChannelModel } from '../video/video-channel' import { ActorModel, unusedActorAttributesForAPI } from './actor' +import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' +import { InstanceListFollowingQueryBuilder, ListFollowingOptions } from './sql/instance-list-following-query-builder' @Table({ tableName: 'actorFollow', @@ -61,10 +66,10 @@ import { ActorModel, unusedActorAttributesForAPI } from './actor' } ] }) -export class ActorFollowModel extends Model { +export class ActorFollowModel extends Model>> { @AllowNull(false) - @Column(DataType.ENUM(...values(FOLLOW_STATES))) + @Column(DataType.ENUM(...Object.values(FOLLOW_STATES))) state: FollowState @AllowNull(false) @@ -117,20 +122,72 @@ export class ActorFollowModel extends Model { @AfterCreate @AfterUpdate static incrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { - if (instance.state !== 'accepted') return undefined - - return Promise.all([ - ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction), - ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction) - ]) + return afterCommitIfTransaction(options.transaction, () => { + return Promise.all([ + ActorModel.rebuildFollowsCount(instance.actorId, 'following'), + ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers') + ]) + }) } @AfterDestroy static decrementFollowerAndFollowingCount (instance: ActorFollowModel, options: any) { - return Promise.all([ - ActorModel.rebuildFollowsCount(instance.actorId, 'following', options.transaction), - ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers', options.transaction) - ]) + return afterCommitIfTransaction(options.transaction, () => { + return Promise.all([ + ActorModel.rebuildFollowsCount(instance.actorId, 'following'), + ActorModel.rebuildFollowsCount(instance.targetActorId, 'followers') + ]) + }) + } + + // --------------------------------------------------------------------------- + + static getSQLAttributes (tableName: string, aliasPrefix = '') { + return buildSQLAttributes({ + model: this, + tableName, + aliasPrefix + }) + } + + // --------------------------------------------------------------------------- + + /* + * @deprecated Use `findOrCreateCustom` instead + */ + static findOrCreate (): any { + throw new Error('Must not be called') + } + + // findOrCreate has issues with actor follow hooks + static async findOrCreateCustom (options: { + byActor: MActor + targetActor: MActor + activityId: string + state: FollowState + transaction: Transaction + }): Promise<[ MActorFollowActors, boolean ]> { + const { byActor, targetActor, activityId, state, transaction } = options + + let created = false + let actorFollow: MActorFollowActors = await ActorFollowModel.loadByActorAndTarget(byActor.id, targetActor.id, transaction) + + if (!actorFollow) { + created = true + + actorFollow = await ActorFollowModel.create({ + actorId: byActor.id, + targetActorId: targetActor.id, + url: activityId, + + state + }, { transaction }) + + actorFollow.ActorFollowing = targetActor + actorFollow.ActorFollower = byActor + } + + return [ actorFollow, created ] } static removeFollowsOf (actorId: number, t?: Transaction) { @@ -164,22 +221,18 @@ export class ActorFollowModel extends Model { } static isFollowedBy (actorId: number, followerActorId: number) { - const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' - const options = { - type: QueryTypes.SELECT as QueryTypes.SELECT, - bind: { actorId, followerActorId }, - raw: true - } + const query = `SELECT 1 FROM "actorFollow" ` + + `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + + `LIMIT 1` - return VideoModel.sequelize.query(query, options) - .then(results => results.length === 1) + return doesExist(this.sequelize, query, { actorId, followerActorId }) } static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise { const query = { where: { actorId, - targetActorId: targetActorId + targetActorId }, include: [ { @@ -199,19 +252,20 @@ export class ActorFollowModel extends Model { return ActorFollowModel.findOne(query) } - static loadByActorAndTargetNameAndHostForAPI ( - actorId: number, - targetName: string, - targetHost: string, - t?: Transaction - ): Promise { + static loadByActorAndTargetNameAndHostForAPI (options: { + actorId: number + targetName: string + targetHost: string + state?: FollowState + transaction?: Transaction + }): Promise { + const { actorId, targetHost, targetName, state, transaction } = options + const actorFollowingPartInclude: IncludeOptions = { model: ActorModel, required: true, as: 'ActorFollowing', - where: { - preferredUsername: targetName - }, + where: ActorModel.wherePreferredUsername(targetName), include: [ { model: VideoChannelModel.unscoped(), @@ -232,10 +286,11 @@ export class ActorFollowModel extends Model { }) } - const query = { - where: { - actorId - }, + const where: WhereAttributeHash> = { actorId } + if (state) where.state = state + + const query: FindOptions> = { + where, include: [ actorFollowingPartInclude, { @@ -244,36 +299,28 @@ export class ActorFollowModel extends Model { as: 'ActorFollower' } ], - transaction: t + transaction } return ActorFollowModel.findOne(query) } - static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise { + static listSubscriptionsOf (actorId: number, targets: { name: string, host?: string }[]): Promise { const whereTab = targets .map(t => { if (t.host) { return { [Op.and]: [ - { - $preferredUsername$: t.name - }, - { - $host$: t.host - } + ActorModel.wherePreferredUsername(t.name), + { $host$: t.host } ] } } return { [Op.and]: [ - { - $preferredUsername$: t.name - }, - { - $serverId$: null - } + ActorModel.wherePreferredUsername(t.name), + { $serverId$: null } ] } }) @@ -286,6 +333,7 @@ export class ActorFollowModel extends Model { [Op.or]: whereTab }, { + state: 'accepted', actorId } ] @@ -310,138 +358,18 @@ export class ActorFollowModel extends Model { return ActorFollowModel.findAll(query) } - static listFollowingForApi (options: { - id: number - start: number - count: number - sort: string - state?: FollowState - actorType?: ActivityPubActorType - search?: string - }) { - const { id, start, count, sort, search, state, actorType } = options - - const followWhere = state ? { state } : {} - const followingWhere: WhereOptions = {} - const followingServerWhere: WhereOptions = {} - - if (search) { - Object.assign(followingServerWhere, { - host: { - [Op.iLike]: '%' + search + '%' - } - }) - } - - if (actorType) { - Object.assign(followingWhere, { type: actorType }) - } - - const query = { - distinct: true, - offset: start, - limit: count, - order: getFollowsSort(sort), - where: followWhere, - include: [ - { - model: ActorModel, - required: true, - as: 'ActorFollower', - where: { - id - } - }, - { - model: ActorModel, - as: 'ActorFollowing', - required: true, - where: followingWhere, - include: [ - { - model: ServerModel, - required: true, - where: followingServerWhere - } - ] - } - ] - } - - return ActorFollowModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) + static listInstanceFollowingForApi (options: ListFollowingOptions) { + return Promise.all([ + new InstanceListFollowingQueryBuilder(this.sequelize, options).countFollowing(), + new InstanceListFollowingQueryBuilder(this.sequelize, options).listFollowing() + ]).then(([ total, data ]) => ({ total, data })) } - static listFollowersForApi (options: { - actorId: number - start: number - count: number - sort: string - state?: FollowState - actorType?: ActivityPubActorType - search?: string - }) { - const { actorId, start, count, sort, search, state, actorType } = options - - const followWhere = state ? { state } : {} - const followerWhere: WhereOptions = {} - const followerServerWhere: WhereOptions = {} - - if (search) { - Object.assign(followerServerWhere, { - host: { - [Op.iLike]: '%' + search + '%' - } - }) - } - - if (actorType) { - Object.assign(followerWhere, { type: actorType }) - } - - const query = { - distinct: true, - offset: start, - limit: count, - order: getFollowsSort(sort), - where: followWhere, - include: [ - { - model: ActorModel, - required: true, - as: 'ActorFollower', - where: followerWhere, - include: [ - { - model: ServerModel, - required: true, - where: followerServerWhere - } - ] - }, - { - model: ActorModel, - as: 'ActorFollowing', - required: true, - where: { - id: actorId - } - } - ] - } - - return ActorFollowModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) + static listFollowersForApi (options: ListFollowersOptions) { + return Promise.all([ + new InstanceListFollowersQueryBuilder(this.sequelize, options).countFollowers(), + new InstanceListFollowersQueryBuilder(this.sequelize, options).listFollowers() + ]).then(([ total, data ]) => ({ total, data })) } static listSubscriptionsForApi (options: { @@ -453,7 +381,8 @@ export class ActorFollowModel extends Model { }) { const { actorId, start, count, sort } = options const where = { - actorId: actorId + state: 'accepted', + actorId } if (options.search) { @@ -465,58 +394,68 @@ export class ActorFollowModel extends Model { }) } - const query = { - attributes: [], - distinct: true, - offset: start, - limit: count, - order: getSort(sort), - where, - include: [ - { - attributes: [ 'id' ], - model: ActorModel.unscoped(), - as: 'ActorFollowing', - required: true, - include: [ - { - model: VideoChannelModel.unscoped(), - required: true, - include: [ - { - attributes: { - exclude: unusedActorAttributesForAPI - }, - model: ActorModel, - required: true + const getQuery = (forCount: boolean) => { + let channelInclude: Includeable[] = [] + + if (forCount !== true) { + channelInclude = [ + { + attributes: { + exclude: unusedActorAttributesForAPI + }, + model: ActorModel, + required: true + }, + { + model: AccountModel.unscoped(), + required: true, + include: [ + { + attributes: { + exclude: unusedActorAttributesForAPI }, - { - model: AccountModel.unscoped(), - required: true, - include: [ - { - attributes: { - exclude: unusedActorAttributesForAPI - }, - model: ActorModel, - required: true - } - ] - } - ] - } - ] - } - ] + model: ActorModel, + required: true + } + ] + } + ] + } + + return { + attributes: forCount === true + ? [] + : SORTABLE_COLUMNS.USER_SUBSCRIPTIONS, + distinct: true, + offset: start, + limit: count, + order: getSort(sort), + where, + include: [ + { + attributes: [ 'id' ], + model: ActorModel.unscoped(), + as: 'ActorFollowing', + required: true, + include: [ + { + model: VideoChannelModel.unscoped(), + required: true, + include: channelInclude + } + ] + } + ] + } } - return ActorFollowModel.findAndCountAll(query) - .then(({ rows, count }) => { - return { - data: rows.map(r => r.ActorFollowing.VideoChannel), - total: count - } - }) + return Promise.all([ + ActorFollowModel.count(getQuery(true)), + ActorFollowModel.findAll(getQuery(false)) + ]).then(([ total, rows ]) => ({ + total, + data: rows.map(r => r.ActorFollowing.VideoChannel) + })) } static async keepUnfollowedInstance (hosts: string[]) { @@ -583,13 +522,15 @@ export class ActorFollowModel extends Model { const totalInstanceFollowing = await ActorFollowModel.count({ where: { - actorId: serverActor.id + actorId: serverActor.id, + state: 'accepted' } }) const totalInstanceFollowers = await ActorFollowModel.count({ where: { - targetActorId: serverActor.id + targetActorId: serverActor.id, + state: 'accepted' } }) @@ -619,7 +560,7 @@ export class ActorFollowModel extends Model { if (serverIds.length === 0) return const me = await getServerActor() - const serverIdsString = createSafeIn(ActorFollowModel, serverIds) + const serverIdsString = createSafeIn(ActorFollowModel.sequelize, serverIds) const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + 'WHERE id IN (' +