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]: {
as: 'InReplyToVideoComment',
onDelete: 'CASCADE'
})
- InReplyToVideoComment: VideoCommentModel
+ InReplyToVideoComment: VideoCommentModel | null
@ForeignKey(() => VideoModel)
@Column
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),
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 }
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, "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
}
}
+ 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,