aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/users/index.ts2
-rw-r--r--server/controllers/api/users/me.ts1
-rw-r--r--server/controllers/api/users/my-history.ts57
-rw-r--r--server/helpers/custom-validators/users.ts5
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0300-user-videos-history-enabled.ts27
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/user-history.ts30
-rw-r--r--server/middlewares/validators/videos/video-watch.ts7
-rw-r--r--server/models/account/user-video-history.ts33
-rw-r--r--server/models/account/user.ts9
-rw-r--r--server/models/utils.ts2
-rw-r--r--server/models/video/video.ts19
-rw-r--r--server/tests/api/check-params/users.ts8
-rw-r--r--server/tests/api/check-params/videos-history.ts70
-rw-r--r--server/tests/api/videos/videos-history.ts85
-rw-r--r--shared/models/users/user-update-me.model.ts7
-rw-r--r--shared/utils/users/users.ts16
-rw-r--r--shared/utils/videos/video-history.ts33
19 files changed, 385 insertions, 29 deletions
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 87fab4a40..bc24792a2 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -38,6 +38,7 @@ import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../h
38import { meRouter } from './me' 38import { meRouter } from './me'
39import { deleteUserToken } from '../../../lib/oauth-model' 39import { deleteUserToken } from '../../../lib/oauth-model'
40import { myBlocklistRouter } from './my-blocklist' 40import { myBlocklistRouter } from './my-blocklist'
41import { myVideosHistoryRouter } from './my-history'
41 42
42const auditLogger = auditLoggerFactory('users') 43const auditLogger = auditLoggerFactory('users')
43 44
@@ -55,6 +56,7 @@ const askSendEmailLimiter = new RateLimit({
55 56
56const usersRouter = express.Router() 57const usersRouter = express.Router()
57usersRouter.use('/', myBlocklistRouter) 58usersRouter.use('/', myBlocklistRouter)
59usersRouter.use('/', myVideosHistoryRouter)
58usersRouter.use('/', meRouter) 60usersRouter.use('/', meRouter)
59 61
60usersRouter.get('/autocomplete', 62usersRouter.get('/autocomplete',
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index f712b0f0b..8a3208160 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -330,6 +330,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
330 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy 330 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
331 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled 331 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
332 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo 332 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
333 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
333 334
334 await sequelizeTypescript.transaction(async t => { 335 await sequelizeTypescript.transaction(async t => {
335 const userAccount = await AccountModel.load(user.Account.id) 336 const userAccount = await AccountModel.load(user.Account.id)
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts
new file mode 100644
index 000000000..6cd782c47
--- /dev/null
+++ b/server/controllers/api/users/my-history.ts
@@ -0,0 +1,57 @@
1import * as express from 'express'
2import {
3 asyncMiddleware,
4 asyncRetryTransactionMiddleware,
5 authenticate,
6 paginationValidator,
7 setDefaultPagination,
8 userHistoryRemoveValidator
9} from '../../../middlewares'
10import { UserModel } from '../../../models/account/user'
11import { getFormattedObjects } from '../../../helpers/utils'
12import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
13import { sequelizeTypescript } from '../../../initializers'
14
15const myVideosHistoryRouter = express.Router()
16
17myVideosHistoryRouter.get('/me/history/videos',
18 authenticate,
19 paginationValidator,
20 setDefaultPagination,
21 asyncMiddleware(listMyVideosHistory)
22)
23
24myVideosHistoryRouter.post('/me/history/videos/remove',
25 authenticate,
26 userHistoryRemoveValidator,
27 asyncRetryTransactionMiddleware(removeUserHistory)
28)
29
30// ---------------------------------------------------------------------------
31
32export {
33 myVideosHistoryRouter
34}
35
36// ---------------------------------------------------------------------------
37
38async function listMyVideosHistory (req: express.Request, res: express.Response) {
39 const user: UserModel = res.locals.oauth.token.User
40
41 const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count)
42
43 return res.json(getFormattedObjects(resultList.data, resultList.total))
44}
45
46async function removeUserHistory (req: express.Request, res: express.Response) {
47 const user: UserModel = res.locals.oauth.token.User
48 const beforeDate = req.body.beforeDate || null
49
50 await sequelizeTypescript.transaction(t => {
51 return UserVideoHistoryModel.removeHistoryBefore(user, beforeDate, t)
52 })
53
54 // Do not send the delete to other instances, we delete OUR copy of this video abuse
55
56 return res.type('json').status(204).end()
57}
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 1cb5e5b0f..80652b479 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -46,6 +46,10 @@ function isUserWebTorrentEnabledValid (value: any) {
46 return isBooleanValid(value) 46 return isBooleanValid(value)
47} 47}
48 48
49function isUserVideosHistoryEnabledValid (value: any) {
50 return isBooleanValid(value)
51}
52
49function isUserAutoPlayVideoValid (value: any) { 53function isUserAutoPlayVideoValid (value: any) {
50 return isBooleanValid(value) 54 return isBooleanValid(value)
51} 55}
@@ -73,6 +77,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
73// --------------------------------------------------------------------------- 77// ---------------------------------------------------------------------------
74 78
75export { 79export {
80 isUserVideosHistoryEnabledValid,
76 isUserBlockedValid, 81 isUserBlockedValid,
77 isUserPasswordValid, 82 isUserPasswordValid,
78 isUserBlockedReasonValid, 83 isUserBlockedReasonValid,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 6971ab775..6e463a1d6 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -16,7 +16,7 @@ let config: IConfig = require('config')
16 16
17// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
18 18
19const LAST_MIGRATION_VERSION = 295 19const LAST_MIGRATION_VERSION = 300
20 20
21// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
22 22
diff --git a/server/initializers/migrations/0300-user-videos-history-enabled.ts b/server/initializers/migrations/0300-user-videos-history-enabled.ts
new file mode 100644
index 000000000..aa5fc21fb
--- /dev/null
+++ b/server/initializers/migrations/0300-user-videos-history-enabled.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 allowNull: false,
13 defaultValue: true
14 }
15
16 await utils.queryInterface.addColumn('user', 'videosHistoryEnabled', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 46c7f0f3a..65dd00335 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -12,3 +12,4 @@ export * from './videos'
12export * from './webfinger' 12export * from './webfinger'
13export * from './search' 13export * from './search'
14export * from './server' 14export * from './server'
15export * from './user-history'
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts
new file mode 100644
index 000000000..3c8971ea1
--- /dev/null
+++ b/server/middlewares/validators/user-history.ts
@@ -0,0 +1,30 @@
1import * as express from 'express'
2import 'express-validator'
3import { body, param, query } from 'express-validator/check'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import { ActorFollowModel } from '../../models/activitypub/actor-follow'
7import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
8import { UserModel } from '../../models/account/user'
9import { CONFIG } from '../../initializers'
10import { isDateValid, toArray } from '../../helpers/custom-validators/misc'
11
12const userHistoryRemoveValidator = [
13 body('beforeDate')
14 .optional()
15 .custom(isDateValid).withMessage('Should have a valid before date'),
16
17 (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body })
19
20 if (areValidationErrors(req, res)) return
21
22 return next()
23 }
24]
25
26// ---------------------------------------------------------------------------
27
28export {
29 userHistoryRemoveValidator
30}
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts
index bca64662f..c38ad8a10 100644
--- a/server/middlewares/validators/videos/video-watch.ts
+++ b/server/middlewares/validators/videos/video-watch.ts
@@ -4,6 +4,7 @@ import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { areValidationErrors } from '../utils' 5import { areValidationErrors } from '../utils'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { UserModel } from '../../../models/account/user'
7 8
8const videoWatchingValidator = [ 9const videoWatchingValidator = [
9 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -17,6 +18,12 @@ const videoWatchingValidator = [
17 if (areValidationErrors(req, res)) return 18 if (areValidationErrors(req, res)) return
18 if (!await isVideoExist(req.params.videoId, res, 'id')) return 19 if (!await isVideoExist(req.params.videoId, res, 'id')) return
19 20
21 const user = res.locals.oauth.token.User as UserModel
22 if (user.videosHistoryEnabled === false) {
23 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id)
24 return res.status(409).end()
25 }
26
20 return next() 27 return next()
21 } 28 }
22] 29]
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
index 0476cad9d..15cb399c9 100644
--- a/server/models/account/user-video-history.ts
+++ b/server/models/account/user-video-history.ts
@@ -1,6 +1,7 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video' 2import { VideoModel } from '../video/video'
3import { UserModel } from './user' 3import { UserModel } from './user'
4import { Transaction, Op, DestroyOptions } from 'sequelize'
4 5
5@Table({ 6@Table({
6 tableName: 'userVideoHistory', 7 tableName: 'userVideoHistory',
@@ -52,4 +53,34 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
52 onDelete: 'CASCADE' 53 onDelete: 'CASCADE'
53 }) 54 })
54 User: UserModel 55 User: UserModel
56
57 static listForApi (user: UserModel, start: number, count: number) {
58 return VideoModel.listForApi({
59 start,
60 count,
61 sort: '-UserVideoHistories.updatedAt',
62 nsfw: null, // All
63 includeLocalVideos: true,
64 withFiles: false,
65 user,
66 historyOfUser: user
67 })
68 }
69
70 static removeHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) {
71 const query: DestroyOptions = {
72 where: {
73 userId: user.id
74 },
75 transaction: t
76 }
77
78 if (beforeDate) {
79 query.where.updatedAt = {
80 [Op.lt]: beforeDate
81 }
82 }
83
84 return UserVideoHistoryModel.destroy(query)
85 }
55} 86}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 1843603f1..ea017c338 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -32,7 +32,8 @@ import {
32 isUserUsernameValid, 32 isUserUsernameValid,
33 isUserVideoQuotaDailyValid, 33 isUserVideoQuotaDailyValid,
34 isUserVideoQuotaValid, 34 isUserVideoQuotaValid,
35 isUserWebTorrentEnabledValid 35 isUserWebTorrentEnabledValid,
36 isUserVideosHistoryEnabledValid
36} from '../../helpers/custom-validators/users' 37} from '../../helpers/custom-validators/users'
37import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 38import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
38import { OAuthTokenModel } from '../oauth/oauth-token' 39import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -116,6 +117,12 @@ export class UserModel extends Model<UserModel> {
116 117
117 @AllowNull(false) 118 @AllowNull(false)
118 @Default(true) 119 @Default(true)
120 @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
121 @Column
122 videosHistoryEnabled: boolean
123
124 @AllowNull(false)
125 @Default(true)
119 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) 126 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
120 @Column 127 @Column
121 autoPlayVideo: boolean 128 autoPlayVideo: boolean
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 60b0906e8..6694eda69 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -29,7 +29,7 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
29 ] 29 ]
30 } 30 }
31 31
32 return [ [ field, direction ], lastSort ] 32 return [ field.split('.').concat([ direction ]), lastSort ]
33} 33}
34 34
35function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { 35function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index adef37937..199ea9ea4 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -153,7 +153,8 @@ type AvailableForListIDsOptions = {
153 accountId?: number 153 accountId?: number
154 videoChannelId?: number 154 videoChannelId?: number
155 trendingDays?: number 155 trendingDays?: number
156 user?: UserModel 156 user?: UserModel,
157 historyOfUser?: UserModel
157} 158}
158 159
159@Scopes({ 160@Scopes({
@@ -416,6 +417,16 @@ type AvailableForListIDsOptions = {
416 query.subQuery = false 417 query.subQuery = false
417 } 418 }
418 419
420 if (options.historyOfUser) {
421 query.include.push({
422 model: UserVideoHistoryModel,
423 required: true,
424 where: {
425 userId: options.historyOfUser.id
426 }
427 })
428 }
429
419 return query 430 return query
420 }, 431 },
421 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 432 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
@@ -987,7 +998,8 @@ export class VideoModel extends Model<VideoModel> {
987 videoChannelId?: number, 998 videoChannelId?: number,
988 followerActorId?: number 999 followerActorId?: number
989 trendingDays?: number, 1000 trendingDays?: number,
990 user?: UserModel 1001 user?: UserModel,
1002 historyOfUser?: UserModel
991 }, countVideos = true) { 1003 }, countVideos = true) {
992 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1004 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
993 throw new Error('Try to filter all-local but no user has not the see all videos right') 1005 throw new Error('Try to filter all-local but no user has not the see all videos right')
@@ -1026,6 +1038,7 @@ export class VideoModel extends Model<VideoModel> {
1026 videoChannelId: options.videoChannelId, 1038 videoChannelId: options.videoChannelId,
1027 includeLocalVideos: options.includeLocalVideos, 1039 includeLocalVideos: options.includeLocalVideos,
1028 user: options.user, 1040 user: options.user,
1041 historyOfUser: options.historyOfUser,
1029 trendingDays 1042 trendingDays
1030 } 1043 }
1031 1044
@@ -1341,7 +1354,7 @@ export class VideoModel extends Model<VideoModel> {
1341 } 1354 }
1342 1355
1343 const [ count, rowsId ] = await Promise.all([ 1356 const [ count, rowsId ] = await Promise.all([
1344 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), 1357 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined),
1345 VideoModel.scope(idsScope).findAll(query) 1358 VideoModel.scope(idsScope).findAll(query)
1346 ]) 1359 ])
1347 const ids = rowsId.map(r => r.id) 1360 const ids = rowsId.map(r => r.id)
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index ec35429d3..f8044cbd4 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -308,6 +308,14 @@ describe('Test users API validators', function () {
308 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) 308 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
309 }) 309 })
310 310
311 it('Should fail with an invalid videosHistoryEnabled attribute', async function () {
312 const fields = {
313 videosHistoryEnabled: -1
314 }
315
316 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
317 })
318
311 it('Should fail with an non authenticated user', async function () { 319 it('Should fail with an non authenticated user', async function () {
312 const fields = { 320 const fields = {
313 currentPassword: 'my super password', 321 currentPassword: 'my super password',
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
index 09c6f7861..8c079a956 100644
--- a/server/tests/api/check-params/videos-history.ts
+++ b/server/tests/api/check-params/videos-history.ts
@@ -3,8 +3,11 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 checkBadCountPagination,
7 checkBadStartPagination,
6 flushTests, 8 flushTests,
7 killallServers, 9 killallServers,
10 makeGetRequest,
8 makePostBodyRequest, 11 makePostBodyRequest,
9 makePutBodyRequest, 12 makePutBodyRequest,
10 runServer, 13 runServer,
@@ -16,7 +19,9 @@ import {
16const expect = chai.expect 19const expect = chai.expect
17 20
18describe('Test videos history API validator', function () { 21describe('Test videos history API validator', function () {
19 let path: string 22 let watchingPath: string
23 let myHistoryPath = '/api/v1/users/me/history/videos'
24 let myHistoryRemove = myHistoryPath + '/remove'
20 let server: ServerInfo 25 let server: ServerInfo
21 26
22 // --------------------------------------------------------------- 27 // ---------------------------------------------------------------
@@ -33,14 +38,14 @@ describe('Test videos history API validator', function () {
33 const res = await uploadVideo(server.url, server.accessToken, {}) 38 const res = await uploadVideo(server.url, server.accessToken, {})
34 const videoUUID = res.body.video.uuid 39 const videoUUID = res.body.video.uuid
35 40
36 path = '/api/v1/videos/' + videoUUID + '/watching' 41 watchingPath = '/api/v1/videos/' + videoUUID + '/watching'
37 }) 42 })
38 43
39 describe('When notifying a user is watching a video', function () { 44 describe('When notifying a user is watching a video', function () {
40 45
41 it('Should fail with an unauthenticated user', async function () { 46 it('Should fail with an unauthenticated user', async function () {
42 const fields = { currentTime: 5 } 47 const fields = { currentTime: 5 }
43 await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 }) 48 await makePutBodyRequest({ url: server.url, path: watchingPath, fields, statusCodeExpected: 401 })
44 }) 49 })
45 50
46 it('Should fail with an incorrect video id', async function () { 51 it('Should fail with an incorrect video id', async function () {
@@ -58,13 +63,68 @@ describe('Test videos history API validator', function () {
58 63
59 it('Should fail with a bad current time', async function () { 64 it('Should fail with a bad current time', async function () {
60 const fields = { currentTime: 'hello' } 65 const fields = { currentTime: 'hello' }
61 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 }) 66 await makePutBodyRequest({ url: server.url, path: watchingPath, fields, token: server.accessToken, statusCodeExpected: 400 })
62 }) 67 })
63 68
64 it('Should succeed with the correct parameters', async function () { 69 it('Should succeed with the correct parameters', async function () {
65 const fields = { currentTime: 5 } 70 const fields = { currentTime: 5 }
66 71
67 await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 }) 72 await makePutBodyRequest({ url: server.url, path: watchingPath, fields, token: server.accessToken, statusCodeExpected: 204 })
73 })
74 })
75
76 describe('When listing user videos history', function () {
77 it('Should fail with a bad start pagination', async function () {
78 await checkBadStartPagination(server.url, myHistoryPath, server.accessToken)
79 })
80
81 it('Should fail with a bad count pagination', async function () {
82 await checkBadCountPagination(server.url, myHistoryPath, server.accessToken)
83 })
84
85 it('Should fail with an unauthenticated user', async function () {
86 await makeGetRequest({ url: server.url, path: myHistoryPath, statusCodeExpected: 401 })
87 })
88
89 it('Should succeed with the correct params', async function () {
90 await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, statusCodeExpected: 200 })
91 })
92 })
93
94 describe('When removing user videos history', function () {
95 it('Should fail with an unauthenticated user', async function () {
96 await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', statusCodeExpected: 401 })
97 })
98
99 it('Should fail with a bad beforeDate parameter', async function () {
100 const body = { beforeDate: '15' }
101 await makePostBodyRequest({
102 url: server.url,
103 token: server.accessToken,
104 path: myHistoryRemove,
105 fields: body,
106 statusCodeExpected: 400
107 })
108 })
109
110 it('Should succeed with a valid beforeDate param', async function () {
111 const body = { beforeDate: new Date().toISOString() }
112 await makePostBodyRequest({
113 url: server.url,
114 token: server.accessToken,
115 path: myHistoryRemove,
116 fields: body,
117 statusCodeExpected: 204
118 })
119 })
120
121 it('Should succeed without body', async function () {
122 await makePostBodyRequest({
123 url: server.url,
124 token: server.accessToken,
125 path: myHistoryRemove,
126 statusCodeExpected: 204
127 })
68 }) 128 })
69 }) 129 })
70 130
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
index 40ae94f79..f654a422b 100644
--- a/server/tests/api/videos/videos-history.ts
+++ b/server/tests/api/videos/videos-history.ts
@@ -3,17 +3,21 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 createUser,
6 flushTests, 7 flushTests,
7 getVideosListWithToken, 8 getVideosListWithToken,
8 getVideoWithToken, 9 getVideoWithToken,
9 killallServers, makePutBodyRequest, 10 killallServers,
10 runServer, searchVideoWithToken, 11 runServer,
12 searchVideoWithToken,
11 ServerInfo, 13 ServerInfo,
12 setAccessTokensToServers, 14 setAccessTokensToServers,
13 uploadVideo 15 updateMyUser,
16 uploadVideo,
17 userLogin
14} from '../../../../shared/utils' 18} from '../../../../shared/utils'
15import { Video, VideoDetails } from '../../../../shared/models/videos' 19import { Video, VideoDetails } from '../../../../shared/models/videos'
16import { userWatchVideo } from '../../../../shared/utils/videos/video-history' 20import { listMyVideosHistory, removeMyVideosHistory, userWatchVideo } from '../../../../shared/utils/videos/video-history'
17 21
18const expect = chai.expect 22const expect = chai.expect
19 23
@@ -22,6 +26,8 @@ describe('Test videos history', function () {
22 let video1UUID: string 26 let video1UUID: string
23 let video2UUID: string 27 let video2UUID: string
24 let video3UUID: string 28 let video3UUID: string
29 let video3WatchedDate: Date
30 let userAccessToken: string
25 31
26 before(async function () { 32 before(async function () {
27 this.timeout(30000) 33 this.timeout(30000)
@@ -46,6 +52,13 @@ describe('Test videos history', function () {
46 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' }) 52 const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
47 video3UUID = res.body.video.uuid 53 video3UUID = res.body.video.uuid
48 } 54 }
55
56 const user = {
57 username: 'user_1',
58 password: 'super password'
59 }
60 await createUser(server.url, server.accessToken, user.username, user.password)
61 userAccessToken = await userLogin(server, user)
49 }) 62 })
50 63
51 it('Should get videos, without watching history', async function () { 64 it('Should get videos, without watching history', async function () {
@@ -62,8 +75,8 @@ describe('Test videos history', function () {
62 }) 75 })
63 76
64 it('Should watch the first and second video', async function () { 77 it('Should watch the first and second video', async function () {
65 await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
66 await userWatchVideo(server.url, server.accessToken, video2UUID, 8) 78 await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
79 await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
67 }) 80 })
68 81
69 it('Should return the correct history when listing, searching and getting videos', async function () { 82 it('Should return the correct history when listing, searching and getting videos', async function () {
@@ -117,6 +130,68 @@ describe('Test videos history', function () {
117 } 130 }
118 }) 131 })
119 132
133 it('Should have these videos when listing my history', async function () {
134 video3WatchedDate = new Date()
135 await userWatchVideo(server.url, server.accessToken, video3UUID, 2)
136
137 const res = await listMyVideosHistory(server.url, server.accessToken)
138
139 expect(res.body.total).to.equal(3)
140
141 const videos: Video[] = res.body.data
142 expect(videos[0].name).to.equal('video 3')
143 expect(videos[1].name).to.equal('video 1')
144 expect(videos[2].name).to.equal('video 2')
145 })
146
147 it('Should not have videos history on another user', async function () {
148 const res = await listMyVideosHistory(server.url, userAccessToken)
149
150 expect(res.body.total).to.equal(0)
151 expect(res.body.data).to.have.lengthOf(0)
152 })
153
154 it('Should clear my history', async function () {
155 await removeMyVideosHistory(server.url, server.accessToken, video3WatchedDate.toISOString())
156 })
157
158 it('Should have my history cleared', async function () {
159 const res = await listMyVideosHistory(server.url, server.accessToken)
160
161 expect(res.body.total).to.equal(1)
162
163 const videos: Video[] = res.body.data
164 expect(videos[0].name).to.equal('video 3')
165 })
166
167 it('Should disable videos history', async function () {
168 await updateMyUser({
169 url: server.url,
170 accessToken: server.accessToken,
171 videosHistoryEnabled: false
172 })
173
174 await userWatchVideo(server.url, server.accessToken, video2UUID, 8, 409)
175 })
176
177 it('Should re-enable videos history', async function () {
178 await updateMyUser({
179 url: server.url,
180 accessToken: server.accessToken,
181 videosHistoryEnabled: true
182 })
183
184 await userWatchVideo(server.url, server.accessToken, video1UUID, 8)
185
186 const res = await listMyVideosHistory(server.url, server.accessToken)
187
188 expect(res.body.total).to.equal(2)
189
190 const videos: Video[] = res.body.data
191 expect(videos[0].name).to.equal('video 1')
192 expect(videos[1].name).to.equal('video 3')
193 })
194
120 after(async function () { 195 after(async function () {
121 killallServers([ server ]) 196 killallServers([ server ])
122 197
diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts
index 10edeee2e..e24afab94 100644
--- a/shared/models/users/user-update-me.model.ts
+++ b/shared/models/users/user-update-me.model.ts
@@ -3,9 +3,12 @@ import { NSFWPolicyType } from '../videos/nsfw-policy.type'
3export interface UserUpdateMe { 3export interface UserUpdateMe {
4 displayName?: string 4 displayName?: string
5 description?: string 5 description?: string
6 nsfwPolicy?: NSFWPolicyType, 6 nsfwPolicy?: NSFWPolicyType
7 webTorrentEnabled?: boolean, 7
8 webTorrentEnabled?: boolean
8 autoPlayVideo?: boolean 9 autoPlayVideo?: boolean
10 videosHistoryEnabled?: boolean
11
9 email?: string 12 email?: string
10 currentPassword?: string 13 currentPassword?: string
11 password?: string 14 password?: string
diff --git a/shared/utils/users/users.ts b/shared/utils/users/users.ts
index 554e42c01..61a7e3757 100644
--- a/shared/utils/users/users.ts
+++ b/shared/utils/users/users.ts
@@ -162,14 +162,15 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
162 162
163function updateMyUser (options: { 163function updateMyUser (options: {
164 url: string 164 url: string
165 accessToken: string, 165 accessToken: string
166 currentPassword?: string, 166 currentPassword?: string
167 newPassword?: string, 167 newPassword?: string
168 nsfwPolicy?: NSFWPolicyType, 168 nsfwPolicy?: NSFWPolicyType
169 email?: string, 169 email?: string
170 autoPlayVideo?: boolean 170 autoPlayVideo?: boolean
171 displayName?: string, 171 displayName?: string
172 description?: string 172 description?: string
173 videosHistoryEnabled?: boolean
173}) { 174}) {
174 const path = '/api/v1/users/me' 175 const path = '/api/v1/users/me'
175 176
@@ -181,6 +182,9 @@ function updateMyUser (options: {
181 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email 182 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
182 if (options.description !== undefined && options.description !== null) toSend['description'] = options.description 183 if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
183 if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName 184 if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
185 if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
186 toSend['videosHistoryEnabled'] = options.videosHistoryEnabled
187 }
184 188
185 return makePutBodyRequest({ 189 return makePutBodyRequest({
186 url: options.url, 190 url: options.url,
diff --git a/shared/utils/videos/video-history.ts b/shared/utils/videos/video-history.ts
index 7635478f7..dc7095b4d 100644
--- a/shared/utils/videos/video-history.ts
+++ b/shared/utils/videos/video-history.ts
@@ -1,14 +1,39 @@
1import { makePutBodyRequest } from '../requests/requests' 1import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2 2
3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) { 3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number, statusCodeExpected = 204) {
4 const path = '/api/v1/videos/' + videoId + '/watching' 4 const path = '/api/v1/videos/' + videoId + '/watching'
5 const fields = { currentTime } 5 const fields = { currentTime }
6 6
7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 }) 7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
8}
9
10function listMyVideosHistory (url: string, token: string) {
11 const path = '/api/v1/users/me/history/videos'
12
13 return makeGetRequest({
14 url,
15 path,
16 token,
17 statusCodeExpected: 200
18 })
19}
20
21function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
22 const path = '/api/v1/users/me/history/videos/remove'
23
24 return makePostBodyRequest({
25 url,
26 path,
27 token,
28 fields: beforeDate ? { beforeDate } : {},
29 statusCodeExpected: 204
30 })
8} 31}
9 32
10// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
11 34
12export { 35export {
13 userWatchVideo 36 userWatchVideo,
37 listMyVideosHistory,
38 removeMyVideosHistory
14} 39}