+ static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Bluebird<MCommentOwner[]> {
+ const query = {
+ order: [ [ 'createdAt', order ] ] as Order,
+ where: {
+ id: {
+ [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' +
+ ')'),
+ [Op.ne]: comment.id
+ }
+ },
+ transaction: t
+ }
+
+ return VideoCommentModel
+ .scope([ ScopeNames.WITH_ACCOUNT ])
+ .findAll(query)
+ }
+
+ 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', 'ASC' ] ] as Order,
+ offset: start,
+ limit: count,
+ where: {
+ videoId: video.id,
+ accountId: {
+ [Op.notIn]: Sequelize.literal(
+ '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
+ )
+ }
+ },
+ transaction: t
+ }
+
+ return VideoCommentModel.findAndCountAll<MComment>(query)
+ }
+
+ static async listForFeed (parameters: {
+ start: number
+ count: number
+ videoId?: number
+ accountId?: number
+ videoChannelId?: number
+ }): Promise<MCommentOwnerVideoFeed[]> {
+ const serverActor = await getServerActor()
+ const { start, count, videoId, accountId, videoChannelId } = parameters
+
+ const whereAnd: WhereOptions[] = buildBlockedAccountSQLOptimized(
+ '"VideoCommentModel"."accountId"',
+ [ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]
+ )
+
+ if (accountId) {
+ whereAnd.push({
+ [Op.eq]: accountId
+ })
+ }
+
+ const accountWhere = {
+ [Op.and]: whereAnd
+ }
+
+ const videoChannelWhere = videoChannelId ? { id: videoChannelId } : undefined
+
+ const query = {
+ order: [ [ 'createdAt', 'DESC' ] ] as Order,
+ offset: start,
+ limit: count,
+ where: {
+ deletedAt: null,
+ accountId: accountWhere
+ },
+ include: [
+ {
+ attributes: [ 'name', 'uuid' ],
+ model: VideoModel.unscoped(),
+ required: true,
+ where: {
+ privacy: VideoPrivacy.PUBLIC
+ },
+ include: [
+ {
+ attributes: [ 'accountId' ],
+ model: VideoChannelModel.unscoped(),
+ required: true,
+ where: videoChannelWhere
+ }
+ ]
+ }
+ ]
+ }
+
+ if (videoId) query.where['videoId'] = videoId
+
+ return VideoCommentModel
+ .scope([ ScopeNames.WITH_ACCOUNT ])
+ .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: [
+ {
+ 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: {
+ [Op.lt]: beforeUpdatedAt
+ },
+ videoId,
+ accountId: {
+ [Op.notIn]: buildLocalAccountIdsIn()
+ },
+ // Do not delete Tombstones
+ deletedAt: null
+ }
+ }
+
+ return VideoCommentModel.destroy(query)
+ }
+
+ getCommentStaticPath () {
+ return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
+ }
+
+ getThreadId (): number {
+ return this.originCommentId || this.id
+ }
+
+ isOwned () {
+ if (!this.Account) {
+ return false
+ }
+
+ return this.Account.isOwned()
+ }
+
+ isDeleted () {
+ return this.deletedAt !== null
+ }
+
+ extractMentions () {
+ let result: string[] = []
+
+ const localMention = `@(${actorNameAlphabet}+)`
+ const remoteMention = `${localMention}@${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 (this: MCommentFormattable) {