X-Git-Url: https://git.librecmc.org/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-comment.ts;h=091cc2a88b77eca3e881925944ecaf32026a51a3;hb=26d6bf6533023326fa017812cf31bbe20c752d36;hp=869a42afef0f14197fa8bf4643c7e87058651017;hpb=b5206dfc455c119b0dcb897bd7144be6ea4d999d;p=oweals%2Fpeertube.git diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 869a42afe..091cc2a88 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -1,20 +1,17 @@ +import * as Bluebird from 'bluebird' +import { uniq } from 'lodash' +import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { getServerActor } from '@server/models/application/application' +import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' +import { VideoPrivacy } from '@shared/models' import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' import { VideoComment } from '../../../shared/models/videos/video-comment.model' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' -import { AccountModel } from '../account/account' -import { ActorModel } from '../activitypub/actor' -import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' -import { VideoModel } from './video' -import { VideoChannelModel } from './video-channel' -import { getServerActor } from '../../helpers/utils' import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { regexpCapture } from '../../helpers/regexp' -import { uniq } from 'lodash' -import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' -import * as Bluebird from 'bluebird' +import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' import { MComment, MCommentAP, @@ -24,26 +21,32 @@ import { MCommentOwnerReplyVideoLight, MCommentOwnerVideo, MCommentOwnerVideoFeed, - MCommentOwnerVideoReply -} from '../../typings/models/video' -import { MUserAccountId } from '@server/typings/models' + MCommentOwnerVideoReply, + MVideoImmutable +} from '../../types/models/video' +import { AccountModel } from '../account/account' +import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' +import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' +import { VideoModel } from './video' +import { VideoChannelModel } from './video-channel' enum ScopeNames { WITH_ACCOUNT = 'WITH_ACCOUNT', + WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API', WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', WITH_VIDEO = 'WITH_VIDEO', ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' } @Scopes(() => ({ - [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { + [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => { return { attributes: { include: [ [ Sequelize.literal( '(' + - 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + + 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + 'SELECT COUNT("replies"."id") - (' + 'SELECT COUNT("replies"."id") ' + 'FROM "videoComment" AS "replies" ' + @@ -56,6 +59,19 @@ enum ScopeNames { ')' ), 'totalReplies' + ], + [ + Sequelize.literal( + '(' + + 'SELECT COUNT("replies"."id") ' + + 'FROM "videoComment" AS "replies" ' + + 'INNER JOIN "video" ON "video"."id" = "replies"."videoId" ' + + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + + 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + + 'AND "replies"."accountId" = "videoChannel"."accountId"' + + ')' + ), + 'totalRepliesFromVideoAuthor' ] ] } @@ -68,6 +84,22 @@ enum ScopeNames { } ] }, + [ScopeNames.WITH_ACCOUNT_FOR_API]: { + include: [ + { + model: AccountModel.unscoped(), + include: [ + { + attributes: { + exclude: unusedActorAttributesForAPI + }, + model: ActorModel, // Default scope includes avatar and server + required: true + } + ] + } + ] + }, [ScopeNames.WITH_IN_REPLY_TO]: { include: [ { @@ -112,6 +144,11 @@ enum ScopeNames { }, { fields: [ 'accountId' ] + }, + { + fields: [ + { name: 'createdAt', order: 'DESC' } + ] } ] }) @@ -244,37 +281,51 @@ export class VideoCommentModel extends Model { } static async listThreadsForApi (parameters: { - videoId: number, - start: number, - count: number, - sort: string, + videoId: number + isVideoOwned: boolean + start: number + count: number + sort: string user?: MUserAccountId }) { - const { videoId, start, count, sort, user } = parameters + const { videoId, isVideoOwned, start, count, sort, user } = parameters - const serverActor = await getServerActor() - const serverAccountId = serverActor.Account.id - const userAccountId = user ? user.Account.id : undefined + const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) const query = { offset: start, limit: count, - order: getSort(sort), + order: getCommentSort(sort), where: { - videoId, - inReplyToCommentId: null, - accountId: { - [Op.notIn]: Sequelize.literal( - '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' - ) - } + [Op.and]: [ + { + videoId + }, + { + inReplyToCommentId: null + }, + { + [Op.or]: [ + { + accountId: { + [Op.notIn]: Sequelize.literal( + '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' + ) + } + }, + { + accountId: null + } + ] + } + ] } } const scopes: (string | ScopeOptions)[] = [ - ScopeNames.WITH_ACCOUNT, + ScopeNames.WITH_ACCOUNT_FOR_API, { - method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] + method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] } ] @@ -287,36 +338,35 @@ export class VideoCommentModel extends Model { } static async listThreadCommentsForApi (parameters: { - videoId: number, - threadId: number, + videoId: number + isVideoOwned: boolean + threadId: number user?: MUserAccountId }) { - const { videoId, threadId, user } = parameters + const { videoId, threadId, user, isVideoOwned } = parameters - const serverActor = await getServerActor() - const serverAccountId = serverActor.Account.id - const userAccountId = user ? user.Account.id : undefined + const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) const query = { order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, where: { videoId, - [ Op.or ]: [ + [Op.or]: [ { id: threadId }, { originCommentId: threadId } ], accountId: { [Op.notIn]: Sequelize.literal( - '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' ) } } } const scopes: any[] = [ - ScopeNames.WITH_ACCOUNT, + ScopeNames.WITH_ACCOUNT_FOR_API, { - method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] + method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] } ] @@ -333,7 +383,7 @@ export class VideoCommentModel extends Model { order: [ [ 'createdAt', order ] ] as Order, where: { id: { - [ Op.in ]: Sequelize.literal('(' + + [Op.in]: Sequelize.literal('(' + 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + 'UNION ' + @@ -342,7 +392,7 @@ export class VideoCommentModel extends Model { ') ' + 'SELECT id FROM children' + ')'), - [ Op.ne ]: comment.id + [Op.ne]: comment.id } }, transaction: t @@ -353,13 +403,23 @@ export class VideoCommentModel extends Model { .findAll(query) } - static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Transaction, order: 'ASC' | 'DESC' = 'ASC') { + static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) { + const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ + videoId: video.id, + isVideoOwned: video.isOwned() + }) + const query = { - order: [ [ 'createdAt', order ] ] as Order, + order: [ [ 'createdAt', 'ASC' ] ] as Order, offset: start, limit: count, where: { - videoId + videoId: video.id, + accountId: { + [Op.notIn]: Sequelize.literal( + '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' + ) + } }, transaction: t } @@ -367,17 +427,36 @@ export class VideoCommentModel extends Model { return VideoCommentModel.findAndCountAll(query) } - static listForFeed (start: number, count: number, videoId?: number): Bluebird { + static async listForFeed (start: number, count: number, videoId?: number): Promise { + const serverActor = await getServerActor() + const query = { order: [ [ 'createdAt', 'DESC' ] ] as Order, offset: start, limit: count, - where: {}, + where: { + deletedAt: null, + accountId: { + [Op.notIn]: Sequelize.literal( + '(' + buildBlockedAccountSQL([ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]) + ')' + ) + } + }, include: [ { attributes: [ 'name', 'uuid' ], model: VideoModel.unscoped(), - required: true + required: true, + where: { + privacy: VideoPrivacy.PUBLIC + }, + include: [ + { + attributes: [ 'accountId' ], + model: VideoChannelModel.unscoped(), + required: true + } + ] } ] } @@ -389,6 +468,43 @@ export class VideoCommentModel extends Model { .findAll(query) } + static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { + const accountWhere = filter.onVideosOfAccount + ? { id: filter.onVideosOfAccount.id } + : {} + + const query = { + limit: 1000, + where: { + deletedAt: null, + accountId: ofAccount.id + }, + include: [ + { + model: VideoModel, + required: true, + include: [ + { + model: VideoChannelModel, + required: true, + include: [ + { + model: AccountModel, + required: true, + where: accountWhere + } + ] + } + ] + } + ] + } + + return VideoCommentModel + .scope([ ScopeNames.WITH_ACCOUNT ]) + .findAll(query) + } + static async getStats () { const totalLocalVideoComments = await VideoCommentModel.count({ include: [ @@ -424,7 +540,9 @@ export class VideoCommentModel extends Model { videoId, accountId: { [Op.notIn]: buildLocalAccountIdsIn() - } + }, + // Do not delete Tombstones + deletedAt: null } } @@ -448,7 +566,7 @@ export class VideoCommentModel extends Model { } isDeleted () { - return null !== this.deletedAt + return this.deletedAt !== null } extractMentions () { @@ -501,6 +619,7 @@ export class VideoCommentModel extends Model { updatedAt: this.updatedAt, deletedAt: this.deletedAt, isDeleted: this.isDeleted(), + totalRepliesFromVideoAuthor: this.get('totalRepliesFromVideoAuthor') || 0, totalReplies: this.get('totalReplies') || 0, account: this.Account ? this.Account.toFormattedJSON() : null } as VideoComment @@ -552,4 +671,24 @@ export class VideoCommentModel extends Model { tag } } + + private static async buildBlockerAccountIds (options: { + videoId: number + isVideoOwned: boolean + user?: MUserAccountId + }) { + const { videoId, user, isVideoOwned } = options + + const serverActor = await getServerActor() + const blockerAccountIds = [ serverActor.Account.id ] + + if (user) blockerAccountIds.push(user.Account.id) + + if (isVideoOwned) { + const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId) + blockerAccountIds.push(videoOwnerAccount.id) + } + + return blockerAccountIds + } }