Add /accounts/:username/ratings endpoint (#1756)
* Add /users/me/videos/ratings endpoint * Move ratings endpoint from users to accounts * /accounts/:name/ratings: add support for rating= and sort= * Restrict ratings list to owner * Wording and better way to ensure current account
This commit is contained in:
parent
8ce1ba6e3e
commit
c100a6142e
|
@ -1,16 +1,25 @@
|
|||
import * as express from 'express'
|
||||
import { getFormattedObjects, getServerActor } from '../../helpers/utils'
|
||||
import {
|
||||
authenticate,
|
||||
asyncMiddleware,
|
||||
commonVideosFiltersValidator,
|
||||
videoRatingValidator,
|
||||
optionalAuthenticate,
|
||||
paginationValidator,
|
||||
setDefaultPagination,
|
||||
setDefaultSort,
|
||||
videoPlaylistsSortValidator
|
||||
videoPlaylistsSortValidator,
|
||||
videoRatesSortValidator
|
||||
} from '../../middlewares'
|
||||
import { accountNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators'
|
||||
import {
|
||||
accountNameWithHostGetValidator,
|
||||
accountsSortValidator,
|
||||
videosSortValidator,
|
||||
ensureAuthUserOwnsAccountValidator
|
||||
} from '../../middlewares/validators'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||
import { VideoModel } from '../../models/video/video'
|
||||
import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||
|
@ -61,6 +70,18 @@ accountsRouter.get('/:accountName/video-playlists',
|
|||
asyncMiddleware(listAccountPlaylists)
|
||||
)
|
||||
|
||||
accountsRouter.get('/:accountName/ratings',
|
||||
authenticate,
|
||||
asyncMiddleware(accountNameWithHostGetValidator),
|
||||
ensureAuthUserOwnsAccountValidator,
|
||||
paginationValidator,
|
||||
videoRatesSortValidator,
|
||||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
videoRatingValidator,
|
||||
asyncMiddleware(listAccountRatings)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -138,3 +159,16 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
|
|||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function listAccountRatings (req: express.Request, res: express.Response) {
|
||||
const account = res.locals.account
|
||||
|
||||
const resultList = await AccountVideoRateModel.listByAccountForApi({
|
||||
accountId: account.id,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
type: req.query.rating
|
||||
})
|
||||
return res.json(getFormattedObjects(resultList.rows, resultList.count))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
function isRatingValid (value: any) {
|
||||
return value === 'like' || value === 'dislike'
|
||||
}
|
||||
|
||||
export { isRatingValid }
|
|
@ -42,6 +42,7 @@ const SORTABLE_COLUMNS = {
|
|||
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
|
||||
VIDEO_IMPORTS: [ 'createdAt' ],
|
||||
VIDEO_COMMENT_THREADS: [ 'createdAt' ],
|
||||
VIDEO_RATES: [ 'createdAt' ],
|
||||
BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
|
||||
FOLLOWERS: [ 'createdAt' ],
|
||||
FOLLOWING: [ 'createdAt' ],
|
||||
|
|
|
@ -11,6 +11,7 @@ const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VI
|
|||
const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
|
||||
const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS)
|
||||
const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
|
||||
const SORTABLE_VIDEO_RATES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_RATES)
|
||||
const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
|
||||
const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
|
||||
const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
|
||||
|
@ -30,6 +31,7 @@ const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
|
|||
const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
|
||||
const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS)
|
||||
const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
|
||||
const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS)
|
||||
const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
|
||||
const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
|
||||
const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
|
||||
|
@ -55,6 +57,7 @@ export {
|
|||
followingSortValidator,
|
||||
jobsSortValidator,
|
||||
videoCommentThreadsSortValidator,
|
||||
videoRatesSortValidator,
|
||||
userSubscriptionsSortValidator,
|
||||
videoChannelsSearchSortValidator,
|
||||
accountsBlocklistSortValidator,
|
||||
|
|
|
@ -22,6 +22,7 @@ import { logger } from '../../helpers/logger'
|
|||
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
|
||||
import { Redis } from '../../lib/redis'
|
||||
import { UserModel } from '../../models/account/user'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { areValidationErrors } from './utils'
|
||||
import { ActorModel } from '../../models/activitypub/actor'
|
||||
|
||||
|
@ -317,6 +318,20 @@ const userAutocompleteValidator = [
|
|||
param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
|
||||
]
|
||||
|
||||
const ensureAuthUserOwnsAccountValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (res.locals.account.id !== user.Account.id) {
|
||||
return res.status(403)
|
||||
.send({ error: 'Only owner can access ratings list.' })
|
||||
.end()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -335,7 +350,8 @@ export {
|
|||
usersResetPasswordValidator,
|
||||
usersAskSendVerifyEmailValidator,
|
||||
usersVerifyEmailValidator,
|
||||
userAutocompleteValidator
|
||||
userAutocompleteValidator,
|
||||
ensureAuthUserOwnsAccountValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import * as express from 'express'
|
||||
import 'express-validator'
|
||||
import { body, param } from 'express-validator/check'
|
||||
import { body, param, query } from 'express-validator/check'
|
||||
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
|
||||
import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
|
||||
import { doesVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { areValidationErrors } from '../utils'
|
||||
|
@ -47,9 +48,22 @@ const getAccountVideoRateValidator = function (rateType: VideoRateType) {
|
|||
]
|
||||
}
|
||||
|
||||
const videoRatingValidator = [
|
||||
query('rating').optional().custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking rating parameter', { parameters: req.params })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoUpdateRateValidator,
|
||||
getAccountVideoRateValidator
|
||||
getAccountVideoRateValidator,
|
||||
videoRatingValidator
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@ import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers'
|
|||
import { VideoModel } from '../video/video'
|
||||
import { AccountModel } from './account'
|
||||
import { ActorModel } from '../activitypub/actor'
|
||||
import { throwIfNotValid } from '../utils'
|
||||
import { throwIfNotValid, getSort } from '../utils'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { AccountVideoRate } from '../../../shared'
|
||||
import { VideoChannelModel, ScopeNames as VideoChannelScopeNames } from '../video/video-channel'
|
||||
|
||||
/*
|
||||
Account rates per video.
|
||||
|
@ -88,6 +90,38 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
return AccountVideoRateModel.findOne(options)
|
||||
}
|
||||
|
||||
static listByAccountForApi (options: {
|
||||
start: number,
|
||||
count: number,
|
||||
sort: string,
|
||||
type?: string,
|
||||
accountId: number
|
||||
}) {
|
||||
const query: IFindOptions<AccountVideoRateModel> = {
|
||||
offset: options.start,
|
||||
limit: options.count,
|
||||
order: getSort(options.sort),
|
||||
where: {
|
||||
accountId: options.accountId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: VideoModel,
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }),
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
if (options.type) query.where['type'] = options.type
|
||||
|
||||
return AccountVideoRateModel.findAndCountAll(query)
|
||||
}
|
||||
|
||||
static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
|
||||
const options: IFindOptions<AccountVideoRateModel> = {
|
||||
where: {
|
||||
|
@ -185,4 +219,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
else if (type === 'dislike') await VideoModel.increment({ dislikes: -deleted }, options)
|
||||
})
|
||||
}
|
||||
|
||||
toFormattedJSON (): AccountVideoRate {
|
||||
return {
|
||||
video: this.Video.toFormattedJSON(),
|
||||
rating: this.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
createUser,
|
||||
deleteMe,
|
||||
flushTests,
|
||||
getAccountRatings,
|
||||
getBlacklistedVideosList,
|
||||
getMyUserInformation,
|
||||
getMyUserVideoQuotaUsed,
|
||||
|
@ -32,7 +33,7 @@ import {
|
|||
updateUser,
|
||||
uploadVideo,
|
||||
userLogin
|
||||
} from '../../../../shared/utils/index'
|
||||
} from '../../../../shared/utils'
|
||||
import { follow } from '../../../../shared/utils/server/follows'
|
||||
import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
|
||||
import { getMyVideos } from '../../../../shared/utils/videos/videos'
|
||||
|
@ -137,6 +138,35 @@ describe('Test users', function () {
|
|||
expect(rating.rating).to.equal('like')
|
||||
})
|
||||
|
||||
it('Should retrieve ratings list', async function () {
|
||||
await rateVideo(server.url, accessToken, videoId, 'like')
|
||||
const res = await getAccountRatings(server.url, server.user.username, server.accessToken, 200)
|
||||
const ratings = res.body
|
||||
|
||||
expect(ratings.data[0].video.id).to.equal(videoId)
|
||||
expect(ratings.data[0].rating).to.equal('like')
|
||||
})
|
||||
|
||||
it('Should retrieve ratings list by rating type', async function () {
|
||||
await rateVideo(server.url, accessToken, videoId, 'like')
|
||||
let res = await getAccountRatings(server.url, server.user.username, server.accessToken, 200, { rating: 'like' })
|
||||
let ratings = res.body
|
||||
expect(ratings.data.length).to.equal(1)
|
||||
res = await getAccountRatings(server.url, server.user.username, server.accessToken, 200, { rating: 'dislike' })
|
||||
ratings = res.body
|
||||
expect(ratings.data.length).to.equal(0)
|
||||
await getAccountRatings(server.url, server.user.username, server.accessToken, 400, { rating: 'invalid' })
|
||||
})
|
||||
|
||||
it('Should not access ratings list if not logged with correct user', async function () {
|
||||
const user = { username: 'anuragh', password: 'passbyme' }
|
||||
const resUser = await createUser(server.url, server.accessToken, user.username, user.password)
|
||||
const userId = resUser.body.user.id
|
||||
const userAccessToken = await userLogin(server, user)
|
||||
await getAccountRatings(server.url, server.user.username, userAccessToken, 403)
|
||||
await removeUser(server.url, userId, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should not be able to remove the video with an incorrect token', async function () {
|
||||
await removeVideo(server.url, 'bad_token', videoId, 401)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export * from './rate/user-video-rate-update.model'
|
||||
export * from './rate/user-video-rate.model'
|
||||
export * from './rate/account-video-rate.model'
|
||||
export * from './rate/user-video-rate.type'
|
||||
export * from './abuse/video-abuse-state.model'
|
||||
export * from './abuse/video-abuse-create.model'
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { UserVideoRateType } from './user-video-rate.type'
|
||||
import { Video } from '../video.model'
|
||||
|
||||
export interface AccountVideoRate {
|
||||
video: Video
|
||||
rating: UserVideoRateType
|
||||
}
|
|
@ -15,6 +15,7 @@ export * from './server/servers'
|
|||
export * from './videos/services'
|
||||
export * from './videos/video-playlists'
|
||||
export * from './users/users'
|
||||
export * from './users/accounts'
|
||||
export * from './videos/video-abuses'
|
||||
export * from './videos/video-blacklist'
|
||||
export * from './videos/video-channels'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* tslint:disable:no-unused-expression */
|
||||
|
||||
import * as request from 'supertest'
|
||||
import { expect } from 'chai'
|
||||
import { existsSync, readdir } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
|
@ -53,11 +54,24 @@ async function checkActorFilesWereRemoved (actorUUID: string, serverNumber: numb
|
|||
}
|
||||
}
|
||||
|
||||
function getAccountRatings (url: string, accountName: string, accessToken: string, statusCodeExpected = 200, query = {}) {
|
||||
const path = '/api/v1/accounts/' + accountName + '/ratings'
|
||||
|
||||
return request(url)
|
||||
.get(path)
|
||||
.query(query)
|
||||
.set('Accept', 'application/json')
|
||||
.set('Authorization', 'Bearer ' + accessToken)
|
||||
.expect(statusCodeExpected)
|
||||
.expect('Content-Type', /json/)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
getAccount,
|
||||
expectAccountFollows,
|
||||
getAccountsList,
|
||||
checkActorFilesWereRemoved
|
||||
checkActorFilesWereRemoved,
|
||||
getAccountRatings
|
||||
}
|
||||
|
|
|
@ -1344,6 +1344,35 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VideoChannel'
|
||||
'/accounts/{name}/ratings':
|
||||
get:
|
||||
summary: Get ratings of an account by its name
|
||||
security:
|
||||
- OAuth2: []
|
||||
tags:
|
||||
- User
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/start'
|
||||
- $ref: '#/components/parameters/count'
|
||||
- $ref: '#/components/parameters/sort'
|
||||
- name: rating
|
||||
in: query
|
||||
required: false
|
||||
description: Optionaly filter which ratings to retrieve
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- like
|
||||
- dislike
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/VideoRating'
|
||||
'/videos/{id}/comment-threads':
|
||||
get:
|
||||
summary: Get the comment threads of a video by its id
|
||||
|
@ -2142,6 +2171,16 @@ components:
|
|||
required:
|
||||
- id
|
||||
- rating
|
||||
VideoRating:
|
||||
properties:
|
||||
video:
|
||||
$ref: '#/components/schemas/Video'
|
||||
rating:
|
||||
type: number
|
||||
description: 'Rating of the video'
|
||||
required:
|
||||
- video
|
||||
- rating
|
||||
RegisterUser:
|
||||
properties:
|
||||
username:
|
||||
|
|
Loading…
Reference in New Issue