Add ability to bulk delete comments
This commit is contained in:
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 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 { jobsRouter } from './jobs'
|
||||
import { oauthClientsRouter } from './oauth-clients'
|
||||
import { overviewsRouter } from './overviews'
|
||||
import { pluginRouter } from './plugins'
|
||||
import { searchRouter } from './search'
|
||||
import { serverRouter } from './server'
|
||||
import { usersRouter } from './users'
|
||||
import { accountsRouter } from './accounts'
|
||||
import { videosRouter } from './videos'
|
||||
import { badRequest } from '../../helpers/express-utils'
|
||||
import { videoChannelRouter } from './video-channel'
|
||||
import * as cors from 'cors'
|
||||
import { searchRouter } from './search'
|
||||
import { overviewsRouter } from './overviews'
|
||||
import { videoPlaylistRouter } from './video-playlist'
|
||||
import { CONFIG } from '../../initializers/config'
|
||||
import { pluginRouter } from './plugins'
|
||||
import * as RateLimit from 'express-rate-limit'
|
||||
import { videosRouter } from './videos'
|
||||
|
||||
const apiRouter = express.Router()
|
||||
|
||||
|
@ -31,6 +32,7 @@ const apiRateLimiter = RateLimit({
|
|||
apiRouter.use(apiRateLimiter)
|
||||
|
||||
apiRouter.use('/server', serverRouter)
|
||||
apiRouter.use('/bulk', bulkRouter)
|
||||
apiRouter.use('/oauth-clients', oauthClientsRouter)
|
||||
apiRouter.use('/config', configRouter)
|
||||
apiRouter.use('/users', usersRouter)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import * as express from 'express'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { ResultList } from '../../../../shared/models'
|
||||
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 { 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 {
|
||||
asyncMiddleware,
|
||||
asyncRetryTransactionMiddleware,
|
||||
|
@ -23,12 +24,8 @@ import {
|
|||
removeVideoCommentValidator,
|
||||
videoCommentThreadsSortValidator
|
||||
} 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 { Notifier } from '../../../lib/notifier'
|
||||
import { Hooks } from '../../../lib/plugins/hooks'
|
||||
import { sendDeleteVideoComment } from '../../../lib/activitypub/send'
|
||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||
|
||||
const auditLogger = auditLoggerFactory('comments')
|
||||
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 })
|
||||
|
||||
return res.json({
|
||||
comment: comment.toFormattedJSON()
|
||||
}).end()
|
||||
return res.json({ comment: comment.toFormattedJSON() })
|
||||
}
|
||||
|
||||
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 })
|
||||
|
||||
return res.json({ comment: comment.toFormattedJSON() }).end()
|
||||
return res.json({ comment: comment.toFormattedJSON() })
|
||||
}
|
||||
|
||||
async function removeVideoComment (req: express.Request, res: express.Response) {
|
||||
const videoCommentInstance = res.locals.videoCommentFull
|
||||
const videoCommentInstanceBefore = cloneDeep(videoCommentInstance)
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
|
||||
await sendDeleteVideoComment(videoCommentInstance, t)
|
||||
}
|
||||
|
||||
markCommentAsDeleted(videoCommentInstance)
|
||||
|
||||
await videoCommentInstance.save()
|
||||
})
|
||||
await removeComment(videoCommentInstance)
|
||||
|
||||
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).end()
|
||||
return res.type('json').status(204)
|
||||
}
|
||||
|
|
|
@ -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 { getServerActor } from '@server/models/application/application'
|
||||
import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { ActorModel } from '../../../models/activitypub/actor'
|
||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||
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 { 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const isVideoOrigin = videoComment.Video.isOwned()
|
||||
|
|
|
@ -1,10 +1,32 @@
|
|||
import { cloneDeep } from 'lodash'
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
import { sequelizeTypescript } from '@server/initializers/database'
|
||||
import { ResultList } from '../../shared/models'
|
||||
import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
|
||||
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 { sendCreateVideoComment } from './activitypub/send'
|
||||
import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models'
|
||||
import { Hooks } from './plugins/hooks'
|
||||
|
||||
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: {
|
||||
text: string
|
||||
|
@ -73,7 +95,7 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
|
|||
return thread
|
||||
}
|
||||
|
||||
function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
|
||||
function markCommentAsDeleted (comment: MComment): void {
|
||||
comment.text = ''
|
||||
comment.deletedAt = new Date()
|
||||
comment.accountId = null
|
||||
|
@ -82,6 +104,7 @@ function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
removeComment,
|
||||
createVideoComment,
|
||||
buildFormattedCommentTree,
|
||||
markCommentAsDeleted
|
||||
|
|
|
@ -24,8 +24,7 @@ const blockAccountValidator = [
|
|||
|
||||
if (user.Account.id === accountToBlock.id) {
|
||||
res.status(409)
|
||||
.send({ error: 'You cannot block yourself.' })
|
||||
.end()
|
||||
.json({ error: 'You cannot block yourself.' })
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -80,8 +79,7 @@ const blockServerValidator = [
|
|||
|
||||
if (host === WEBSERVER.HOST) {
|
||||
return res.status(409)
|
||||
.send({ error: 'You cannot block your own server.' })
|
||||
.end()
|
||||
.json({ error: 'You cannot block your own server.' })
|
||||
}
|
||||
|
||||
const server = await ServerModel.loadOrCreateByHost(host)
|
||||
|
@ -139,8 +137,7 @@ async function doesUnblockAccountExist (accountId: number, targetAccountId: numb
|
|||
const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
|
||||
if (!accountBlock) {
|
||||
res.status(404)
|
||||
.send({ error: 'Account block entry not found.' })
|
||||
.end()
|
||||
.json({ error: 'Account block entry not found.' })
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -154,8 +151,7 @@ async function doesUnblockServerExist (accountId: number, host: string, res: exp
|
|||
const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
|
||||
if (!serverBlock) {
|
||||
res.status(404)
|
||||
.send({ error: 'Server block entry not found.' })
|
||||
.end()
|
||||
.json({ error: 'Server block entry not found.' })
|
||||
|
||||
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 { 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 { 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, 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 { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { regexpCapture } from '../../helpers/regexp'
|
||||
import { uniq } from 'lodash'
|
||||
import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||
import {
|
||||
MComment,
|
||||
MCommentAP,
|
||||
|
@ -25,9 +23,11 @@ import {
|
|||
MCommentOwnerVideoFeed,
|
||||
MCommentOwnerVideoReply
|
||||
} from '../../typings/models/video'
|
||||
import { MUserAccountId } from '@server/typings/models'
|
||||
import { VideoPrivacy } from '@shared/models'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
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'
|
||||
|
||||
enum ScopeNames {
|
||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||
|
@ -415,6 +415,43 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
|||
.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: [
|
||||
|
@ -450,7 +487,9 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
|||
videoId,
|
||||
accountId: {
|
||||
[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 './blocklist'
|
||||
import './bulk'
|
||||
import './config'
|
||||
import './contact-form'
|
||||
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 './bulk/bulk'
|
||||
export * from './cli/cli'
|
||||
export * from './server/clients'
|
||||
export * from './server/config'
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export interface BulkRemoveCommentsOfBody {
|
||||
accountName: string
|
||||
scope: 'my-videos' | 'instance'
|
||||
}
|
Loading…
Reference in New Issue