1
0
Fork 0

Add ability for users to block an account/instance on server side

This commit is contained in:
Chocobozzz 2018-10-12 15:26:04 +02:00
parent dffd5d127f
commit 7ad9b9846c
33 changed files with 1344 additions and 56 deletions

View file

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

View file

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

View file

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

View file

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

View 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()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

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

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

View file

@ -49,4 +49,10 @@ export class ServerModel extends Model<ServerModel> {
return ServerModel.findOne(query) return ServerModel.findOne(query)
} }
toFormattedJSON () {
return {
host: this.host
}
}
} }

View file

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

View file

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

View file

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

View 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()
}
})
})

View file

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

View 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()
}
})
})

View file

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

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

View file

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

View file

@ -0,0 +1,7 @@
import { Account } from '../actors'
export interface AccountBlock {
byAccount: Account
accountBlocked: Account
createdAt: Date | string
}

View file

@ -0,0 +1,2 @@
export * from './account-block.model'
export * from './server-block.model'

View file

@ -0,0 +1,9 @@
import { Account } from '../actors'
export interface ServerBlock {
byAccount: Account
serverBlocked: {
host: string
}
createdAt: Date | string
}

View file

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