import * as Sequelize from 'sequelize'
import {
- AllowNull, BeforeDestroy, 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 { CONFIG, 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 { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
import { VideoChannelModel } from './video-channel'
+import { getServerActor } from '../../helpers/utils'
+import { UserModel } from '../account/user'
+import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
+import { regexpCapture } from '../../helpers/regexp'
+import { uniq } from 'lodash'
enum ScopeNames {
WITH_ACCOUNT = 'WITH_ACCOUNT',
}
@Scopes({
- [ScopeNames.ATTRIBUTES_FOR_API]: {
- attributes: {
- include: [
- [
- Sequelize.literal(
- '(SELECT COUNT("replies"."id") ' +
- 'FROM "videoComment" AS "replies" ' +
- 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")'
- ),
- 'totalReplies'
+ [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
+ return {
+ attributes: {
+ include: [
+ [
+ Sequelize.literal(
+ '(' +
+ 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' +
+ 'SELECT COUNT("replies"."id") - (' +
+ 'SELECT COUNT("replies"."id") ' +
+ 'FROM "videoComment" AS "replies" ' +
+ 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
+ 'AND "accountId" IN (SELECT "id" FROM "blocklist")' +
+ ')' +
+ 'FROM "videoComment" AS "replies" ' +
+ 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
+ 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
+ ')'
+ ),
+ 'totalReplies'
+ ]
]
- ]
+ }
}
},
[ScopeNames.WITH_ACCOUNT]: {
},
{
fields: [ 'videoId', 'originCommentId' ]
+ },
+ {
+ fields: [ 'url' ],
+ unique: true
+ },
+ {
+ fields: [ 'accountId' ]
}
]
})
as: 'InReplyToVideoComment',
onDelete: 'CASCADE'
})
- InReplyToVideoComment: VideoCommentModel
+ InReplyToVideoComment: VideoCommentModel | null
@ForeignKey(() => VideoModel)
@Column
}) 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)
}
return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
}
- static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
+ static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
+ const serverActor = await getServerActor()
+ const serverAccountId = serverActor.Account.id
+ const userAccountId = user ? user.Account.id : undefined
+
const query = {
offset: start,
limit: count,
- order: [ getSort(sort) ],
+ order: getSort(sort),
where: {
videoId,
- inReplyToCommentId: null
+ inReplyToCommentId: null,
+ accountId: {
+ [Sequelize.Op.notIn]: Sequelize.literal(
+ '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
+ )
+ }
}
}
+ // FIXME: typings
+ const scopes: any[] = [
+ ScopeNames.WITH_ACCOUNT,
+ {
+ method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
+ }
+ ]
+
return VideoCommentModel
- .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
+ .scope(scopes)
.findAndCountAll(query)
.then(({ rows, count }) => {
return { total: count, data: rows }
})
}
- static listThreadCommentsForApi (videoId: number, threadId: number) {
+ static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
+ const serverActor = await getServerActor()
+ const serverAccountId = serverActor.Account.id
+ const userAccountId = user ? user.Account.id : undefined
+
const query = {
order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
where: {
[ Sequelize.Op.or ]: [
{ id: threadId },
{ originCommentId: threadId }
- ]
+ ],
+ accountId: {
+ [Sequelize.Op.notIn]: Sequelize.literal(
+ '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
+ )
+ }
}
}
+ const scopes: any[] = [
+ ScopeNames.WITH_ACCOUNT,
+ {
+ method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
+ }
+ ]
+
return VideoCommentModel
- .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
+ .scope(scopes)
.findAndCountAll(query)
.then(({ rows, count }) => {
return { total: count, data: rows }
const query = {
order: [ [ 'createdAt', order ] ],
where: {
- [ Sequelize.Op.or ]: [
- { id: comment.getThreadId() },
- { originCommentId: comment.getThreadId() }
- ],
id: {
+ [ Sequelize.Op.in ]: Sequelize.literal('(' +
+ 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
+ `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
+ 'UNION ' +
+ 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
+ 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
+ ') ' +
+ 'SELECT id FROM children' +
+ ')'),
[ Sequelize.Op.ne ]: comment.id
- },
- createdAt: {
- [ Sequelize.Op.lt ]: comment.createdAt
}
},
transaction: t
.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', 'uuid' ],
+ 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
+ }
+ }
+
+ static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) {
+ const query = {
+ where: {
+ updatedAt: {
+ [Sequelize.Op.lt]: beforeUpdatedAt
+ },
+ videoId
+ }
+ }
+
+ return VideoCommentModel.destroy(query)
+ }
+
+ getCommentStaticPath () {
+ return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
+ }
+
getThreadId (): number {
return this.originCommentId || this.id
}
return this.Account.isOwned()
}
+ extractMentions () {
+ let result: string[] = []
+
+ const localMention = `@(${actorNameAlphabet}+)`
+ const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}`
+
+ const mentionRegex = this.isOwned()
+ ? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
+ : '(?:' + remoteMention + ')'
+
+ const firstMentionRegex = new RegExp(`^${mentionRegex} `, 'g')
+ const endMentionRegex = new RegExp(` ${mentionRegex}$`, 'g')
+ const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g')
+
+ result = result.concat(
+ regexpCapture(this.text, firstMentionRegex)
+ .map(([ , username1, username2 ]) => username1 || username2),
+
+ regexpCapture(this.text, endMentionRegex)
+ .map(([ , username1, username2 ]) => username1 || username2),
+
+ regexpCapture(this.text, remoteMentionsRegex)
+ .map(([ , username ]) => username)
+ )
+
+ // Include local mentions
+ if (this.isOwned()) {
+ const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
+
+ result = result.concat(
+ regexpCapture(this.text, localMentionsRegex)
+ .map(([ , username ]) => username)
+ )
+ }
+
+ return uniq(result)
+ }
+
toFormattedJSON () {
return {
id: this.id,