Add ability for users to block an account/instance on server side
This commit is contained in:
parent
dffd5d127f
commit
7ad9b9846c
33 changed files with 1344 additions and 56 deletions
|
@ -1,7 +1,8 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { getFormattedObjects } from '../../helpers/utils'
|
import { getFormattedObjects } from '../../helpers/utils'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware, commonVideosFiltersValidator,
|
asyncMiddleware,
|
||||||
|
commonVideosFiltersValidator,
|
||||||
listVideoAccountChannelsValidator,
|
listVideoAccountChannelsValidator,
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
|
@ -90,7 +91,7 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
|
||||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||||
withFiles: false,
|
withFiles: false,
|
||||||
accountId: account.id,
|
accountId: account.id,
|
||||||
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
|
@ -119,7 +119,7 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
|
||||||
includeLocalVideos: true,
|
includeLocalVideos: true,
|
||||||
nsfw: buildNSFWFilter(res, query.nsfw),
|
nsfw: buildNSFWFilter(res, query.nsfw),
|
||||||
filter: query.filter,
|
filter: query.filter,
|
||||||
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
})
|
})
|
||||||
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
|
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ import { UserModel } from '../../../models/account/user'
|
||||||
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
|
||||||
import { meRouter } from './me'
|
import { meRouter } from './me'
|
||||||
import { deleteUserToken } from '../../../lib/oauth-model'
|
import { deleteUserToken } from '../../../lib/oauth-model'
|
||||||
|
import { myBlocklistRouter } from './my-blocklist'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('users')
|
const auditLogger = auditLoggerFactory('users')
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ const askSendEmailLimiter = new RateLimit({
|
||||||
})
|
})
|
||||||
|
|
||||||
const usersRouter = express.Router()
|
const usersRouter = express.Router()
|
||||||
|
usersRouter.use('/', myBlocklistRouter)
|
||||||
usersRouter.use('/', meRouter)
|
usersRouter.use('/', meRouter)
|
||||||
|
|
||||||
usersRouter.get('/autocomplete',
|
usersRouter.get('/autocomplete',
|
||||||
|
|
|
@ -238,7 +238,8 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
|
||||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||||
filter: req.query.filter as VideoFilter,
|
filter: req.query.filter as VideoFilter,
|
||||||
withFiles: false,
|
withFiles: false,
|
||||||
actorId: user.Account.Actor.id
|
actorId: user.Account.Actor.id,
|
||||||
|
user
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
125
server/controllers/api/users/my-blocklist.ts
Normal file
125
server/controllers/api/users/my-blocklist.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import 'multer'
|
||||||
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
|
import {
|
||||||
|
asyncMiddleware,
|
||||||
|
asyncRetryTransactionMiddleware,
|
||||||
|
authenticate,
|
||||||
|
paginationValidator,
|
||||||
|
serverGetValidator,
|
||||||
|
setDefaultPagination,
|
||||||
|
setDefaultSort,
|
||||||
|
unblockAccountByAccountValidator
|
||||||
|
} from '../../../middlewares'
|
||||||
|
import {
|
||||||
|
accountsBlocklistSortValidator,
|
||||||
|
blockAccountByAccountValidator,
|
||||||
|
serversBlocklistSortValidator,
|
||||||
|
unblockServerByAccountValidator
|
||||||
|
} from '../../../middlewares/validators'
|
||||||
|
import { UserModel } from '../../../models/account/user'
|
||||||
|
import { AccountModel } from '../../../models/account/account'
|
||||||
|
import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
|
||||||
|
import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
|
||||||
|
import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
|
||||||
|
import { ServerModel } from '../../../models/server/server'
|
||||||
|
|
||||||
|
const myBlocklistRouter = express.Router()
|
||||||
|
|
||||||
|
myBlocklistRouter.get('/me/blocklist/accounts',
|
||||||
|
authenticate,
|
||||||
|
paginationValidator,
|
||||||
|
accountsBlocklistSortValidator,
|
||||||
|
setDefaultSort,
|
||||||
|
setDefaultPagination,
|
||||||
|
asyncMiddleware(listBlockedAccounts)
|
||||||
|
)
|
||||||
|
|
||||||
|
myBlocklistRouter.post('/me/blocklist/accounts',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(blockAccountByAccountValidator),
|
||||||
|
asyncRetryTransactionMiddleware(blockAccount)
|
||||||
|
)
|
||||||
|
|
||||||
|
myBlocklistRouter.delete('/me/blocklist/accounts/:accountName',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(unblockAccountByAccountValidator),
|
||||||
|
asyncRetryTransactionMiddleware(unblockAccount)
|
||||||
|
)
|
||||||
|
|
||||||
|
myBlocklistRouter.get('/me/blocklist/servers',
|
||||||
|
authenticate,
|
||||||
|
paginationValidator,
|
||||||
|
serversBlocklistSortValidator,
|
||||||
|
setDefaultSort,
|
||||||
|
setDefaultPagination,
|
||||||
|
asyncMiddleware(listBlockedServers)
|
||||||
|
)
|
||||||
|
|
||||||
|
myBlocklistRouter.post('/me/blocklist/servers',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(serverGetValidator),
|
||||||
|
asyncRetryTransactionMiddleware(blockServer)
|
||||||
|
)
|
||||||
|
|
||||||
|
myBlocklistRouter.delete('/me/blocklist/servers/:host',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(unblockServerByAccountValidator),
|
||||||
|
asyncRetryTransactionMiddleware(unblockServer)
|
||||||
|
)
|
||||||
|
|
||||||
|
export {
|
||||||
|
myBlocklistRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function listBlockedAccounts (req: express.Request, res: express.Response) {
|
||||||
|
const user: UserModel = res.locals.oauth.token.User
|
||||||
|
|
||||||
|
const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
|
||||||
|
|
||||||
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function blockAccount (req: express.Request, res: express.Response) {
|
||||||
|
const user: UserModel = res.locals.oauth.token.User
|
||||||
|
const accountToBlock: AccountModel = res.locals.account
|
||||||
|
|
||||||
|
await addAccountInBlocklist(user.Account.id, accountToBlock.id)
|
||||||
|
|
||||||
|
return res.status(204).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unblockAccount (req: express.Request, res: express.Response) {
|
||||||
|
const accountBlock: AccountBlocklistModel = res.locals.accountBlock
|
||||||
|
|
||||||
|
await removeAccountFromBlocklist(accountBlock)
|
||||||
|
|
||||||
|
return res.status(204).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listBlockedServers (req: express.Request, res: express.Response) {
|
||||||
|
const user: UserModel = res.locals.oauth.token.User
|
||||||
|
|
||||||
|
const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
|
||||||
|
|
||||||
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function blockServer (req: express.Request, res: express.Response) {
|
||||||
|
const user: UserModel = res.locals.oauth.token.User
|
||||||
|
const serverToBlock: ServerModel = res.locals.server
|
||||||
|
|
||||||
|
await addServerInBlocklist(user.Account.id, serverToBlock.id)
|
||||||
|
|
||||||
|
return res.status(204).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unblockServer (req: express.Request, res: express.Response) {
|
||||||
|
const serverBlock: ServerBlocklistModel = res.locals.serverBlock
|
||||||
|
|
||||||
|
await removeServerFromBlocklist(serverBlock)
|
||||||
|
|
||||||
|
return res.status(204).end()
|
||||||
|
}
|
|
@ -219,7 +219,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
|
||||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||||
withFiles: false,
|
withFiles: false,
|
||||||
videoChannelId: videoChannelInstance.id,
|
videoChannelId: videoChannelInstance.id,
|
||||||
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
authenticate,
|
authenticate, optionalAuthenticate,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
setDefaultSort
|
setDefaultSort
|
||||||
|
@ -36,10 +36,12 @@ videoCommentRouter.get('/:videoId/comment-threads',
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
asyncMiddleware(listVideoCommentThreadsValidator),
|
asyncMiddleware(listVideoCommentThreadsValidator),
|
||||||
|
optionalAuthenticate,
|
||||||
asyncMiddleware(listVideoThreads)
|
asyncMiddleware(listVideoThreads)
|
||||||
)
|
)
|
||||||
videoCommentRouter.get('/:videoId/comment-threads/:threadId',
|
videoCommentRouter.get('/:videoId/comment-threads/:threadId',
|
||||||
asyncMiddleware(listVideoThreadCommentsValidator),
|
asyncMiddleware(listVideoThreadCommentsValidator),
|
||||||
|
optionalAuthenticate,
|
||||||
asyncMiddleware(listVideoThreadComments)
|
asyncMiddleware(listVideoThreadComments)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,10 +71,12 @@ export {
|
||||||
|
|
||||||
async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const video = res.locals.video as VideoModel
|
const video = res.locals.video as VideoModel
|
||||||
|
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
|
|
||||||
let resultList: ResultList<VideoCommentModel>
|
let resultList: ResultList<VideoCommentModel>
|
||||||
|
|
||||||
if (video.commentsEnabled === true) {
|
if (video.commentsEnabled === true) {
|
||||||
resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort)
|
resultList = await VideoCommentModel.listThreadsForApi(video.id, req.query.start, req.query.count, req.query.sort, user)
|
||||||
} else {
|
} else {
|
||||||
resultList = {
|
resultList = {
|
||||||
total: 0,
|
total: 0,
|
||||||
|
@ -85,10 +89,12 @@ async function listVideoThreads (req: express.Request, res: express.Response, ne
|
||||||
|
|
||||||
async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const video = res.locals.video as VideoModel
|
const video = res.locals.video as VideoModel
|
||||||
|
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
|
|
||||||
let resultList: ResultList<VideoCommentModel>
|
let resultList: ResultList<VideoCommentModel>
|
||||||
|
|
||||||
if (video.commentsEnabled === true) {
|
if (video.commentsEnabled === true) {
|
||||||
resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id)
|
resultList = await VideoCommentModel.listThreadCommentsForApi(video.id, res.locals.videoCommentThread.id, user)
|
||||||
} else {
|
} else {
|
||||||
resultList = {
|
resultList = {
|
||||||
total: 0,
|
total: 0,
|
||||||
|
|
|
@ -437,7 +437,7 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
|
||||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||||
filter: req.query.filter as VideoFilter,
|
filter: req.query.filter as VideoFilter,
|
||||||
withFiles: false,
|
withFiles: false,
|
||||||
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
|
@ -40,7 +40,10 @@ const getServerActor = memoizee(async function () {
|
||||||
const application = await ApplicationModel.load()
|
const application = await ApplicationModel.load()
|
||||||
if (!application) throw Error('Could not load Application from database.')
|
if (!application) throw Error('Could not load Application from database.')
|
||||||
|
|
||||||
return application.Account.Actor
|
const actor = application.Account.Actor
|
||||||
|
actor.Account = application.Account
|
||||||
|
|
||||||
|
return actor
|
||||||
})
|
})
|
||||||
|
|
||||||
function generateVideoTmpPath (target: string | ParseTorrent) {
|
function generateVideoTmpPath (target: string | ParseTorrent) {
|
||||||
|
|
|
@ -47,7 +47,10 @@ const SORTABLE_COLUMNS = {
|
||||||
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ],
|
VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ],
|
||||||
|
|
||||||
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ],
|
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ],
|
||||||
VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ]
|
VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
|
||||||
|
|
||||||
|
ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
|
||||||
|
SERVERS_BLOCKLIST: [ 'createdAt' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
const OAUTH_LIFETIME = {
|
const OAUTH_LIFETIME = {
|
||||||
|
|
|
@ -29,6 +29,8 @@ import { VideoViewModel } from '../models/video/video-views'
|
||||||
import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
|
import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
|
||||||
import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
|
import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
|
||||||
import { UserVideoHistoryModel } from '../models/account/user-video-history'
|
import { UserVideoHistoryModel } from '../models/account/user-video-history'
|
||||||
|
import { AccountBlocklistModel } from '../models/account/account-blocklist'
|
||||||
|
import { ServerBlocklistModel } from '../models/server/server-blocklist'
|
||||||
|
|
||||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -91,7 +93,9 @@ async function initDatabaseModels (silent: boolean) {
|
||||||
VideoImportModel,
|
VideoImportModel,
|
||||||
VideoViewModel,
|
VideoViewModel,
|
||||||
VideoRedundancyModel,
|
VideoRedundancyModel,
|
||||||
UserVideoHistoryModel
|
UserVideoHistoryModel,
|
||||||
|
AccountBlocklistModel,
|
||||||
|
ServerBlocklistModel
|
||||||
])
|
])
|
||||||
|
|
||||||
// Check extensions exist in the database
|
// Check extensions exist in the database
|
||||||
|
|
40
server/lib/blocklist.ts
Normal file
40
server/lib/blocklist.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { sequelizeTypescript } from '../initializers'
|
||||||
|
import { AccountBlocklistModel } from '../models/account/account-blocklist'
|
||||||
|
import { ServerBlocklistModel } from '../models/server/server-blocklist'
|
||||||
|
|
||||||
|
function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
|
||||||
|
return sequelizeTypescript.transaction(async t => {
|
||||||
|
return AccountBlocklistModel.create({
|
||||||
|
accountId: byAccountId,
|
||||||
|
targetAccountId: targetAccountId
|
||||||
|
}, { transaction: t })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addServerInBlocklist (byAccountId: number, targetServerId: number) {
|
||||||
|
return sequelizeTypescript.transaction(async t => {
|
||||||
|
return ServerBlocklistModel.create({
|
||||||
|
accountId: byAccountId,
|
||||||
|
targetServerId
|
||||||
|
}, { transaction: t })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAccountFromBlocklist (accountBlock: AccountBlocklistModel) {
|
||||||
|
return sequelizeTypescript.transaction(async t => {
|
||||||
|
return accountBlock.destroy({ transaction: t })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeServerFromBlocklist (serverBlock: ServerBlocklistModel) {
|
||||||
|
return sequelizeTypescript.transaction(async t => {
|
||||||
|
return serverBlock.destroy({ transaction: t })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
addAccountInBlocklist,
|
||||||
|
addServerInBlocklist,
|
||||||
|
removeAccountFromBlocklist,
|
||||||
|
removeServerFromBlocklist
|
||||||
|
}
|
|
@ -64,10 +64,8 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentCommentThread = idx[childComment.inReplyToCommentId]
|
const parentCommentThread = idx[childComment.inReplyToCommentId]
|
||||||
if (!parentCommentThread) {
|
// Maybe the parent comment was blocked by the admin/user
|
||||||
const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}`
|
if (!parentCommentThread) continue
|
||||||
throw new Error(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
parentCommentThread.children.push(childCommentThread)
|
parentCommentThread.children.push(childCommentThread)
|
||||||
idx[childComment.id] = childCommentThread
|
idx[childComment.id] = childCommentThread
|
||||||
|
|
94
server/middlewares/validators/blocklist.ts
Normal file
94
server/middlewares/validators/blocklist.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { param, body } from 'express-validator/check'
|
||||||
|
import * as express from 'express'
|
||||||
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { areValidationErrors } from './utils'
|
||||||
|
import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
|
||||||
|
import { UserModel } from '../../models/account/user'
|
||||||
|
import { AccountBlocklistModel } from '../../models/account/account-blocklist'
|
||||||
|
import { isHostValid } from '../../helpers/custom-validators/servers'
|
||||||
|
import { ServerBlocklistModel } from '../../models/server/server-blocklist'
|
||||||
|
|
||||||
|
const blockAccountByAccountValidator = [
|
||||||
|
body('accountName').exists().withMessage('Should have an account name with host'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
if (!await isAccountNameWithHostExist(req.body.accountName, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const unblockAccountByAccountValidator = [
|
||||||
|
param('accountName').exists().withMessage('Should have an account name with host'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
if (!await isAccountNameWithHostExist(req.params.accountName, res)) return
|
||||||
|
|
||||||
|
const user = res.locals.oauth.token.User as UserModel
|
||||||
|
const targetAccount = res.locals.account
|
||||||
|
if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const unblockServerByAccountValidator = [
|
||||||
|
param('host').custom(isHostValid).withMessage('Should have an account name with host'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking unblockServerByAccountValidator parameters', { parameters: req.params })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
const user = res.locals.oauth.token.User as UserModel
|
||||||
|
if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
blockAccountByAccountValidator,
|
||||||
|
unblockAccountByAccountValidator,
|
||||||
|
unblockServerByAccountValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) {
|
||||||
|
const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
|
||||||
|
if (!accountBlock) {
|
||||||
|
res.status(404)
|
||||||
|
.send({ error: 'Account block entry not found.' })
|
||||||
|
.end()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.accountBlock = accountBlock
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isUnblockServerExists (accountId: number, host: string, res: express.Response) {
|
||||||
|
const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
|
||||||
|
if (!serverBlock) {
|
||||||
|
res.status(404)
|
||||||
|
.send({ error: 'Server block entry not found.' })
|
||||||
|
.end()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.serverBlock = serverBlock
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './account'
|
export * from './account'
|
||||||
|
export * from './blocklist'
|
||||||
export * from './oembed'
|
export * from './oembed'
|
||||||
export * from './activitypub'
|
export * from './activitypub'
|
||||||
export * from './pagination'
|
export * from './pagination'
|
||||||
|
@ -10,3 +11,4 @@ export * from './user-subscriptions'
|
||||||
export * from './videos'
|
export * from './videos'
|
||||||
export * from './webfinger'
|
export * from './webfinger'
|
||||||
export * from './search'
|
export * from './search'
|
||||||
|
export * from './server'
|
||||||
|
|
33
server/middlewares/validators/server.ts
Normal file
33
server/middlewares/validators/server.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { areValidationErrors } from './utils'
|
||||||
|
import { isHostValid } from '../../helpers/custom-validators/servers'
|
||||||
|
import { ServerModel } from '../../models/server/server'
|
||||||
|
import { body } from 'express-validator/check'
|
||||||
|
|
||||||
|
const serverGetValidator = [
|
||||||
|
body('host').custom(isHostValid).withMessage('Should have a valid host'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking serverGetValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
const server = await ServerModel.loadByHost(req.body.host)
|
||||||
|
if (!server) {
|
||||||
|
return res.status(404)
|
||||||
|
.send({ error: 'Server host not found.' })
|
||||||
|
.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.server = server
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
serverGetValidator
|
||||||
|
}
|
|
@ -16,6 +16,8 @@ const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.V
|
||||||
const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
|
const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
|
||||||
const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
|
const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
|
||||||
const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
|
const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
|
||||||
|
const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
|
||||||
|
const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
|
||||||
|
|
||||||
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
|
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
|
||||||
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
|
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
|
||||||
|
@ -31,6 +33,8 @@ const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
|
||||||
const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
|
const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
|
||||||
const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
|
const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
|
||||||
const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
|
const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
|
||||||
|
const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS)
|
||||||
|
const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -48,5 +52,7 @@ export {
|
||||||
jobsSortValidator,
|
jobsSortValidator,
|
||||||
videoCommentThreadsSortValidator,
|
videoCommentThreadsSortValidator,
|
||||||
userSubscriptionsSortValidator,
|
userSubscriptionsSortValidator,
|
||||||
videoChannelsSearchSortValidator
|
videoChannelsSearchSortValidator,
|
||||||
|
accountsBlocklistSortValidator,
|
||||||
|
serversBlocklistSortValidator
|
||||||
}
|
}
|
||||||
|
|
111
server/models/account/account-blocklist.ts
Normal file
111
server/models/account/account-blocklist.ts
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
|
import { AccountModel } from './account'
|
||||||
|
import { getSort } from '../utils'
|
||||||
|
import { AccountBlock } from '../../../shared/models/blocklist'
|
||||||
|
|
||||||
|
enum ScopeNames {
|
||||||
|
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scopes({
|
||||||
|
[ScopeNames.WITH_ACCOUNTS]: {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: () => AccountModel,
|
||||||
|
required: true,
|
||||||
|
as: 'ByAccount'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: () => AccountModel,
|
||||||
|
required: true,
|
||||||
|
as: 'AccountBlocked'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 'accountBlocklist',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: [ 'accountId', 'targetAccountId' ],
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [ 'targetAccountId' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdatedAt
|
||||||
|
updatedAt: Date
|
||||||
|
|
||||||
|
@ForeignKey(() => AccountModel)
|
||||||
|
@Column
|
||||||
|
accountId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => AccountModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'accountId',
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
as: 'ByAccount',
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
ByAccount: AccountModel
|
||||||
|
|
||||||
|
@ForeignKey(() => AccountModel)
|
||||||
|
@Column
|
||||||
|
targetAccountId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => AccountModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'targetAccountId',
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
as: 'AccountBlocked',
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
AccountBlocked: AccountModel
|
||||||
|
|
||||||
|
static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
accountId,
|
||||||
|
targetAccountId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AccountBlocklistModel.findOne(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
static listForApi (accountId: number, start: number, count: number, sort: string) {
|
||||||
|
const query = {
|
||||||
|
offset: start,
|
||||||
|
limit: count,
|
||||||
|
order: getSort(sort),
|
||||||
|
where: {
|
||||||
|
accountId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AccountBlocklistModel
|
||||||
|
.scope([ ScopeNames.WITH_ACCOUNTS ])
|
||||||
|
.findAndCountAll(query)
|
||||||
|
.then(({ rows, count }) => {
|
||||||
|
return { total: count, data: rows }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toFormattedJSON (): AccountBlock {
|
||||||
|
return {
|
||||||
|
byAccount: this.ByAccount.toFormattedJSON(),
|
||||||
|
accountBlocked: this.AccountBlocked.toFormattedJSON(),
|
||||||
|
createdAt: this.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
server/models/server/server-blocklist.ts
Normal file
121
server/models/server/server-blocklist.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
|
import { AccountModel } from '../account/account'
|
||||||
|
import { ServerModel } from './server'
|
||||||
|
import { ServerBlock } from '../../../shared/models/blocklist'
|
||||||
|
import { getSort } from '../utils'
|
||||||
|
|
||||||
|
enum ScopeNames {
|
||||||
|
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||||
|
WITH_SERVER = 'WITH_SERVER'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scopes({
|
||||||
|
[ScopeNames.WITH_ACCOUNT]: {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: () => AccountModel,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[ScopeNames.WITH_SERVER]: {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: () => ServerModel,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 'serverBlocklist',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: [ 'accountId', 'targetServerId' ],
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [ 'targetServerId' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
|
||||||
|
|
||||||
|
@CreatedAt
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdatedAt
|
||||||
|
updatedAt: Date
|
||||||
|
|
||||||
|
@ForeignKey(() => AccountModel)
|
||||||
|
@Column
|
||||||
|
accountId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => AccountModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'accountId',
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
ByAccount: AccountModel
|
||||||
|
|
||||||
|
@ForeignKey(() => ServerModel)
|
||||||
|
@Column
|
||||||
|
targetServerId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => ServerModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'targetServerId',
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
ServerBlocked: ServerModel
|
||||||
|
|
||||||
|
static loadByAccountAndHost (accountId: number, host: string) {
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
accountId
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ServerModel,
|
||||||
|
where: {
|
||||||
|
host
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerBlocklistModel.findOne(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
static listForApi (accountId: number, start: number, count: number, sort: string) {
|
||||||
|
const query = {
|
||||||
|
offset: start,
|
||||||
|
limit: count,
|
||||||
|
order: getSort(sort),
|
||||||
|
where: {
|
||||||
|
accountId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerBlocklistModel
|
||||||
|
.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ])
|
||||||
|
.findAndCountAll(query)
|
||||||
|
.then(({ rows, count }) => {
|
||||||
|
return { total: count, data: rows }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toFormattedJSON (): ServerBlock {
|
||||||
|
return {
|
||||||
|
byAccount: this.ByAccount.toFormattedJSON(),
|
||||||
|
serverBlocked: this.ServerBlocked.toFormattedJSON(),
|
||||||
|
createdAt: this.createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,4 +49,10 @@ export class ServerModel extends Model<ServerModel> {
|
||||||
|
|
||||||
return ServerModel.findOne(query)
|
return ServerModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toFormattedJSON () {
|
||||||
|
return {
|
||||||
|
host: this.host
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,9 +64,27 @@ function createSimilarityAttribute (col: string, value: string) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number) {
|
||||||
|
const blockerIds = [ serverAccountId ]
|
||||||
|
if (userAccountId) blockerIds.push(userAccountId)
|
||||||
|
|
||||||
|
const blockerIdsString = blockerIds.join(', ')
|
||||||
|
|
||||||
|
const query = 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
|
||||||
|
' UNION ALL ' +
|
||||||
|
// 'SELECT "accountId" FROM "accountBlocklist" WHERE "targetAccountId" = user.account.id
|
||||||
|
// UNION ALL
|
||||||
|
'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
|
||||||
|
'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
|
||||||
|
'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
buildBlockedAccountSQL,
|
||||||
SortType,
|
SortType,
|
||||||
getSort,
|
getSort,
|
||||||
getVideoSort,
|
getVideoSort,
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
|
AllowNull,
|
||||||
|
BeforeDestroy,
|
||||||
|
BelongsTo,
|
||||||
|
Column,
|
||||||
|
CreatedAt,
|
||||||
|
DataType,
|
||||||
|
ForeignKey,
|
||||||
|
IFindOptions,
|
||||||
|
Is,
|
||||||
|
Model,
|
||||||
|
Scopes,
|
||||||
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
|
import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
|
||||||
|
@ -13,9 +24,11 @@ import { AccountModel } from '../account/account'
|
||||||
import { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
import { AvatarModel } from '../avatar/avatar'
|
import { AvatarModel } from '../avatar/avatar'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
import { VideoChannelModel } from './video-channel'
|
import { VideoChannelModel } from './video-channel'
|
||||||
|
import { getServerActor } from '../../helpers/utils'
|
||||||
|
import { UserModel } from '../account/user'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||||
|
@ -25,18 +38,29 @@ enum ScopeNames {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scopes({
|
@Scopes({
|
||||||
[ScopeNames.ATTRIBUTES_FOR_API]: {
|
[ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
|
||||||
attributes: {
|
return {
|
||||||
include: [
|
attributes: {
|
||||||
[
|
include: [
|
||||||
Sequelize.literal(
|
[
|
||||||
'(SELECT COUNT("replies"."id") ' +
|
Sequelize.literal(
|
||||||
'FROM "videoComment" AS "replies" ' +
|
'(' +
|
||||||
'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")'
|
'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' +
|
||||||
),
|
'SELECT COUNT("replies"."id") - (' +
|
||||||
'totalReplies'
|
'SELECT COUNT("replies"."id") ' +
|
||||||
|
'FROM "videoComment" AS "replies" ' +
|
||||||
|
'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
|
||||||
|
'AND "accountId" IN (SELECT "id" FROM "blocklist")' +
|
||||||
|
')' +
|
||||||
|
'FROM "videoComment" AS "replies" ' +
|
||||||
|
'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
|
||||||
|
'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
|
||||||
|
')'
|
||||||
|
),
|
||||||
|
'totalReplies'
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ScopeNames.WITH_ACCOUNT]: {
|
[ScopeNames.WITH_ACCOUNT]: {
|
||||||
|
@ -267,26 +291,47 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
|
return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
|
static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
|
||||||
|
const serverActor = await getServerActor()
|
||||||
|
const serverAccountId = serverActor.Account.id
|
||||||
|
const userAccountId = user.Account.id
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
offset: start,
|
offset: start,
|
||||||
limit: count,
|
limit: count,
|
||||||
order: getSort(sort),
|
order: getSort(sort),
|
||||||
where: {
|
where: {
|
||||||
videoId,
|
videoId,
|
||||||
inReplyToCommentId: null
|
inReplyToCommentId: null,
|
||||||
|
accountId: {
|
||||||
|
[Sequelize.Op.notIn]: Sequelize.literal(
|
||||||
|
'(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: typings
|
||||||
|
const scopes: any[] = [
|
||||||
|
ScopeNames.WITH_ACCOUNT,
|
||||||
|
{
|
||||||
|
method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
return VideoCommentModel
|
return VideoCommentModel
|
||||||
.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
|
.scope(scopes)
|
||||||
.findAndCountAll(query)
|
.findAndCountAll(query)
|
||||||
.then(({ rows, count }) => {
|
.then(({ rows, count }) => {
|
||||||
return { total: count, data: rows }
|
return { total: count, data: rows }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static listThreadCommentsForApi (videoId: number, threadId: number) {
|
static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
|
||||||
|
const serverActor = await getServerActor()
|
||||||
|
const serverAccountId = serverActor.Account.id
|
||||||
|
const userAccountId = user.Account.id
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
|
order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
|
||||||
where: {
|
where: {
|
||||||
|
@ -294,12 +339,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
[ Sequelize.Op.or ]: [
|
[ Sequelize.Op.or ]: [
|
||||||
{ id: threadId },
|
{ id: threadId },
|
||||||
{ originCommentId: threadId }
|
{ originCommentId: threadId }
|
||||||
]
|
],
|
||||||
|
accountId: {
|
||||||
|
[Sequelize.Op.notIn]: Sequelize.literal(
|
||||||
|
'(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scopes: any[] = [
|
||||||
|
ScopeNames.WITH_ACCOUNT,
|
||||||
|
{
|
||||||
|
method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
return VideoCommentModel
|
return VideoCommentModel
|
||||||
.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
|
.scope(scopes)
|
||||||
.findAndCountAll(query)
|
.findAndCountAll(query)
|
||||||
.then(({ rows, count }) => {
|
.then(({ rows, count }) => {
|
||||||
return { total: count, data: rows }
|
return { total: count, data: rows }
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { VideoPrivacy, VideoState } from '../../../shared'
|
import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
|
||||||
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||||
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
||||||
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
|
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
|
||||||
|
@ -70,7 +70,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
|
||||||
import { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
import { AvatarModel } from '../avatar/avatar'
|
import { AvatarModel } from '../avatar/avatar'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
import { buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
|
import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils'
|
||||||
import { TagModel } from './tag'
|
import { TagModel } from './tag'
|
||||||
import { VideoAbuseModel } from './video-abuse'
|
import { VideoAbuseModel } from './video-abuse'
|
||||||
import { VideoChannelModel } from './video-channel'
|
import { VideoChannelModel } from './video-channel'
|
||||||
|
@ -93,6 +93,7 @@ import {
|
||||||
} from './video-format-utils'
|
} from './video-format-utils'
|
||||||
import * as validator from 'validator'
|
import * as validator from 'validator'
|
||||||
import { UserVideoHistoryModel } from '../account/user-video-history'
|
import { UserVideoHistoryModel } from '../account/user-video-history'
|
||||||
|
import { UserModel } from '../account/user'
|
||||||
|
|
||||||
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
||||||
const indexes: Sequelize.DefineIndexesOptions[] = [
|
const indexes: Sequelize.DefineIndexesOptions[] = [
|
||||||
|
@ -138,6 +139,7 @@ type ForAPIOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvailableForListIDsOptions = {
|
type AvailableForListIDsOptions = {
|
||||||
|
serverAccountId: number
|
||||||
actorId: number
|
actorId: number
|
||||||
includeLocalVideos: boolean
|
includeLocalVideos: boolean
|
||||||
filter?: VideoFilter
|
filter?: VideoFilter
|
||||||
|
@ -151,6 +153,7 @@ type AvailableForListIDsOptions = {
|
||||||
accountId?: number
|
accountId?: number
|
||||||
videoChannelId?: number
|
videoChannelId?: number
|
||||||
trendingDays?: number
|
trendingDays?: number
|
||||||
|
user?: UserModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scopes({
|
@Scopes({
|
||||||
|
@ -235,6 +238,15 @@ type AvailableForListIDsOptions = {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
channelId: {
|
||||||
|
[ Sequelize.Op.notIn ]: Sequelize.literal(
|
||||||
|
'(' +
|
||||||
|
'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
|
||||||
|
buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
|
||||||
|
')' +
|
||||||
|
')'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
include: []
|
include: []
|
||||||
|
@ -975,10 +987,10 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
videoChannelId?: number,
|
videoChannelId?: number,
|
||||||
actorId?: number
|
actorId?: number
|
||||||
trendingDays?: number,
|
trendingDays?: number,
|
||||||
userId?: number
|
user?: UserModel
|
||||||
}, countVideos = true) {
|
}, countVideos = true) {
|
||||||
if (options.filter && options.filter === 'all-local' && !options.userId) {
|
if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
|
||||||
throw new Error('Try to filter all-local but no userId is provided')
|
throw new Error('Try to filter all-local but no user has not the see all videos right')
|
||||||
}
|
}
|
||||||
|
|
||||||
const query: IFindOptions<VideoModel> = {
|
const query: IFindOptions<VideoModel> = {
|
||||||
|
@ -994,11 +1006,14 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
query.group = 'VideoModel.id'
|
query.group = 'VideoModel.id'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverActor = await getServerActor()
|
||||||
|
|
||||||
// actorId === null has a meaning, so just check undefined
|
// actorId === null has a meaning, so just check undefined
|
||||||
const actorId = options.actorId !== undefined ? options.actorId : (await getServerActor()).id
|
const actorId = options.actorId !== undefined ? options.actorId : serverActor.id
|
||||||
|
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
actorId,
|
actorId,
|
||||||
|
serverAccountId: serverActor.Account.id,
|
||||||
nsfw: options.nsfw,
|
nsfw: options.nsfw,
|
||||||
categoryOneOf: options.categoryOneOf,
|
categoryOneOf: options.categoryOneOf,
|
||||||
licenceOneOf: options.licenceOneOf,
|
licenceOneOf: options.licenceOneOf,
|
||||||
|
@ -1010,7 +1025,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
accountId: options.accountId,
|
accountId: options.accountId,
|
||||||
videoChannelId: options.videoChannelId,
|
videoChannelId: options.videoChannelId,
|
||||||
includeLocalVideos: options.includeLocalVideos,
|
includeLocalVideos: options.includeLocalVideos,
|
||||||
userId: options.userId,
|
user: options.user,
|
||||||
trendingDays
|
trendingDays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1033,7 +1048,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
tagsAllOf?: string[]
|
tagsAllOf?: string[]
|
||||||
durationMin?: number // seconds
|
durationMin?: number // seconds
|
||||||
durationMax?: number // seconds
|
durationMax?: number // seconds
|
||||||
userId?: number,
|
user?: UserModel,
|
||||||
filter?: VideoFilter
|
filter?: VideoFilter
|
||||||
}) {
|
}) {
|
||||||
const whereAnd = []
|
const whereAnd = []
|
||||||
|
@ -1104,6 +1119,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
const serverActor = await getServerActor()
|
const serverActor = await getServerActor()
|
||||||
const queryOptions = {
|
const queryOptions = {
|
||||||
actorId: serverActor.id,
|
actorId: serverActor.id,
|
||||||
|
serverAccountId: serverActor.Account.id,
|
||||||
includeLocalVideos: options.includeLocalVideos,
|
includeLocalVideos: options.includeLocalVideos,
|
||||||
nsfw: options.nsfw,
|
nsfw: options.nsfw,
|
||||||
categoryOneOf: options.categoryOneOf,
|
categoryOneOf: options.categoryOneOf,
|
||||||
|
@ -1111,7 +1127,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
languageOneOf: options.languageOneOf,
|
languageOneOf: options.languageOneOf,
|
||||||
tagsOneOf: options.tagsOneOf,
|
tagsOneOf: options.tagsOneOf,
|
||||||
tagsAllOf: options.tagsAllOf,
|
tagsAllOf: options.tagsAllOf,
|
||||||
userId: options.userId,
|
user: options.user,
|
||||||
filter: options.filter
|
filter: options.filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1287,7 +1303,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
|
|
||||||
private static async getAvailableForApi (
|
private static async getAvailableForApi (
|
||||||
query: IFindOptions<VideoModel>,
|
query: IFindOptions<VideoModel>,
|
||||||
options: AvailableForListIDsOptions & { userId?: number},
|
options: AvailableForListIDsOptions,
|
||||||
countVideos = true
|
countVideos = true
|
||||||
) {
|
) {
|
||||||
const idsScope = {
|
const idsScope = {
|
||||||
|
@ -1320,8 +1336,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (options.userId) {
|
if (options.user) {
|
||||||
apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] })
|
apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondQuery = {
|
const secondQuery = {
|
||||||
|
|
222
server/tests/api/check-params/blocklist.ts
Normal file
222
server/tests/api/check-params/blocklist.ts
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import 'mocha'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createUser,
|
||||||
|
doubleFollow,
|
||||||
|
flushAndRunMultipleServers,
|
||||||
|
flushTests,
|
||||||
|
killallServers,
|
||||||
|
makeDeleteRequest,
|
||||||
|
makeGetRequest,
|
||||||
|
makePostBodyRequest,
|
||||||
|
ServerInfo,
|
||||||
|
setAccessTokensToServers
|
||||||
|
} from '../../utils'
|
||||||
|
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
|
||||||
|
|
||||||
|
describe('Test blocklist API validators', function () {
|
||||||
|
let servers: ServerInfo[]
|
||||||
|
let server: ServerInfo
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await flushTests()
|
||||||
|
|
||||||
|
servers = await flushAndRunMultipleServers(2)
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
server = servers[0]
|
||||||
|
|
||||||
|
const user = { username: 'user1', password: 'password' }
|
||||||
|
await createUser(server.url, server.accessToken, user.username, user.password)
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('When managing user blocklist', function () {
|
||||||
|
const path = '/api/v1/users/me/blocklist/accounts'
|
||||||
|
|
||||||
|
describe('When managing user accounts blocklist', function () {
|
||||||
|
|
||||||
|
describe('When listing blocked accounts', function () {
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makeGetRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
statusCodeExpected: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad start pagination', async function () {
|
||||||
|
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad count pagination', async function () {
|
||||||
|
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an incorrect sort', async function () {
|
||||||
|
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When blocking an account', function () {
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user1' },
|
||||||
|
statusCodeExpected: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown account', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user2' },
|
||||||
|
statusCodeExpected: 404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
fields: { accountName: 'user1' },
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When unblocking an account', function () {
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: path + '/user1',
|
||||||
|
statusCodeExpected: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown account block', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: path + '/user2',
|
||||||
|
token: server.accessToken,
|
||||||
|
statusCodeExpected: 404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: path + '/user1',
|
||||||
|
token: server.accessToken,
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When managing user servers blocklist', function () {
|
||||||
|
const path = '/api/v1/users/me/blocklist/servers'
|
||||||
|
|
||||||
|
describe('When listing blocked servers', function () {
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makeGetRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
statusCodeExpected: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad start pagination', async function () {
|
||||||
|
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad count pagination', async function () {
|
||||||
|
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an incorrect sort', async function () {
|
||||||
|
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When blocking a server', function () {
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
fields: { host: 'localhost:9002' },
|
||||||
|
statusCodeExpected: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown server', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
fields: { host: 'localhost:9003' },
|
||||||
|
statusCodeExpected: 404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path,
|
||||||
|
fields: { host: 'localhost:9002' },
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When unblocking a server', function () {
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: path + '/localhost:9002',
|
||||||
|
statusCodeExpected: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown server block', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: path + '/localhost:9003',
|
||||||
|
token: server.accessToken,
|
||||||
|
statusCodeExpected: 404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await makeDeleteRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: path + '/localhost:9002',
|
||||||
|
token: server.accessToken,
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers(servers)
|
||||||
|
|
||||||
|
// Keep the logs if the test failed
|
||||||
|
if (this['ok']) {
|
||||||
|
await flushTests()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,6 @@
|
||||||
// Order of the tests we want to execute
|
// Order of the tests we want to execute
|
||||||
import './accounts'
|
import './accounts'
|
||||||
|
import './blocklist'
|
||||||
import './config'
|
import './config'
|
||||||
import './follows'
|
import './follows'
|
||||||
import './jobs'
|
import './jobs'
|
||||||
|
|
294
server/tests/api/users/account-blocklist.ts
Normal file
294
server/tests/api/users/account-blocklist.ts
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import { AccountBlock, ServerBlock, Video } from '../../../../shared/index'
|
||||||
|
import {
|
||||||
|
createUser,
|
||||||
|
doubleFollow,
|
||||||
|
flushAndRunMultipleServers,
|
||||||
|
flushTests,
|
||||||
|
killallServers,
|
||||||
|
ServerInfo,
|
||||||
|
uploadVideo,
|
||||||
|
userLogin
|
||||||
|
} from '../../utils/index'
|
||||||
|
import { setAccessTokensToServers } from '../../utils/users/login'
|
||||||
|
import { getVideosListWithToken } from '../../utils/videos/videos'
|
||||||
|
import {
|
||||||
|
addVideoCommentReply,
|
||||||
|
addVideoCommentThread,
|
||||||
|
getVideoCommentThreads,
|
||||||
|
getVideoThreadComments
|
||||||
|
} from '../../utils/videos/video-comments'
|
||||||
|
import { waitJobs } from '../../utils/server/jobs'
|
||||||
|
import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
|
||||||
|
import {
|
||||||
|
addAccountToAccountBlocklist,
|
||||||
|
addServerToAccountBlocklist,
|
||||||
|
getAccountBlocklistByAccount, getServerBlocklistByAccount,
|
||||||
|
removeAccountFromAccountBlocklist,
|
||||||
|
removeServerFromAccountBlocklist
|
||||||
|
} from '../../utils/users/blocklist'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
async function checkAllVideos (url: string, token: string) {
|
||||||
|
const res = await getVideosListWithToken(url, token)
|
||||||
|
|
||||||
|
expect(res.body.data).to.have.lengthOf(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAllComments (url: string, token: string, videoUUID: string) {
|
||||||
|
const resThreads = await getVideoCommentThreads(url, videoUUID, 0, 5, '-createdAt', token)
|
||||||
|
|
||||||
|
const threads: VideoComment[] = resThreads.body.data
|
||||||
|
expect(threads).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
for (const thread of threads) {
|
||||||
|
const res = await getVideoThreadComments(url, videoUUID, thread.id, token)
|
||||||
|
|
||||||
|
const tree: VideoCommentThreadTree = res.body
|
||||||
|
expect(tree.children).to.have.lengthOf(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Test accounts blocklist', function () {
|
||||||
|
let servers: ServerInfo[]
|
||||||
|
let videoUUID1: string
|
||||||
|
let videoUUID2: string
|
||||||
|
let userToken1: string
|
||||||
|
let userToken2: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await flushTests()
|
||||||
|
|
||||||
|
servers = await flushAndRunMultipleServers(2)
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
{
|
||||||
|
const user = { username: 'user1', password: 'password' }
|
||||||
|
await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
|
||||||
|
|
||||||
|
userToken1 = await userLogin(servers[0], user)
|
||||||
|
await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const user = { username: 'user2', password: 'password' }
|
||||||
|
await createUser(servers[1].url, servers[1].accessToken, user.username, user.password)
|
||||||
|
|
||||||
|
userToken2 = await userLogin(servers[1], user)
|
||||||
|
await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' })
|
||||||
|
videoUUID1 = res.body.video.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' })
|
||||||
|
videoUUID2 = res.body.video.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
{
|
||||||
|
const resComment = await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, 'comment root 1')
|
||||||
|
const resReply = await addVideoCommentReply(servers[ 0 ].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1')
|
||||||
|
await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1')
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const resComment = await addVideoCommentThread(servers[ 0 ].url, userToken1, videoUUID1, 'comment user 1')
|
||||||
|
await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1')
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When managing account blocklist', function () {
|
||||||
|
it('Should list all videos', function () {
|
||||||
|
return checkAllVideos(servers[0].url, servers[0].accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list the comments', function () {
|
||||||
|
return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should block a remote account', async function () {
|
||||||
|
await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:9002')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should hide its videos', async function () {
|
||||||
|
const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
|
||||||
|
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
expect(videos).to.have.lengthOf(3)
|
||||||
|
|
||||||
|
const v = videos.find(v => v.name === 'video user 2')
|
||||||
|
expect(v).to.be.undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should block a local account', async function () {
|
||||||
|
await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should hide its videos', async function () {
|
||||||
|
const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
|
||||||
|
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
expect(videos).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
const v = videos.find(v => v.name === 'video user 1')
|
||||||
|
expect(v).to.be.undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should hide its comments', async function () {
|
||||||
|
const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5, '-createdAt', servers[0].accessToken)
|
||||||
|
|
||||||
|
const threads: VideoComment[] = resThreads.body.data
|
||||||
|
expect(threads).to.have.lengthOf(1)
|
||||||
|
expect(threads[0].totalReplies).to.equal(0)
|
||||||
|
|
||||||
|
const t = threads.find(t => t.text === 'comment user 1')
|
||||||
|
expect(t).to.be.undefined
|
||||||
|
|
||||||
|
for (const thread of threads) {
|
||||||
|
const res = await getVideoThreadComments(servers[0].url, videoUUID1, thread.id, servers[0].accessToken)
|
||||||
|
|
||||||
|
const tree: VideoCommentThreadTree = res.body
|
||||||
|
expect(tree.children).to.have.lengthOf(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list all the videos with another user', async function () {
|
||||||
|
return checkAllVideos(servers[0].url, userToken1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list all the comments with another user', async function () {
|
||||||
|
return checkAllComments(servers[0].url, userToken1, videoUUID1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list blocked accounts', async function () {
|
||||||
|
{
|
||||||
|
const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
|
||||||
|
const blocks: AccountBlock[] = res.body.data
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(2)
|
||||||
|
|
||||||
|
const block = blocks[0]
|
||||||
|
expect(block.byAccount.displayName).to.equal('root')
|
||||||
|
expect(block.byAccount.name).to.equal('root')
|
||||||
|
expect(block.accountBlocked.displayName).to.equal('user2')
|
||||||
|
expect(block.accountBlocked.name).to.equal('user2')
|
||||||
|
expect(block.accountBlocked.host).to.equal('localhost:9002')
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt')
|
||||||
|
const blocks: AccountBlock[] = res.body.data
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(2)
|
||||||
|
|
||||||
|
const block = blocks[0]
|
||||||
|
expect(block.byAccount.displayName).to.equal('root')
|
||||||
|
expect(block.byAccount.name).to.equal('root')
|
||||||
|
expect(block.accountBlocked.displayName).to.equal('user1')
|
||||||
|
expect(block.accountBlocked.name).to.equal('user1')
|
||||||
|
expect(block.accountBlocked.host).to.equal('localhost:9001')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should unblock the remote account', async function () {
|
||||||
|
await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:9002')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should display its videos', async function () {
|
||||||
|
const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
|
||||||
|
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
expect(videos).to.have.lengthOf(3)
|
||||||
|
|
||||||
|
const v = videos.find(v => v.name === 'video user 2')
|
||||||
|
expect(v).not.to.be.undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should unblock the local account', async function () {
|
||||||
|
await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should display its comments', function () {
|
||||||
|
return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When managing server blocklist', function () {
|
||||||
|
it('Should list all videos', function () {
|
||||||
|
return checkAllVideos(servers[0].url, servers[0].accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list the comments', function () {
|
||||||
|
return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should block a remote server', async function () {
|
||||||
|
await addServerToAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:9002')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should hide its videos', async function () {
|
||||||
|
const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
|
||||||
|
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
expect(videos).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
const v1 = videos.find(v => v.name === 'video user 2')
|
||||||
|
const v2 = videos.find(v => v.name === 'video server 2')
|
||||||
|
|
||||||
|
expect(v1).to.be.undefined
|
||||||
|
expect(v2).to.be.undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list all the videos with another user', async function () {
|
||||||
|
return checkAllVideos(servers[0].url, userToken1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should hide its comments')
|
||||||
|
|
||||||
|
it('Should list blocked servers', async function () {
|
||||||
|
const res = await getServerBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt')
|
||||||
|
const blocks: ServerBlock[] = res.body.data
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
|
||||||
|
const block = blocks[0]
|
||||||
|
expect(block.byAccount.displayName).to.equal('root')
|
||||||
|
expect(block.byAccount.name).to.equal('root')
|
||||||
|
expect(block.serverBlocked.host).to.equal('localhost:9002')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should unblock the remote server', async function () {
|
||||||
|
await removeServerFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:9002')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should display its videos', function () {
|
||||||
|
return checkAllVideos(servers[0].url, servers[0].accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should display its comments', function () {
|
||||||
|
return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers(servers)
|
||||||
|
|
||||||
|
// Keep the logs if the test failed
|
||||||
|
if (this[ 'ok' ]) {
|
||||||
|
await flushTests()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -37,9 +37,7 @@ function makeDeleteRequest (options: {
|
||||||
|
|
||||||
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
|
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
|
||||||
|
|
||||||
return req
|
return req.expect(options.statusCodeExpected)
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(options.statusCodeExpected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUploadRequest (options: {
|
function makeUploadRequest (options: {
|
||||||
|
|
103
server/tests/utils/users/blocklist.ts
Normal file
103
server/tests/utils/users/blocklist.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import { makeDeleteRequest, makePostBodyRequest } from '../index'
|
||||||
|
import { makeGetRequest } from '../requests/requests'
|
||||||
|
|
||||||
|
function getAccountBlocklistByAccount (
|
||||||
|
url: string,
|
||||||
|
token: string,
|
||||||
|
start: number,
|
||||||
|
count: number,
|
||||||
|
sort = '-createdAt',
|
||||||
|
statusCodeExpected = 200
|
||||||
|
) {
|
||||||
|
const path = '/api/v1/users/me/blocklist/accounts'
|
||||||
|
|
||||||
|
return makeGetRequest({
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
query: { start, count, sort },
|
||||||
|
path,
|
||||||
|
statusCodeExpected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
|
||||||
|
const path = '/api/v1/users/me/blocklist/accounts'
|
||||||
|
|
||||||
|
return makePostBodyRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token,
|
||||||
|
fields: {
|
||||||
|
accountName: accountToBlock
|
||||||
|
},
|
||||||
|
statusCodeExpected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
|
||||||
|
const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
|
||||||
|
|
||||||
|
return makeDeleteRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token,
|
||||||
|
statusCodeExpected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServerBlocklistByAccount (
|
||||||
|
url: string,
|
||||||
|
token: string,
|
||||||
|
start: number,
|
||||||
|
count: number,
|
||||||
|
sort = '-createdAt',
|
||||||
|
statusCodeExpected = 200
|
||||||
|
) {
|
||||||
|
const path = '/api/v1/users/me/blocklist/servers'
|
||||||
|
|
||||||
|
return makeGetRequest({
|
||||||
|
url,
|
||||||
|
token,
|
||||||
|
query: { start, count, sort },
|
||||||
|
path,
|
||||||
|
statusCodeExpected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
|
||||||
|
const path = '/api/v1/users/me/blocklist/servers'
|
||||||
|
|
||||||
|
return makePostBodyRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token,
|
||||||
|
fields: {
|
||||||
|
host: serverToBlock
|
||||||
|
},
|
||||||
|
statusCodeExpected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
|
||||||
|
const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
|
||||||
|
|
||||||
|
return makeDeleteRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token,
|
||||||
|
statusCodeExpected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
getAccountBlocklistByAccount,
|
||||||
|
addAccountToAccountBlocklist,
|
||||||
|
removeAccountFromAccountBlocklist,
|
||||||
|
getServerBlocklistByAccount,
|
||||||
|
addServerToAccountBlocklist,
|
||||||
|
removeServerFromAccountBlocklist
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import * as request from 'supertest'
|
import * as request from 'supertest'
|
||||||
import { makeDeleteRequest } from '../'
|
import { makeDeleteRequest } from '../'
|
||||||
|
|
||||||
function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string) {
|
function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
|
||||||
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
||||||
|
|
||||||
const req = request(url)
|
const req = request(url)
|
||||||
|
@ -10,20 +10,24 @@ function getVideoCommentThreads (url: string, videoId: number | string, start: n
|
||||||
.query({ count: count })
|
.query({ count: count })
|
||||||
|
|
||||||
if (sort) req.query({ sort })
|
if (sort) req.query({ sort })
|
||||||
|
if (token) req.set('Authorization', 'Bearer ' + token)
|
||||||
|
|
||||||
return req.set('Accept', 'application/json')
|
return req.set('Accept', 'application/json')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideoThreadComments (url: string, videoId: number | string, threadId: number) {
|
function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
|
||||||
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
|
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
|
||||||
|
|
||||||
return request(url)
|
const req = request(url)
|
||||||
.get(path)
|
.get(path)
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.expect(200)
|
|
||||||
.expect('Content-Type', /json/)
|
if (token) req.set('Authorization', 'Bearer ' + token)
|
||||||
|
|
||||||
|
return req.expect(200)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) {
|
function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) {
|
||||||
|
|
7
shared/models/blocklist/account-block.model.ts
Normal file
7
shared/models/blocklist/account-block.model.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { Account } from '../actors'
|
||||||
|
|
||||||
|
export interface AccountBlock {
|
||||||
|
byAccount: Account
|
||||||
|
accountBlocked: Account
|
||||||
|
createdAt: Date | string
|
||||||
|
}
|
2
shared/models/blocklist/index.ts
Normal file
2
shared/models/blocklist/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './account-block.model'
|
||||||
|
export * from './server-block.model'
|
9
shared/models/blocklist/server-block.model.ts
Normal file
9
shared/models/blocklist/server-block.model.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Account } from '../actors'
|
||||||
|
|
||||||
|
export interface ServerBlock {
|
||||||
|
byAccount: Account
|
||||||
|
serverBlocked: {
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
createdAt: Date | string
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
export * from './activitypub'
|
export * from './activitypub'
|
||||||
export * from './actors'
|
export * from './actors'
|
||||||
export * from './avatars'
|
export * from './avatars'
|
||||||
|
export * from './blocklist'
|
||||||
export * from './redundancy'
|
export * from './redundancy'
|
||||||
export * from './users'
|
export * from './users'
|
||||||
export * from './videos'
|
export * from './videos'
|
||||||
|
|
Loading…
Reference in a new issue