--- /dev/null
+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)
+ }
+}
+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()
apiRouter.use(apiRateLimiter)
apiRouter.use('/server', serverRouter)
+apiRouter.use('/bulk', bulkRouter)
apiRouter.use('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter)
apiRouter.use('/users', usersRouter)
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,
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()
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) {
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)
}
--- /dev/null
+function isBulkRemoveCommentsOfScopeValid (value: string) {
+ return value === 'my-videos' || value === 'instance'
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ isBulkRemoveCommentsOfScopeValid
+}
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)
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()
+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
return thread
}
-function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
+function markCommentAsDeleted (comment: MComment): void {
comment.text = ''
comment.deletedAt = new Date()
comment.accountId = null
// ---------------------------------------------------------------------------
export {
+ removeComment,
createVideoComment,
buildFormattedCommentTree,
markCommentAsDeleted
if (user.Account.id === accountToBlock.id) {
res.status(409)
- .send({ error: 'You cannot block yourself.' })
- .end()
+ .json({ error: 'You cannot block yourself.' })
return
}
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)
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
}
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
}
--- /dev/null
+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
+}
+
+// ---------------------------------------------------------------------------
+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,
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',
.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: [
videoId,
accountId: {
[Op.notIn]: buildLocalAccountIdsIn()
- }
+ },
+ // Do not delete Tombstones
+ deletedAt: null
}
}
--- /dev/null
+/* 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 ])
+ })
+})
import './accounts'
import './blocklist'
+import './bulk'
import './config'
import './contact-form'
import './debug'
--- /dev/null
+/* 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)
+ })
+})
--- /dev/null
+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
+}
export * from './server/activitypub'
+export * from './bulk/bulk'
export * from './cli/cli'
export * from './server/clients'
export * from './server/config'
--- /dev/null
+export interface BulkRemoveCommentsOfBody {
+ accountName: string
+ scope: 'my-videos' | 'instance'
+}