Add ability to bulk delete comments

This commit is contained in:
Chocobozzz 2020-05-14 16:56:15 +02:00 committed by Chocobozzz
parent 99139e7753
commit 444c0a0e01
15 changed files with 516 additions and 66 deletions

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -0,0 +1,9 @@
function isBulkRemoveCommentsOfScopeValid (value: string) {
return value === 'my-videos' || value === 'instance'
}
// ---------------------------------------------------------------------------
export {
isBulkRemoveCommentsOfScopeValid
}

View File

@ -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()

View File

@ -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

View File

@ -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
}

View File

@ -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
}
// ---------------------------------------------------------------------------

View File

@ -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
}
}

View File

@ -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 ])
})
})

View File

@ -1,5 +1,6 @@
import './accounts'
import './blocklist'
import './bulk'
import './config'
import './contact-form'
import './debug'

View File

@ -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)
})
})

View File

@ -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
}

View File

@ -1,4 +1,5 @@
export * from './server/activitypub'
export * from './bulk/bulk'
export * from './cli/cli'
export * from './server/clients'
export * from './server/config'

View File

@ -0,0 +1,4 @@
export interface BulkRemoveCommentsOfBody {
accountName: string
scope: 'my-videos' | 'instance'
}