X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Fvideo%2Fvideo-comment.ts;h=bf8da924d5555ee5d06911941100f5ee47881c7d;hb=066e94c5382a761180c7d82fa24b31b66dbeaca4;hp=92c0c6112fe08f37eb252ac600481261062370e2;hpb=6d8524702874120a4667269a81a61e3c7c5e300d;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 92c0c6112..bf8da924d 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -1,19 +1,113 @@ import * as Sequelize from 'sequelize' import { - AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IFindOptions, Is, IsUUID, Model, Table, + AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' +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 { throwIfNotValid } from '../utils' +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', + WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', + WITH_VIDEO = 'WITH_VIDEO', + ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' +} + +@Scopes({ + [ScopeNames.ATTRIBUTES_FOR_API]: { + attributes: { + include: [ + [ + Sequelize.literal( + '(SELECT COUNT("replies"."id") ' + + 'FROM "videoComment" AS "replies" ' + + 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' + ), + 'totalReplies' + ] + ] + } + }, + [ScopeNames.WITH_ACCOUNT]: { + include: [ + { + model: () => AccountModel, + include: [ + { + model: () => ActorModel, + include: [ + { + model: () => ServerModel, + required: false + }, + { + model: () => AvatarModel, + required: false + } + ] + } + ] + } + ] + }, + [ScopeNames.WITH_IN_REPLY_TO]: { + include: [ + { + model: () => VideoCommentModel, + as: 'InReplyToVideoComment' + } + ] + }, + [ScopeNames.WITH_VIDEO]: { + include: [ + { + model: () => VideoModel, + required: true, + include: [ + { + model: () => VideoChannelModel.unscoped(), + required: true, + include: [ + { + model: () => AccountModel, + required: true, + include: [ + { + model: () => ActorModel, + required: true + } + ] + } + ] + } + ] + } + ] + } +}) @Table({ tableName: 'videoComment', indexes: [ { fields: [ 'videoId' ] + }, + { + fields: [ 'videoId', 'originCommentId' ] + }, + { + fields: [ 'url' ], + unique: true } ] }) @@ -39,8 +133,10 @@ export class VideoCommentModel extends Model { @BelongsTo(() => VideoCommentModel, { foreignKey: { + name: 'originCommentId', allowNull: true }, + as: 'OriginVideoComment', onDelete: 'CASCADE' }) OriginVideoComment: VideoCommentModel @@ -51,8 +147,10 @@ export class VideoCommentModel extends Model { @BelongsTo(() => VideoCommentModel, { foreignKey: { + name: 'inReplyToCommentId', allowNull: true }, + as: 'InReplyToVideoComment', onDelete: 'CASCADE' }) InReplyToVideoComment: VideoCommentModel @@ -69,22 +167,57 @@ export class VideoCommentModel extends Model { }) Video: VideoModel - @ForeignKey(() => ActorModel) + @ForeignKey(() => AccountModel) @Column - actorId: number + accountId: number - @BelongsTo(() => ActorModel, { + @BelongsTo(() => AccountModel, { foreignKey: { allowNull: false }, onDelete: 'CASCADE' }) - Actor: ActorModel + Account: AccountModel + + @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 + } - static loadByUrl (url: string, t?: Sequelize.Transaction) { + if (instance.isOwned()) { + await sendDeleteVideoComment(instance, options.transaction) + } + } + + static loadById (id: number, t?: Sequelize.Transaction) { const query: IFindOptions = { where: { - url + id } } @@ -92,4 +225,186 @@ export class VideoCommentModel extends Model { return VideoCommentModel.findOne(query) } + + static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + id + } + } + + if (t !== undefined) query.transaction = t + + return VideoCommentModel + .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ]) + .findOne(query) + } + + static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + url + } + } + + if (t !== undefined) query.transaction = t + + 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), + where: { + videoId, + inReplyToCommentId: null + } + } + + return VideoCommentModel + .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) + .findAndCountAll(query) + .then(({ rows, count }) => { + return { total: count, data: rows } + }) + } + + static listThreadCommentsForApi (videoId: number, threadId: number) { + const query = { + order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], + where: { + videoId, + [ Sequelize.Op.or ]: [ + { id: threadId }, + { originCommentId: threadId } + ] + } + } + + return VideoCommentModel + .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) + .findAndCountAll(query) + .then(({ rows, count }) => { + return { total: count, data: rows } + }) + } + + static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') { + const query = { + order: [ [ 'createdAt', order ] ], + where: { + [ Sequelize.Op.or ]: [ + { id: comment.getThreadId() }, + { originCommentId: comment.getThreadId() } + ], + id: { + [ Sequelize.Op.ne ]: comment.id + }, + createdAt: { + [ Sequelize.Op.lt ]: comment.createdAt + } + }, + transaction: t + } + + 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, + url: this.url, + text: this.text, + threadId: this.originCommentId || this.id, + inReplyToCommentId: this.inReplyToCommentId || null, + videoId: this.videoId, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + totalReplies: this.get('totalReplies') || 0, + account: this.Account.toFormattedJSON() + } as VideoComment + } + + toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject { + let inReplyTo: string + // New thread, so in AS we reply to the video + if (this.inReplyToCommentId === null) { + inReplyTo = this.Video.url + } else { + 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, + content: this.text, + inReplyTo, + updated: this.updatedAt.toISOString(), + published: this.createdAt.toISOString(), + url: this.url, + attributedTo: this.Account.Actor.url, + tag + } + } }