aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorYohan Boniface <yohanboniface@free.fr>2019-04-09 11:02:02 +0200
committerChocobozzz <chocobozzz@cpy.re>2019-04-09 11:02:02 +0200
commitc100a6142e6571312db9f6407698a21a08b593fb (patch)
treec80ac9c7754b8f5b133255e003557937e415ee23 /server
parent8ce1ba6e3efe0a688d6a0b57e8201cde33ad7aac (diff)
downloadPeerTube-c100a6142e6571312db9f6407698a21a08b593fb.tar.gz
PeerTube-c100a6142e6571312db9f6407698a21a08b593fb.tar.zst
PeerTube-c100a6142e6571312db9f6407698a21a08b593fb.zip
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
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/accounts.ts38
-rw-r--r--server/helpers/custom-validators/video-rates.ts5
-rw-r--r--server/initializers/constants.ts1
-rw-r--r--server/middlewares/validators/sort.ts3
-rw-r--r--server/middlewares/validators/users.ts18
-rw-r--r--server/middlewares/validators/videos/video-rates.ts18
-rw-r--r--server/models/account/account-video-rate.ts43
-rw-r--r--server/tests/api/users/users.ts32
8 files changed, 151 insertions, 7 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index adbf69781..aa01ea1eb 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,16 +1,25 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects, getServerActor } from '../../helpers/utils' 2import { getFormattedObjects, getServerActor } from '../../helpers/utils'
3import { 3import {
4 authenticate,
4 asyncMiddleware, 5 asyncMiddleware,
5 commonVideosFiltersValidator, 6 commonVideosFiltersValidator,
7 videoRatingValidator,
6 optionalAuthenticate, 8 optionalAuthenticate,
7 paginationValidator, 9 paginationValidator,
8 setDefaultPagination, 10 setDefaultPagination,
9 setDefaultSort, 11 setDefaultSort,
10 videoPlaylistsSortValidator 12 videoPlaylistsSortValidator,
13 videoRatesSortValidator
11} from '../../middlewares' 14} from '../../middlewares'
12import { accountNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' 15import {
16 accountNameWithHostGetValidator,
17 accountsSortValidator,
18 videosSortValidator,
19 ensureAuthUserOwnsAccountValidator
20} from '../../middlewares/validators'
13import { AccountModel } from '../../models/account/account' 21import { AccountModel } from '../../models/account/account'
22import { AccountVideoRateModel } from '../../models/account/account-video-rate'
14import { VideoModel } from '../../models/video/video' 23import { VideoModel } from '../../models/video/video'
15import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 24import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
16import { VideoChannelModel } from '../../models/video/video-channel' 25import { VideoChannelModel } from '../../models/video/video-channel'
@@ -61,6 +70,18 @@ accountsRouter.get('/:accountName/video-playlists',
61 asyncMiddleware(listAccountPlaylists) 70 asyncMiddleware(listAccountPlaylists)
62) 71)
63 72
73accountsRouter.get('/:accountName/ratings',
74 authenticate,
75 asyncMiddleware(accountNameWithHostGetValidator),
76 ensureAuthUserOwnsAccountValidator,
77 paginationValidator,
78 videoRatesSortValidator,
79 setDefaultSort,
80 setDefaultPagination,
81 videoRatingValidator,
82 asyncMiddleware(listAccountRatings)
83)
84
64// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
65 86
66export { 87export {
@@ -138,3 +159,16 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
138 159
139 return res.json(getFormattedObjects(resultList.data, resultList.total)) 160 return res.json(getFormattedObjects(resultList.data, resultList.total))
140} 161}
162
163async function listAccountRatings (req: express.Request, res: express.Response) {
164 const account = res.locals.account
165
166 const resultList = await AccountVideoRateModel.listByAccountForApi({
167 accountId: account.id,
168 start: req.query.start,
169 count: req.query.count,
170 sort: req.query.sort,
171 type: req.query.rating
172 })
173 return res.json(getFormattedObjects(resultList.rows, resultList.count))
174}
diff --git a/server/helpers/custom-validators/video-rates.ts b/server/helpers/custom-validators/video-rates.ts
new file mode 100644
index 000000000..f2b6f7cae
--- /dev/null
+++ b/server/helpers/custom-validators/video-rates.ts
@@ -0,0 +1,5 @@
1function isRatingValid (value: any) {
2 return value === 'like' || value === 'dislike'
3}
4
5export { isRatingValid }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 78dd7cb9d..097199f84 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -42,6 +42,7 @@ const SORTABLE_COLUMNS = {
42 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], 42 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
43 VIDEO_IMPORTS: [ 'createdAt' ], 43 VIDEO_IMPORTS: [ 'createdAt' ],
44 VIDEO_COMMENT_THREADS: [ 'createdAt' ], 44 VIDEO_COMMENT_THREADS: [ 'createdAt' ],
45 VIDEO_RATES: [ 'createdAt' ],
45 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], 46 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
46 FOLLOWERS: [ 'createdAt' ], 47 FOLLOWERS: [ 'createdAt' ],
47 FOLLOWING: [ 'createdAt' ], 48 FOLLOWING: [ 'createdAt' ],
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index ea59fbf73..44295c325 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -11,6 +11,7 @@ const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VI
11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) 11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
12const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) 12const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS)
13const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) 13const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
14const SORTABLE_VIDEO_RATES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_RATES)
14const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) 15const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
15const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) 16const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) 17const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
@@ -30,6 +31,7 @@ const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
30const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) 31const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
31const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS) 32const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS)
32const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) 33const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
34const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS)
33const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) 35const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
34const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) 36const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
35const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) 37const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
@@ -55,6 +57,7 @@ export {
55 followingSortValidator, 57 followingSortValidator,
56 jobsSortValidator, 58 jobsSortValidator,
57 videoCommentThreadsSortValidator, 59 videoCommentThreadsSortValidator,
60 videoRatesSortValidator,
58 userSubscriptionsSortValidator, 61 userSubscriptionsSortValidator,
59 videoChannelsSearchSortValidator, 62 videoChannelsSearchSortValidator,
60 accountsBlocklistSortValidator, 63 accountsBlocklistSortValidator,
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 4be446732..35f41c450 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -22,6 +22,7 @@ import { logger } from '../../helpers/logger'
22import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' 22import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
23import { Redis } from '../../lib/redis' 23import { Redis } from '../../lib/redis'
24import { UserModel } from '../../models/account/user' 24import { UserModel } from '../../models/account/user'
25import { AccountModel } from '../../models/account/account'
25import { areValidationErrors } from './utils' 26import { areValidationErrors } from './utils'
26import { ActorModel } from '../../models/activitypub/actor' 27import { ActorModel } from '../../models/activitypub/actor'
27 28
@@ -317,6 +318,20 @@ const userAutocompleteValidator = [
317 param('search').isString().not().isEmpty().withMessage('Should have a search parameter') 318 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
318] 319]
319 320
321const ensureAuthUserOwnsAccountValidator = [
322 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
323 const user = res.locals.oauth.token.User
324
325 if (res.locals.account.id !== user.Account.id) {
326 return res.status(403)
327 .send({ error: 'Only owner can access ratings list.' })
328 .end()
329 }
330
331 return next()
332 }
333]
334
320// --------------------------------------------------------------------------- 335// ---------------------------------------------------------------------------
321 336
322export { 337export {
@@ -335,7 +350,8 @@ export {
335 usersResetPasswordValidator, 350 usersResetPasswordValidator,
336 usersAskSendVerifyEmailValidator, 351 usersAskSendVerifyEmailValidator,
337 usersVerifyEmailValidator, 352 usersVerifyEmailValidator,
338 userAutocompleteValidator 353 userAutocompleteValidator,
354 ensureAuthUserOwnsAccountValidator
339} 355}
340 356
341// --------------------------------------------------------------------------- 357// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
index 280385912..e79d80e97 100644
--- a/server/middlewares/validators/videos/video-rates.ts
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -1,7 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param } from 'express-validator/check' 3import { body, param, query } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
5import { doesVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' 6import { doesVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from '../utils' 8import { areValidationErrors } from '../utils'
@@ -47,9 +48,22 @@ const getAccountVideoRateValidator = function (rateType: VideoRateType) {
47 ] 48 ]
48} 49}
49 50
51const videoRatingValidator = [
52 query('rating').optional().custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'),
53
54 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
55 logger.debug('Checking rating parameter', { parameters: req.params })
56
57 if (areValidationErrors(req, res)) return
58
59 return next()
60 }
61]
62
50// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
51 64
52export { 65export {
53 videoUpdateRateValidator, 66 videoUpdateRateValidator,
54 getAccountVideoRateValidator 67 getAccountVideoRateValidator,
68 videoRatingValidator
55} 69}
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index e5d39582b..f462df4b3 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -7,8 +7,10 @@ import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers'
7import { VideoModel } from '../video/video' 7import { VideoModel } from '../video/video'
8import { AccountModel } from './account' 8import { AccountModel } from './account'
9import { ActorModel } from '../activitypub/actor' 9import { ActorModel } from '../activitypub/actor'
10import { throwIfNotValid } from '../utils' 10import { throwIfNotValid, getSort } from '../utils'
11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
12import { AccountVideoRate } from '../../../shared'
13import { VideoChannelModel, ScopeNames as VideoChannelScopeNames } from '../video/video-channel'
12 14
13/* 15/*
14 Account rates per video. 16 Account rates per video.
@@ -88,6 +90,38 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
88 return AccountVideoRateModel.findOne(options) 90 return AccountVideoRateModel.findOne(options)
89 } 91 }
90 92
93 static listByAccountForApi (options: {
94 start: number,
95 count: number,
96 sort: string,
97 type?: string,
98 accountId: number
99 }) {
100 const query: IFindOptions<AccountVideoRateModel> = {
101 offset: options.start,
102 limit: options.count,
103 order: getSort(options.sort),
104 where: {
105 accountId: options.accountId
106 },
107 include: [
108 {
109 model: VideoModel,
110 required: true,
111 include: [
112 {
113 model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }),
114 required: true
115 }
116 ]
117 }
118 ]
119 }
120 if (options.type) query.where['type'] = options.type
121
122 return AccountVideoRateModel.findAndCountAll(query)
123 }
124
91 static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) { 125 static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
92 const options: IFindOptions<AccountVideoRateModel> = { 126 const options: IFindOptions<AccountVideoRateModel> = {
93 where: { 127 where: {
@@ -185,4 +219,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
185 else if (type === 'dislike') await VideoModel.increment({ dislikes: -deleted }, options) 219 else if (type === 'dislike') await VideoModel.increment({ dislikes: -deleted }, options)
186 }) 220 })
187 } 221 }
222
223 toFormattedJSON (): AccountVideoRate {
224 return {
225 video: this.Video.toFormattedJSON(),
226 rating: this.type
227 }
228 }
188} 229}
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index c4465d541..bc069a7be 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -8,6 +8,7 @@ import {
8 createUser, 8 createUser,
9 deleteMe, 9 deleteMe,
10 flushTests, 10 flushTests,
11 getAccountRatings,
11 getBlacklistedVideosList, 12 getBlacklistedVideosList,
12 getMyUserInformation, 13 getMyUserInformation,
13 getMyUserVideoQuotaUsed, 14 getMyUserVideoQuotaUsed,
@@ -32,7 +33,7 @@ import {
32 updateUser, 33 updateUser,
33 uploadVideo, 34 uploadVideo,
34 userLogin 35 userLogin
35} from '../../../../shared/utils/index' 36} from '../../../../shared/utils'
36import { follow } from '../../../../shared/utils/server/follows' 37import { follow } from '../../../../shared/utils/server/follows'
37import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 38import { setAccessTokensToServers } from '../../../../shared/utils/users/login'
38import { getMyVideos } from '../../../../shared/utils/videos/videos' 39import { getMyVideos } from '../../../../shared/utils/videos/videos'
@@ -137,6 +138,35 @@ describe('Test users', function () {
137 expect(rating.rating).to.equal('like') 138 expect(rating.rating).to.equal('like')
138 }) 139 })
139 140
141 it('Should retrieve ratings list', async function () {
142 await rateVideo(server.url, accessToken, videoId, 'like')
143 const res = await getAccountRatings(server.url, server.user.username, server.accessToken, 200)
144 const ratings = res.body
145
146 expect(ratings.data[0].video.id).to.equal(videoId)
147 expect(ratings.data[0].rating).to.equal('like')
148 })
149
150 it('Should retrieve ratings list by rating type', async function () {
151 await rateVideo(server.url, accessToken, videoId, 'like')
152 let res = await getAccountRatings(server.url, server.user.username, server.accessToken, 200, { rating: 'like' })
153 let ratings = res.body
154 expect(ratings.data.length).to.equal(1)
155 res = await getAccountRatings(server.url, server.user.username, server.accessToken, 200, { rating: 'dislike' })
156 ratings = res.body
157 expect(ratings.data.length).to.equal(0)
158 await getAccountRatings(server.url, server.user.username, server.accessToken, 400, { rating: 'invalid' })
159 })
160
161 it('Should not access ratings list if not logged with correct user', async function () {
162 const user = { username: 'anuragh', password: 'passbyme' }
163 const resUser = await createUser(server.url, server.accessToken, user.username, user.password)
164 const userId = resUser.body.user.id
165 const userAccessToken = await userLogin(server, user)
166 await getAccountRatings(server.url, server.user.username, userAccessToken, 403)
167 await removeUser(server.url, userId, server.accessToken)
168 })
169
140 it('Should not be able to remove the video with an incorrect token', async function () { 170 it('Should not be able to remove the video with an incorrect token', async function () {
141 await removeVideo(server.url, 'bad_token', videoId, 401) 171 await removeVideo(server.url, 'bad_token', videoId, 401)
142 }) 172 })