X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-comment.ts;h=86766a5d1edc841c49139e966fe0ac9ae8443bcb;hb=9a4a9b6c4e193739d542f73ee85cd305067f2aea;hp=1992c2dd88f1d175d6e88f32f3f0594ad3f86008;hpb=da854ddd502cd70685ef779c673b9e63757b8aa0;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 1992c2dd8..86766a5d1 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -1,17 +1,21 @@ import * as Sequelize from 'sequelize' import { - AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, + AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { ActivityTagObject } 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 } from '../../initializers' +import { sendDeleteVideoComment } from '../../lib/activitypub/send' import { AccountModel } from '../account/account' import { ActorModel } from '../activitypub/actor' +import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' import { getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' +import { VideoChannelModel } from './video-channel' enum ScopeNames { WITH_ACCOUNT = 'WITH_ACCOUNT', @@ -46,6 +50,10 @@ enum ScopeNames { { model: () => ServerModel, required: false + }, + { + model: () => AvatarModel, + required: false } ] } @@ -65,7 +73,25 @@ enum ScopeNames { include: [ { model: () => VideoModel, - required: false + required: true, + include: [ + { + model: () => VideoChannelModel.unscoped(), + required: true, + include: [ + { + model: () => AccountModel, + required: true, + include: [ + { + model: () => ActorModel, + required: true + } + ] + } + ] + } + ] } ] } @@ -78,6 +104,10 @@ enum ScopeNames { }, { fields: [ 'videoId', 'originCommentId' ] + }, + { + fields: [ 'url' ], + unique: true } ] }) @@ -103,8 +133,10 @@ export class VideoCommentModel extends Model { @BelongsTo(() => VideoCommentModel, { foreignKey: { + name: 'originCommentId', allowNull: true }, + as: 'OriginVideoComment', onDelete: 'CASCADE' }) OriginVideoComment: VideoCommentModel @@ -115,6 +147,7 @@ export class VideoCommentModel extends Model { @BelongsTo(() => VideoCommentModel, { foreignKey: { + name: 'inReplyToCommentId', allowNull: true }, as: 'InReplyToVideoComment', @@ -146,10 +179,39 @@ export class VideoCommentModel extends Model { }) Account: AccountModel - @AfterDestroy - static sendDeleteIfOwned (instance: VideoCommentModel) { - // TODO - return undefined + @BeforeDestroy + static async sendDeleteIfOwned (instance: VideoCommentModel, options) { + if (!instance.Account || !instance.Account.Actor) { + instance.Account = await instance.$get('Account', { + include: [ ActorModel ], + transaction: options.transaction + }) as AccountModel + } + + if (!instance.Video) { + instance.Video = await instance.$get('Video', { + include: [ + { + model: VideoChannelModel, + include: [ + { + model: AccountModel, + include: [ + { + model: ActorModel + } + ] + } + ] + } + ], + transaction: options.transaction + }) as VideoModel + } + + if (instance.isOwned()) { + await sendDeleteVideoComment(instance, options.transaction) + } } static loadById (id: number, t?: Sequelize.Transaction) { @@ -178,7 +240,7 @@ export class VideoCommentModel extends Model { .findOne(query) } - static loadByUrl (url: string, t?: Sequelize.Transaction) { + static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { const query: IFindOptions = { where: { url @@ -187,14 +249,26 @@ export class VideoCommentModel extends Model { if (t !== undefined) query.transaction = t - return VideoCommentModel.findOne(query) + return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query) + } + + static loadByUrlAndPopulateReplyAndVideo (url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + url + } + } + + if (t !== undefined) query.transaction = t + + return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) } static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { const query = { offset: start, limit: count, - order: [ getSort(sort) ], + order: getSort(sort), where: { videoId, inReplyToCommentId: null @@ -211,7 +285,7 @@ export class VideoCommentModel extends Model { static listThreadCommentsForApi (videoId: number, threadId: number) { const query = { - order: [ [ 'id', 'ASC' ] ], + order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], where: { videoId, [ Sequelize.Op.or ]: [ @@ -229,6 +303,99 @@ export class VideoCommentModel extends Model { }) } + static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') { + const query = { + order: [ [ 'createdAt', order ] ], + where: { + id: { + [ Sequelize.Op.in ]: Sequelize.literal('(' + + 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + + 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + + 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + + 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + + 'SELECT id FROM children' + + ')'), + [ Sequelize.Op.ne ]: comment.id + } + }, + transaction: t + } + + return VideoCommentModel + .scope([ ScopeNames.WITH_ACCOUNT ]) + .findAll(query) + } + + static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') { + const query = { + order: [ [ 'createdAt', order ] ], + offset: start, + limit: count, + where: { + videoId + }, + transaction: t + } + + return VideoCommentModel.findAndCountAll(query) + } + + static listForFeed (start: number, count: number, videoId?: number) { + const query = { + order: [ [ 'createdAt', 'DESC' ] ], + offset: start, + limit: count, + where: {}, + include: [ + { + attributes: [ 'name' ], + model: VideoModel.unscoped(), + required: true + } + ] + } + + if (videoId) query.where['videoId'] = videoId + + return VideoCommentModel + .scope([ ScopeNames.WITH_ACCOUNT ]) + .findAll(query) + } + + static async getStats () { + const totalLocalVideoComments = await VideoCommentModel.count({ + include: [ + { + model: AccountModel, + required: true, + include: [ + { + model: ActorModel, + required: true, + where: { + serverId: null + } + } + ] + } + ] + }) + const totalVideoComments = await VideoCommentModel.count() + + return { + totalLocalVideoComments, + totalVideoComments + } + } + + getThreadId (): number { + return this.originCommentId || this.id + } + + isOwned () { + return this.Account.isOwned() + } + toFormattedJSON () { return { id: this.id, @@ -240,14 +407,11 @@ export class VideoCommentModel extends Model { createdAt: this.createdAt, updatedAt: this.updatedAt, totalReplies: this.get('totalReplies') || 0, - account: { - name: this.Account.name, - host: this.Account.Actor.getHost() - } + account: this.Account.toFormattedJSON() } as VideoComment } - toActivityPubObject (): VideoCommentObject { + toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject { let inReplyTo: string // New thread, so in AS we reply to the video if (this.inReplyToCommentId === null) { @@ -256,6 +420,17 @@ export class VideoCommentModel extends Model { inReplyTo = this.InReplyToVideoComment.url } + const tag: ActivityTagObject[] = [] + for (const parentComment of threadParentComments) { + const actor = parentComment.Account.Actor + + tag.push({ + type: 'Mention', + href: actor.url, + name: `@${actor.preferredUsername}@${actor.getHost()}` + }) + } + return { type: 'Note' as 'Note', id: this.url, @@ -264,7 +439,8 @@ export class VideoCommentModel extends Model { updated: this.updatedAt.toISOString(), published: this.createdAt.toISOString(), url: this.url, - attributedTo: this.Account.Actor.url + attributedTo: this.Account.Actor.url, + tag } } }