mirror of https://github.com/Chocobozzz/PeerTube
Add ability to bulk delete comments
parent
99139e7753
commit
444c0a0e01
|
@ -0,0 +1,41 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import { asyncMiddleware, authenticate } from '../../middlewares'
|
||||||
|
import { bulkRemoveCommentsOfValidator } from '@server/middlewares/validators/bulk'
|
||||||
|
import { VideoCommentModel } from '@server/models/video/video-comment'
|
||||||
|
import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
|
||||||
|
import { removeComment } from '@server/lib/video-comment'
|
||||||
|
|
||||||
|
const bulkRouter = express.Router()
|
||||||
|
|
||||||
|
bulkRouter.post('/remove-comments-of',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(bulkRemoveCommentsOfValidator),
|
||||||
|
asyncMiddleware(bulkRemoveCommentsOf)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
bulkRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function bulkRemoveCommentsOf (req: express.Request, res: express.Response) {
|
||||||
|
const account = res.locals.account
|
||||||
|
const body = req.body as BulkRemoveCommentsOfBody
|
||||||
|
const user = res.locals.oauth.token.User
|
||||||
|
|
||||||
|
const filter = body.scope === 'my-videos'
|
||||||
|
? { onVideosOfAccount: user.Account }
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const comments = await VideoCommentModel.listForBulkDelete(account, filter)
|
||||||
|
|
||||||
|
// Don't wait result
|
||||||
|
res.sendStatus(204)
|
||||||
|
|
||||||
|
for (const comment of comments) {
|
||||||
|
await removeComment(comment)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,21 @@
|
||||||
|
import * as cors from 'cors'
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
|
import * as RateLimit from 'express-rate-limit'
|
||||||
|
import { badRequest } from '../../helpers/express-utils'
|
||||||
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
import { accountsRouter } from './accounts'
|
||||||
|
import { bulkRouter } from './bulk'
|
||||||
import { configRouter } from './config'
|
import { configRouter } from './config'
|
||||||
import { jobsRouter } from './jobs'
|
import { jobsRouter } from './jobs'
|
||||||
import { oauthClientsRouter } from './oauth-clients'
|
import { oauthClientsRouter } from './oauth-clients'
|
||||||
|
import { overviewsRouter } from './overviews'
|
||||||
|
import { pluginRouter } from './plugins'
|
||||||
|
import { searchRouter } from './search'
|
||||||
import { serverRouter } from './server'
|
import { serverRouter } from './server'
|
||||||
import { usersRouter } from './users'
|
import { usersRouter } from './users'
|
||||||
import { accountsRouter } from './accounts'
|
|
||||||
import { videosRouter } from './videos'
|
|
||||||
import { badRequest } from '../../helpers/express-utils'
|
|
||||||
import { videoChannelRouter } from './video-channel'
|
import { videoChannelRouter } from './video-channel'
|
||||||
import * as cors from 'cors'
|
|
||||||
import { searchRouter } from './search'
|
|
||||||
import { overviewsRouter } from './overviews'
|
|
||||||
import { videoPlaylistRouter } from './video-playlist'
|
import { videoPlaylistRouter } from './video-playlist'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { videosRouter } from './videos'
|
||||||
import { pluginRouter } from './plugins'
|
|
||||||
import * as RateLimit from 'express-rate-limit'
|
|
||||||
|
|
||||||
const apiRouter = express.Router()
|
const apiRouter = express.Router()
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ const apiRateLimiter = RateLimit({
|
||||||
apiRouter.use(apiRateLimiter)
|
apiRouter.use(apiRateLimiter)
|
||||||
|
|
||||||
apiRouter.use('/server', serverRouter)
|
apiRouter.use('/server', serverRouter)
|
||||||
|
apiRouter.use('/bulk', bulkRouter)
|
||||||
apiRouter.use('/oauth-clients', oauthClientsRouter)
|
apiRouter.use('/oauth-clients', oauthClientsRouter)
|
||||||
apiRouter.use('/config', configRouter)
|
apiRouter.use('/config', configRouter)
|
||||||
apiRouter.use('/users', usersRouter)
|
apiRouter.use('/users', usersRouter)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { cloneDeep } from 'lodash'
|
|
||||||
import { ResultList } from '../../../../shared/models'
|
import { ResultList } from '../../../../shared/models'
|
||||||
import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
|
import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
|
||||||
import { getFormattedObjects } from '../../../helpers/utils'
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
import { buildFormattedCommentTree, createVideoComment, markCommentAsDeleted } from '../../../lib/video-comment'
|
import { Notifier } from '../../../lib/notifier'
|
||||||
|
import { Hooks } from '../../../lib/plugins/hooks'
|
||||||
|
import { buildFormattedCommentTree, createVideoComment, removeComment } from '../../../lib/video-comment'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
|
@ -23,12 +24,8 @@ import {
|
||||||
removeVideoCommentValidator,
|
removeVideoCommentValidator,
|
||||||
videoCommentThreadsSortValidator
|
videoCommentThreadsSortValidator
|
||||||
} from '../../../middlewares/validators'
|
} from '../../../middlewares/validators'
|
||||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
|
||||||
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
|
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { Notifier } from '../../../lib/notifier'
|
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
import { Hooks } from '../../../lib/plugins/hooks'
|
|
||||||
import { sendDeleteVideoComment } from '../../../lib/activitypub/send'
|
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('comments')
|
const auditLogger = auditLoggerFactory('comments')
|
||||||
const videoCommentRouter = express.Router()
|
const videoCommentRouter = express.Router()
|
||||||
|
@ -149,9 +146,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
|
||||||
|
|
||||||
Hooks.runAction('action:api.video-thread.created', { comment })
|
Hooks.runAction('action:api.video-thread.created', { comment })
|
||||||
|
|
||||||
return res.json({
|
return res.json({ comment: comment.toFormattedJSON() })
|
||||||
comment: comment.toFormattedJSON()
|
|
||||||
}).end()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addVideoCommentReply (req: express.Request, res: express.Response) {
|
async function addVideoCommentReply (req: express.Request, res: express.Response) {
|
||||||
|
@ -173,27 +168,15 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
|
||||||
|
|
||||||
Hooks.runAction('action:api.video-comment-reply.created', { comment })
|
Hooks.runAction('action:api.video-comment-reply.created', { comment })
|
||||||
|
|
||||||
return res.json({ comment: comment.toFormattedJSON() }).end()
|
return res.json({ comment: comment.toFormattedJSON() })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeVideoComment (req: express.Request, res: express.Response) {
|
async function removeVideoComment (req: express.Request, res: express.Response) {
|
||||||
const videoCommentInstance = res.locals.videoCommentFull
|
const videoCommentInstance = res.locals.videoCommentFull
|
||||||
const videoCommentInstanceBefore = cloneDeep(videoCommentInstance)
|
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await removeComment(videoCommentInstance)
|
||||||
if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
|
|
||||||
await sendDeleteVideoComment(videoCommentInstance, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
markCommentAsDeleted(videoCommentInstance)
|
|
||||||
|
|
||||||
await videoCommentInstance.save()
|
|
||||||
})
|
|
||||||
|
|
||||||
auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
|
auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
|
||||||
logger.info('Video comment %d deleted.', videoCommentInstance.id)
|
|
||||||
|
|
||||||
Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore })
|
return res.type('json').status(204)
|
||||||
|
|
||||||
return res.type('json').status(204).end()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
function isBulkRemoveCommentsOfScopeValid (value: string) {
|
||||||
|
return value === 'my-videos' || value === 'instance'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
isBulkRemoveCommentsOfScopeValid
|
||||||
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
import { Transaction } from 'sequelize'
|
import { Transaction } from 'sequelize'
|
||||||
|
import { getServerActor } from '@server/models/application/application'
|
||||||
import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
|
import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
|
||||||
|
import { logger } from '../../../helpers/logger'
|
||||||
import { ActorModel } from '../../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
import { VideoShareModel } from '../../../models/video/video-share'
|
import { VideoShareModel } from '../../../models/video/video-share'
|
||||||
|
import { MActorUrl } from '../../../typings/models'
|
||||||
|
import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
|
||||||
|
import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
|
||||||
import { getDeleteActivityPubUrl } from '../url'
|
import { getDeleteActivityPubUrl } from '../url'
|
||||||
import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
|
import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
|
||||||
import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
|
|
||||||
import { logger } from '../../../helpers/logger'
|
|
||||||
import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
|
|
||||||
import { MActorUrl } from '../../../typings/models'
|
|
||||||
import { getServerActor } from '@server/models/application/application'
|
|
||||||
|
|
||||||
async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
|
async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
|
||||||
logger.info('Creating job to broadcast delete of video %s.', video.url)
|
logger.info('Creating job to broadcast delete of video %s.', video.url)
|
||||||
|
@ -42,7 +42,7 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
|
||||||
return broadcastToFollowers(activity, byActor, actorsInvolved, t)
|
return broadcastToFollowers(activity, byActor, actorsInvolved, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendDeleteVideoComment (videoComment: MCommentOwnerVideoReply, t: Transaction) {
|
async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, t: Transaction) {
|
||||||
logger.info('Creating job to send delete of comment %s.', videoComment.url)
|
logger.info('Creating job to send delete of comment %s.', videoComment.url)
|
||||||
|
|
||||||
const isVideoOrigin = videoComment.Video.isOwned()
|
const isVideoOrigin = videoComment.Video.isOwned()
|
||||||
|
|
|
@ -1,10 +1,32 @@
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
|
import { logger } from '@server/helpers/logger'
|
||||||
|
import { sequelizeTypescript } from '@server/initializers/database'
|
||||||
import { ResultList } from '../../shared/models'
|
import { ResultList } from '../../shared/models'
|
||||||
import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
|
import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
|
||||||
import { VideoCommentModel } from '../models/video/video-comment'
|
import { VideoCommentModel } from '../models/video/video-comment'
|
||||||
|
import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight, MCommentOwnerVideo } from '../typings/models'
|
||||||
|
import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send'
|
||||||
import { getVideoCommentActivityPubUrl } from './activitypub/url'
|
import { getVideoCommentActivityPubUrl } from './activitypub/url'
|
||||||
import { sendCreateVideoComment } from './activitypub/send'
|
import { Hooks } from './plugins/hooks'
|
||||||
import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models'
|
|
||||||
|
async function removeComment (videoCommentInstance: MCommentOwnerVideo) {
|
||||||
|
const videoCommentInstanceBefore = cloneDeep(videoCommentInstance)
|
||||||
|
|
||||||
|
await sequelizeTypescript.transaction(async t => {
|
||||||
|
if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
|
||||||
|
await sendDeleteVideoComment(videoCommentInstance, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
markCommentAsDeleted(videoCommentInstance)
|
||||||
|
|
||||||
|
await videoCommentInstance.save()
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info('Video comment %d deleted.', videoCommentInstance.id)
|
||||||
|
|
||||||
|
Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore })
|
||||||
|
}
|
||||||
|
|
||||||
async function createVideoComment (obj: {
|
async function createVideoComment (obj: {
|
||||||
text: string
|
text: string
|
||||||
|
@ -73,7 +95,7 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
|
||||||
return thread
|
return thread
|
||||||
}
|
}
|
||||||
|
|
||||||
function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
|
function markCommentAsDeleted (comment: MComment): void {
|
||||||
comment.text = ''
|
comment.text = ''
|
||||||
comment.deletedAt = new Date()
|
comment.deletedAt = new Date()
|
||||||
comment.accountId = null
|
comment.accountId = null
|
||||||
|
@ -82,6 +104,7 @@ function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
removeComment,
|
||||||
createVideoComment,
|
createVideoComment,
|
||||||
buildFormattedCommentTree,
|
buildFormattedCommentTree,
|
||||||
markCommentAsDeleted
|
markCommentAsDeleted
|
||||||
|
|
|
@ -24,8 +24,7 @@ const blockAccountValidator = [
|
||||||
|
|
||||||
if (user.Account.id === accountToBlock.id) {
|
if (user.Account.id === accountToBlock.id) {
|
||||||
res.status(409)
|
res.status(409)
|
||||||
.send({ error: 'You cannot block yourself.' })
|
.json({ error: 'You cannot block yourself.' })
|
||||||
.end()
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -80,8 +79,7 @@ const blockServerValidator = [
|
||||||
|
|
||||||
if (host === WEBSERVER.HOST) {
|
if (host === WEBSERVER.HOST) {
|
||||||
return res.status(409)
|
return res.status(409)
|
||||||
.send({ error: 'You cannot block your own server.' })
|
.json({ error: 'You cannot block your own server.' })
|
||||||
.end()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = await ServerModel.loadOrCreateByHost(host)
|
const server = await ServerModel.loadOrCreateByHost(host)
|
||||||
|
@ -139,8 +137,7 @@ async function doesUnblockAccountExist (accountId: number, targetAccountId: numb
|
||||||
const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
|
const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
|
||||||
if (!accountBlock) {
|
if (!accountBlock) {
|
||||||
res.status(404)
|
res.status(404)
|
||||||
.send({ error: 'Account block entry not found.' })
|
.json({ error: 'Account block entry not found.' })
|
||||||
.end()
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -154,8 +151,7 @@ async function doesUnblockServerExist (accountId: number, host: string, res: exp
|
||||||
const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
|
const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
|
||||||
if (!serverBlock) {
|
if (!serverBlock) {
|
||||||
res.status(404)
|
res.status(404)
|
||||||
.send({ error: 'Server block entry not found.' })
|
.json({ error: 'Server block entry not found.' })
|
||||||
.end()
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import { body } from 'express-validator'
|
||||||
|
import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk'
|
||||||
|
import { doesAccountNameWithHostExist } from '@server/helpers/middlewares'
|
||||||
|
import { UserRight } from '@shared/models'
|
||||||
|
import { BulkRemoveCommentsOfBody } from '@shared/models/bulk/bulk-remove-comments-of-body.model'
|
||||||
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { areValidationErrors } from './utils'
|
||||||
|
|
||||||
|
const bulkRemoveCommentsOfValidator = [
|
||||||
|
body('accountName').exists().withMessage('Should have an account name with host'),
|
||||||
|
body('scope')
|
||||||
|
.custom(isBulkRemoveCommentsOfScopeValid).withMessage('Should have a valid scope'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking bulkRemoveCommentsOfValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
if (!await doesAccountNameWithHostExist(req.body.accountName, res)) return
|
||||||
|
|
||||||
|
const user = res.locals.oauth.token.User
|
||||||
|
const body = req.body as BulkRemoveCommentsOfBody
|
||||||
|
|
||||||
|
if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) {
|
||||||
|
return res.status(403)
|
||||||
|
.json({
|
||||||
|
error: 'User cannot remove any comments of this instance.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
bulkRemoveCommentsOfValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
|
@ -1,19 +1,17 @@
|
||||||
|
import * as Bluebird from 'bluebird'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
|
import { getServerActor } from '@server/models/application/application'
|
||||||
|
import { MAccount, MAccountId, MUserAccountId } from '@server/typings/models'
|
||||||
|
import { VideoPrivacy } from '@shared/models'
|
||||||
import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
|
import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
|
||||||
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
|
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
|
||||||
import { VideoComment } from '../../../shared/models/videos/video-comment.model'
|
import { VideoComment } from '../../../shared/models/videos/video-comment.model'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
|
||||||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
|
||||||
import { AccountModel } from '../account/account'
|
|
||||||
import { ActorModel } from '../activitypub/actor'
|
|
||||||
import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
|
|
||||||
import { VideoModel } from './video'
|
|
||||||
import { VideoChannelModel } from './video-channel'
|
|
||||||
import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
|
import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
|
||||||
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||||
import { regexpCapture } from '../../helpers/regexp'
|
import { regexpCapture } from '../../helpers/regexp'
|
||||||
import { uniq } from 'lodash'
|
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||||
import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
|
|
||||||
import * as Bluebird from 'bluebird'
|
|
||||||
import {
|
import {
|
||||||
MComment,
|
MComment,
|
||||||
MCommentAP,
|
MCommentAP,
|
||||||
|
@ -25,9 +23,11 @@ import {
|
||||||
MCommentOwnerVideoFeed,
|
MCommentOwnerVideoFeed,
|
||||||
MCommentOwnerVideoReply
|
MCommentOwnerVideoReply
|
||||||
} from '../../typings/models/video'
|
} from '../../typings/models/video'
|
||||||
import { MUserAccountId } from '@server/typings/models'
|
import { AccountModel } from '../account/account'
|
||||||
import { VideoPrivacy } from '@shared/models'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
|
||||||
|
import { VideoModel } from './video'
|
||||||
|
import { VideoChannelModel } from './video-channel'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||||
|
@ -415,6 +415,43 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
.findAll(query)
|
.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 () {
|
static async getStats () {
|
||||||
const totalLocalVideoComments = await VideoCommentModel.count({
|
const totalLocalVideoComments = await VideoCommentModel.count({
|
||||||
include: [
|
include: [
|
||||||
|
@ -450,7 +487,9 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
videoId,
|
videoId,
|
||||||
accountId: {
|
accountId: {
|
||||||
[Op.notIn]: buildLocalAccountIdsIn()
|
[Op.notIn]: buildLocalAccountIdsIn()
|
||||||
}
|
},
|
||||||
|
// Do not delete Tombstones
|
||||||
|
deletedAt: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import 'mocha'
|
||||||
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createUser,
|
||||||
|
flushAndRunServer,
|
||||||
|
ServerInfo,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
userLogin
|
||||||
|
} from '../../../../shared/extra-utils'
|
||||||
|
import { makePostBodyRequest } from '../../../../shared/extra-utils/requests/requests'
|
||||||
|
|
||||||
|
describe('Test bulk API validators', function () {
|
||||||
|
let server: ServerInfo
|
||||||
|
let userAccessToken: string
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
server = await flushAndRunServer(1)
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
|
const user = { username: 'user1', password: 'password' }
|
||||||
|
await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
|
||||||
|
|
||||||
|
userAccessToken = await userLogin(server, user)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When removing comments of', function () {
|
||||||
|
const path = '/api/v1/bulk/remove-comments-of'
|
||||||
|
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user1', scope: 'my-videos' },
|
||||||
|
statusCodeExpected: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown account', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user2', scope: 'my-videos' },
|
||||||
|
statusCodeExpected: 404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid scope', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user1', scope: 'my-videoss' },
|
||||||
|
statusCodeExpected: 400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to delete comments of the instance without the appropriate rights', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: userAccessToken,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user1', scope: 'instance' },
|
||||||
|
statusCodeExpected: 403
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user1', scope: 'instance' },
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,6 @@
|
||||||
import './accounts'
|
import './accounts'
|
||||||
import './blocklist'
|
import './blocklist'
|
||||||
|
import './bulk'
|
||||||
import './config'
|
import './config'
|
||||||
import './contact-form'
|
import './contact-form'
|
||||||
import './debug'
|
import './debug'
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import 'mocha'
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import { VideoComment } from '@shared/models/videos/video-comment.model'
|
||||||
|
import {
|
||||||
|
addVideoCommentThread,
|
||||||
|
bulkRemoveCommentsOf,
|
||||||
|
cleanupTests,
|
||||||
|
createUser,
|
||||||
|
flushAndRunMultipleServers,
|
||||||
|
getVideoCommentThreads,
|
||||||
|
getVideosList,
|
||||||
|
ServerInfo,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
uploadVideo,
|
||||||
|
userLogin,
|
||||||
|
waitJobs,
|
||||||
|
addVideoCommentReply
|
||||||
|
} from '../../../../shared/extra-utils/index'
|
||||||
|
import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
|
||||||
|
import { Video } from '@shared/models'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test bulk actions', function () {
|
||||||
|
const commentsUser3: { videoId: number, commentId: number }[] = []
|
||||||
|
|
||||||
|
let servers: ServerInfo[] = []
|
||||||
|
let user1AccessToken: string
|
||||||
|
let user2AccessToken: string
|
||||||
|
let user3AccessToken: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
servers = await flushAndRunMultipleServers(2)
|
||||||
|
|
||||||
|
// Get the access tokens
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
{
|
||||||
|
const user = { username: 'user1', password: 'password' }
|
||||||
|
await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
|
||||||
|
|
||||||
|
user1AccessToken = await userLogin(servers[0], user)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const user = { username: 'user2', password: 'password' }
|
||||||
|
await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
|
||||||
|
|
||||||
|
user2AccessToken = await userLogin(servers[0], user)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const user = { username: 'user3', password: 'password' }
|
||||||
|
await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
|
||||||
|
|
||||||
|
user3AccessToken = await userLogin(servers[1], user)
|
||||||
|
}
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Bulk remove comments', function () {
|
||||||
|
async function checkInstanceCommentsRemoved () {
|
||||||
|
{
|
||||||
|
const res = await getVideosList(servers[0].url)
|
||||||
|
const videos = res.body.data as Video[]
|
||||||
|
|
||||||
|
// Server 1 should not have these comments anymore
|
||||||
|
for (const video of videos) {
|
||||||
|
const resThreads = await getVideoCommentThreads(servers[0].url, video.id, 0, 10)
|
||||||
|
const comments = resThreads.body.data as VideoComment[]
|
||||||
|
const comment = comments.find(c => c.text === 'comment by user 3')
|
||||||
|
|
||||||
|
expect(comment).to.not.exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await getVideosList(servers[1].url)
|
||||||
|
const videos = res.body.data as Video[]
|
||||||
|
|
||||||
|
// Server 1 should not have these comments on videos of server 1
|
||||||
|
for (const video of videos) {
|
||||||
|
const resThreads = await getVideoCommentThreads(servers[1].url, video.id, 0, 10)
|
||||||
|
const comments = resThreads.body.data as VideoComment[]
|
||||||
|
const comment = comments.find(c => c.text === 'comment by user 3')
|
||||||
|
|
||||||
|
if (video.account.host === 'localhost:' + servers[0].port) {
|
||||||
|
expect(comment).to.not.exist
|
||||||
|
} else {
|
||||||
|
expect(comment).to.exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 1 server 1' })
|
||||||
|
await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 2 server 1' })
|
||||||
|
await uploadVideo(servers[0].url, user1AccessToken, { name: 'video 3 server 1' })
|
||||||
|
|
||||||
|
await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await getVideosList(servers[0].url)
|
||||||
|
for (const video of res.body.data) {
|
||||||
|
await addVideoCommentThread(servers[0].url, servers[0].accessToken, video.id, 'comment by root server 1')
|
||||||
|
await addVideoCommentThread(servers[0].url, user1AccessToken, video.id, 'comment by user 1')
|
||||||
|
await addVideoCommentThread(servers[0].url, user2AccessToken, video.id, 'comment by user 2')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await getVideosList(servers[1].url)
|
||||||
|
for (const video of res.body.data) {
|
||||||
|
await addVideoCommentThread(servers[1].url, servers[1].accessToken, video.id, 'comment by root server 2')
|
||||||
|
|
||||||
|
const res = await addVideoCommentThread(servers[1].url, user3AccessToken, video.id, 'comment by user 3')
|
||||||
|
commentsUser3.push({ videoId: video.id, commentId: res.body.comment.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should delete comments of an account on my videos', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await bulkRemoveCommentsOf({
|
||||||
|
url: servers[0].url,
|
||||||
|
token: user1AccessToken,
|
||||||
|
attributes: {
|
||||||
|
accountName: 'user2',
|
||||||
|
scope: 'my-videos'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
const res = await getVideosList(server.url)
|
||||||
|
|
||||||
|
for (const video of res.body.data) {
|
||||||
|
const resThreads = await getVideoCommentThreads(server.url, video.id, 0, 10)
|
||||||
|
const comments = resThreads.body.data as VideoComment[]
|
||||||
|
const comment = comments.find(c => c.text === 'comment by user 2')
|
||||||
|
|
||||||
|
if (video.name === 'video 3 server 1') {
|
||||||
|
expect(comment).to.not.exist
|
||||||
|
} else {
|
||||||
|
expect(comment).to.exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should delete comments of an account on the instance', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await bulkRemoveCommentsOf({
|
||||||
|
url: servers[0].url,
|
||||||
|
token: servers[0].accessToken,
|
||||||
|
attributes: {
|
||||||
|
accountName: 'user3@localhost:' + servers[1].port,
|
||||||
|
scope: 'instance'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await checkInstanceCommentsRemoved()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not re create the comment on video update', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
for (const obj of commentsUser3) {
|
||||||
|
await addVideoCommentReply(servers[1].url, user3AccessToken, obj.videoId, obj.commentId, 'comment by user 3 bis')
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await checkInstanceCommentsRemoved()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests(servers)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { BulkRemoveCommentsOfBody } from "@shared/models/bulk/bulk-remove-comments-of-body.model"
|
||||||
|
import { makePostBodyRequest } from "../requests/requests"
|
||||||
|
|
||||||
|
function bulkRemoveCommentsOf (options: {
|
||||||
|
url: string
|
||||||
|
token: string
|
||||||
|
attributes: BulkRemoveCommentsOfBody
|
||||||
|
expectedStatus?: number
|
||||||
|
}) {
|
||||||
|
const { url, token, attributes, expectedStatus } = options
|
||||||
|
const path = '/api/v1/bulk/remove-comments-of'
|
||||||
|
|
||||||
|
return makePostBodyRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token,
|
||||||
|
fields: attributes,
|
||||||
|
statusCodeExpected: expectedStatus || 204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
bulkRemoveCommentsOf
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './server/activitypub'
|
export * from './server/activitypub'
|
||||||
|
export * from './bulk/bulk'
|
||||||
export * from './cli/cli'
|
export * from './cli/cli'
|
||||||
export * from './server/clients'
|
export * from './server/clients'
|
||||||
export * from './server/config'
|
export * from './server/config'
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface BulkRemoveCommentsOfBody {
|
||||||
|
accountName: string
|
||||||
|
scope: 'my-videos' | 'instance'
|
||||||
|
}
|
Loading…
Reference in New Issue