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'
asyncMiddleware(listAccountPlaylists)
)
+accountsRouter.get('/:accountName/ratings',
+ authenticate,
+ asyncMiddleware(accountNameWithHostGetValidator),
+ ensureAuthUserOwnsAccountValidator,
+ paginationValidator,
+ videoRatesSortValidator,
+ setDefaultSort,
+ setDefaultPagination,
+ videoRatingValidator,
+ asyncMiddleware(listAccountRatings)
+)
+
// ---------------------------------------------------------------------------
export {
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))
+}
--- /dev/null
+function isRatingValid (value: any) {
+ return value === 'like' || value === 'dislike'
+}
+
+export { isRatingValid }
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' ],
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)
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)
followingSortValidator,
jobsSortValidator,
videoCommentThreadsSortValidator,
+ videoRatesSortValidator,
userSubscriptionsSortValidator,
videoChannelsSearchSortValidator,
accountsBlocklistSortValidator,
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'
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 {
usersResetPasswordValidator,
usersAskSendVerifyEmailValidator,
usersVerifyEmailValidator,
- userAutocompleteValidator
+ userAutocompleteValidator,
+ ensureAuthUserOwnsAccountValidator
}
// ---------------------------------------------------------------------------
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'
]
}
+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
}
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.
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: {
else if (type === 'dislike') await VideoModel.increment({ dislikes: -deleted }, options)
})
}
+
+ toFormattedJSON (): AccountVideoRate {
+ return {
+ video: this.Video.toFormattedJSON(),
+ rating: this.type
+ }
+ }
}
createUser,
deleteMe,
flushTests,
+ getAccountRatings,
getBlacklistedVideosList,
getMyUserInformation,
getMyUserVideoQuotaUsed,
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'
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)
})
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'
--- /dev/null
+import { UserVideoRateType } from './user-video-rate.type'
+import { Video } from '../video.model'
+
+export interface AccountVideoRate {
+ video: Video
+ rating: UserVideoRateType
+}
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'
/* tslint:disable:no-unused-expression */
+import * as request from 'supertest'
import { expect } from 'chai'
import { existsSync, readdir } from 'fs-extra'
import { join } from 'path'
}
}
+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
}
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
required:
- id
- rating
+ VideoRating:
+ properties:
+ video:
+ $ref: '#/components/schemas/Video'
+ rating:
+ type: number
+ description: 'Rating of the video'
+ required:
+ - video
+ - rating
RegisterUser:
properties:
username: