Add history on server side
Add ability to disable, clear and list user videos history
This commit is contained in:
parent
583cd0d212
commit
8b9a525a18
|
@ -38,6 +38,7 @@ import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../h
|
||||||
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'
|
import { myBlocklistRouter } from './my-blocklist'
|
||||||
|
import { myVideosHistoryRouter } from './my-history'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('users')
|
const auditLogger = auditLoggerFactory('users')
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ const askSendEmailLimiter = new RateLimit({
|
||||||
|
|
||||||
const usersRouter = express.Router()
|
const usersRouter = express.Router()
|
||||||
usersRouter.use('/', myBlocklistRouter)
|
usersRouter.use('/', myBlocklistRouter)
|
||||||
|
usersRouter.use('/', myVideosHistoryRouter)
|
||||||
usersRouter.use('/', meRouter)
|
usersRouter.use('/', meRouter)
|
||||||
|
|
||||||
usersRouter.get('/autocomplete',
|
usersRouter.get('/autocomplete',
|
||||||
|
|
|
@ -330,6 +330,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
|
||||||
if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
|
if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
|
||||||
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
|
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
|
||||||
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
|
||||||
|
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
const userAccount = await AccountModel.load(user.Account.id)
|
const userAccount = await AccountModel.load(user.Account.id)
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import {
|
||||||
|
asyncMiddleware,
|
||||||
|
asyncRetryTransactionMiddleware,
|
||||||
|
authenticate,
|
||||||
|
paginationValidator,
|
||||||
|
setDefaultPagination,
|
||||||
|
userHistoryRemoveValidator
|
||||||
|
} from '../../../middlewares'
|
||||||
|
import { UserModel } from '../../../models/account/user'
|
||||||
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
|
import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
|
||||||
|
import { sequelizeTypescript } from '../../../initializers'
|
||||||
|
|
||||||
|
const myVideosHistoryRouter = express.Router()
|
||||||
|
|
||||||
|
myVideosHistoryRouter.get('/me/history/videos',
|
||||||
|
authenticate,
|
||||||
|
paginationValidator,
|
||||||
|
setDefaultPagination,
|
||||||
|
asyncMiddleware(listMyVideosHistory)
|
||||||
|
)
|
||||||
|
|
||||||
|
myVideosHistoryRouter.post('/me/history/videos/remove',
|
||||||
|
authenticate,
|
||||||
|
userHistoryRemoveValidator,
|
||||||
|
asyncRetryTransactionMiddleware(removeUserHistory)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
myVideosHistoryRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function listMyVideosHistory (req: express.Request, res: express.Response) {
|
||||||
|
const user: UserModel = res.locals.oauth.token.User
|
||||||
|
|
||||||
|
const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count)
|
||||||
|
|
||||||
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeUserHistory (req: express.Request, res: express.Response) {
|
||||||
|
const user: UserModel = res.locals.oauth.token.User
|
||||||
|
const beforeDate = req.body.beforeDate || null
|
||||||
|
|
||||||
|
await sequelizeTypescript.transaction(t => {
|
||||||
|
return UserVideoHistoryModel.removeHistoryBefore(user, beforeDate, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Do not send the delete to other instances, we delete OUR copy of this video abuse
|
||||||
|
|
||||||
|
return res.type('json').status(204).end()
|
||||||
|
}
|
|
@ -46,6 +46,10 @@ function isUserWebTorrentEnabledValid (value: any) {
|
||||||
return isBooleanValid(value)
|
return isBooleanValid(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUserVideosHistoryEnabledValid (value: any) {
|
||||||
|
return isBooleanValid(value)
|
||||||
|
}
|
||||||
|
|
||||||
function isUserAutoPlayVideoValid (value: any) {
|
function isUserAutoPlayVideoValid (value: any) {
|
||||||
return isBooleanValid(value)
|
return isBooleanValid(value)
|
||||||
}
|
}
|
||||||
|
@ -73,6 +77,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
isUserVideosHistoryEnabledValid,
|
||||||
isUserBlockedValid,
|
isUserBlockedValid,
|
||||||
isUserPasswordValid,
|
isUserPasswordValid,
|
||||||
isUserBlockedReasonValid,
|
isUserBlockedReasonValid,
|
||||||
|
|
|
@ -16,7 +16,7 @@ let config: IConfig = require('config')
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 295
|
const LAST_MIGRATION_VERSION = 300
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction,
|
||||||
|
queryInterface: Sequelize.QueryInterface,
|
||||||
|
sequelize: Sequelize.Sequelize,
|
||||||
|
db: any
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.queryInterface.addColumn('user', 'videosHistoryEnabled', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -12,3 +12,4 @@ export * from './videos'
|
||||||
export * from './webfinger'
|
export * from './webfinger'
|
||||||
export * from './search'
|
export * from './search'
|
||||||
export * from './server'
|
export * from './server'
|
||||||
|
export * from './user-history'
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import 'express-validator'
|
||||||
|
import { body, param, query } from 'express-validator/check'
|
||||||
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { areValidationErrors } from './utils'
|
||||||
|
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
||||||
|
import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
|
||||||
|
import { UserModel } from '../../models/account/user'
|
||||||
|
import { CONFIG } from '../../initializers'
|
||||||
|
import { isDateValid, toArray } from '../../helpers/custom-validators/misc'
|
||||||
|
|
||||||
|
const userHistoryRemoveValidator = [
|
||||||
|
body('beforeDate')
|
||||||
|
.optional()
|
||||||
|
.custom(isDateValid).withMessage('Should have a valid before date'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
userHistoryRemoveValidator
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
|
||||||
import { isVideoExist } from '../../../helpers/custom-validators/videos'
|
import { isVideoExist } from '../../../helpers/custom-validators/videos'
|
||||||
import { areValidationErrors } from '../utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
|
import { UserModel } from '../../../models/account/user'
|
||||||
|
|
||||||
const videoWatchingValidator = [
|
const videoWatchingValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
@ -17,6 +18,12 @@ const videoWatchingValidator = [
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
if (!await isVideoExist(req.params.videoId, res, 'id')) return
|
if (!await isVideoExist(req.params.videoId, res, 'id')) return
|
||||||
|
|
||||||
|
const user = res.locals.oauth.token.User as UserModel
|
||||||
|
if (user.videosHistoryEnabled === false) {
|
||||||
|
logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id)
|
||||||
|
return res.status(409).end()
|
||||||
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { VideoModel } from '../video/video'
|
import { VideoModel } from '../video/video'
|
||||||
import { UserModel } from './user'
|
import { UserModel } from './user'
|
||||||
|
import { Transaction, Op, DestroyOptions } from 'sequelize'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'userVideoHistory',
|
tableName: 'userVideoHistory',
|
||||||
|
@ -52,4 +53,34 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
})
|
})
|
||||||
User: UserModel
|
User: UserModel
|
||||||
|
|
||||||
|
static listForApi (user: UserModel, start: number, count: number) {
|
||||||
|
return VideoModel.listForApi({
|
||||||
|
start,
|
||||||
|
count,
|
||||||
|
sort: '-UserVideoHistories.updatedAt',
|
||||||
|
nsfw: null, // All
|
||||||
|
includeLocalVideos: true,
|
||||||
|
withFiles: false,
|
||||||
|
user,
|
||||||
|
historyOfUser: user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) {
|
||||||
|
const query: DestroyOptions = {
|
||||||
|
where: {
|
||||||
|
userId: user.id
|
||||||
|
},
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeDate) {
|
||||||
|
query.where.updatedAt = {
|
||||||
|
[Op.lt]: beforeDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserVideoHistoryModel.destroy(query)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,8 @@ import {
|
||||||
isUserUsernameValid,
|
isUserUsernameValid,
|
||||||
isUserVideoQuotaDailyValid,
|
isUserVideoQuotaDailyValid,
|
||||||
isUserVideoQuotaValid,
|
isUserVideoQuotaValid,
|
||||||
isUserWebTorrentEnabledValid
|
isUserWebTorrentEnabledValid,
|
||||||
|
isUserVideosHistoryEnabledValid
|
||||||
} from '../../helpers/custom-validators/users'
|
} from '../../helpers/custom-validators/users'
|
||||||
import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
|
import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
|
||||||
import { OAuthTokenModel } from '../oauth/oauth-token'
|
import { OAuthTokenModel } from '../oauth/oauth-token'
|
||||||
|
@ -114,6 +115,12 @@ export class UserModel extends Model<UserModel> {
|
||||||
@Column
|
@Column
|
||||||
webTorrentEnabled: boolean
|
webTorrentEnabled: boolean
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@Default(true)
|
||||||
|
@Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
|
||||||
|
@Column
|
||||||
|
videosHistoryEnabled: boolean
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Default(true)
|
@Default(true)
|
||||||
@Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
|
@Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
|
||||||
|
|
|
@ -29,7 +29,7 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ [ field, direction ], lastSort ]
|
return [ field.split('.').concat([ direction ]), lastSort ]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
|
function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
|
||||||
|
|
|
@ -153,7 +153,8 @@ type AvailableForListIDsOptions = {
|
||||||
accountId?: number
|
accountId?: number
|
||||||
videoChannelId?: number
|
videoChannelId?: number
|
||||||
trendingDays?: number
|
trendingDays?: number
|
||||||
user?: UserModel
|
user?: UserModel,
|
||||||
|
historyOfUser?: UserModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scopes({
|
@Scopes({
|
||||||
|
@ -416,6 +417,16 @@ type AvailableForListIDsOptions = {
|
||||||
query.subQuery = false
|
query.subQuery = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.historyOfUser) {
|
||||||
|
query.include.push({
|
||||||
|
model: UserVideoHistoryModel,
|
||||||
|
required: true,
|
||||||
|
where: {
|
||||||
|
userId: options.historyOfUser.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return query
|
return query
|
||||||
},
|
},
|
||||||
[ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
|
[ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
|
||||||
|
@ -987,7 +998,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
videoChannelId?: number,
|
videoChannelId?: number,
|
||||||
followerActorId?: number
|
followerActorId?: number
|
||||||
trendingDays?: number,
|
trendingDays?: number,
|
||||||
user?: UserModel
|
user?: UserModel,
|
||||||
|
historyOfUser?: UserModel
|
||||||
}, countVideos = true) {
|
}, countVideos = true) {
|
||||||
if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
|
if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
|
||||||
throw new Error('Try to filter all-local but no user has not the see all videos right')
|
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> {
|
||||||
videoChannelId: options.videoChannelId,
|
videoChannelId: options.videoChannelId,
|
||||||
includeLocalVideos: options.includeLocalVideos,
|
includeLocalVideos: options.includeLocalVideos,
|
||||||
user: options.user,
|
user: options.user,
|
||||||
|
historyOfUser: options.historyOfUser,
|
||||||
trendingDays
|
trendingDays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1341,7 +1354,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ count, rowsId ] = await Promise.all([
|
const [ count, rowsId ] = await Promise.all([
|
||||||
countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined),
|
countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined),
|
||||||
VideoModel.scope(idsScope).findAll(query)
|
VideoModel.scope(idsScope).findAll(query)
|
||||||
])
|
])
|
||||||
const ids = rowsId.map(r => r.id)
|
const ids = rowsId.map(r => r.id)
|
||||||
|
|
|
@ -308,6 +308,14 @@ describe('Test users API validators', function () {
|
||||||
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
|
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid videosHistoryEnabled attribute', async function () {
|
||||||
|
const fields = {
|
||||||
|
videosHistoryEnabled: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
|
||||||
|
})
|
||||||
|
|
||||||
it('Should fail with an non authenticated user', async function () {
|
it('Should fail with an non authenticated user', async function () {
|
||||||
const fields = {
|
const fields = {
|
||||||
currentPassword: 'my super password',
|
currentPassword: 'my super password',
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import {
|
import {
|
||||||
|
checkBadCountPagination,
|
||||||
|
checkBadStartPagination,
|
||||||
flushTests,
|
flushTests,
|
||||||
killallServers,
|
killallServers,
|
||||||
|
makeGetRequest,
|
||||||
makePostBodyRequest,
|
makePostBodyRequest,
|
||||||
makePutBodyRequest,
|
makePutBodyRequest,
|
||||||
runServer,
|
runServer,
|
||||||
|
@ -16,7 +19,9 @@ import {
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe('Test videos history API validator', function () {
|
describe('Test videos history API validator', function () {
|
||||||
let path: string
|
let watchingPath: string
|
||||||
|
let myHistoryPath = '/api/v1/users/me/history/videos'
|
||||||
|
let myHistoryRemove = myHistoryPath + '/remove'
|
||||||
let server: ServerInfo
|
let server: ServerInfo
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
@ -33,14 +38,14 @@ describe('Test videos history API validator', function () {
|
||||||
const res = await uploadVideo(server.url, server.accessToken, {})
|
const res = await uploadVideo(server.url, server.accessToken, {})
|
||||||
const videoUUID = res.body.video.uuid
|
const videoUUID = res.body.video.uuid
|
||||||
|
|
||||||
path = '/api/v1/videos/' + videoUUID + '/watching'
|
watchingPath = '/api/v1/videos/' + videoUUID + '/watching'
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When notifying a user is watching a video', function () {
|
describe('When notifying a user is watching a video', function () {
|
||||||
|
|
||||||
it('Should fail with an unauthenticated user', async function () {
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
const fields = { currentTime: 5 }
|
const fields = { currentTime: 5 }
|
||||||
await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 })
|
await makePutBodyRequest({ url: server.url, path: watchingPath, fields, statusCodeExpected: 401 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an incorrect video id', async function () {
|
it('Should fail with an incorrect video id', async function () {
|
||||||
|
@ -58,13 +63,68 @@ describe('Test videos history API validator', function () {
|
||||||
|
|
||||||
it('Should fail with a bad current time', async function () {
|
it('Should fail with a bad current time', async function () {
|
||||||
const fields = { currentTime: 'hello' }
|
const fields = { currentTime: 'hello' }
|
||||||
await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
|
await makePutBodyRequest({ url: server.url, path: watchingPath, fields, token: server.accessToken, statusCodeExpected: 400 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct parameters', async function () {
|
it('Should succeed with the correct parameters', async function () {
|
||||||
const fields = { currentTime: 5 }
|
const fields = { currentTime: 5 }
|
||||||
|
|
||||||
await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 })
|
await makePutBodyRequest({ url: server.url, path: watchingPath, fields, token: server.accessToken, statusCodeExpected: 204 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When listing user videos history', function () {
|
||||||
|
it('Should fail with a bad start pagination', async function () {
|
||||||
|
await checkBadStartPagination(server.url, myHistoryPath, server.accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad count pagination', async function () {
|
||||||
|
await checkBadCountPagination(server.url, myHistoryPath, server.accessToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makeGetRequest({ url: server.url, path: myHistoryPath, statusCodeExpected: 401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, statusCodeExpected: 200 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When removing user videos history', function () {
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', statusCodeExpected: 401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad beforeDate parameter', async function () {
|
||||||
|
const body = { beforeDate: '15' }
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path: myHistoryRemove,
|
||||||
|
fields: body,
|
||||||
|
statusCodeExpected: 400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with a valid beforeDate param', async function () {
|
||||||
|
const body = { beforeDate: new Date().toISOString() }
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path: myHistoryRemove,
|
||||||
|
fields: body,
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed without body', async function () {
|
||||||
|
await makePostBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
token: server.accessToken,
|
||||||
|
path: myHistoryRemove,
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,17 +3,21 @@
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import {
|
import {
|
||||||
|
createUser,
|
||||||
flushTests,
|
flushTests,
|
||||||
getVideosListWithToken,
|
getVideosListWithToken,
|
||||||
getVideoWithToken,
|
getVideoWithToken,
|
||||||
killallServers, makePutBodyRequest,
|
killallServers,
|
||||||
runServer, searchVideoWithToken,
|
runServer,
|
||||||
|
searchVideoWithToken,
|
||||||
ServerInfo,
|
ServerInfo,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
uploadVideo
|
updateMyUser,
|
||||||
|
uploadVideo,
|
||||||
|
userLogin
|
||||||
} from '../../../../shared/utils'
|
} from '../../../../shared/utils'
|
||||||
import { Video, VideoDetails } from '../../../../shared/models/videos'
|
import { Video, VideoDetails } from '../../../../shared/models/videos'
|
||||||
import { userWatchVideo } from '../../../../shared/utils/videos/video-history'
|
import { listMyVideosHistory, removeMyVideosHistory, userWatchVideo } from '../../../../shared/utils/videos/video-history'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
|
@ -22,6 +26,8 @@ describe('Test videos history', function () {
|
||||||
let video1UUID: string
|
let video1UUID: string
|
||||||
let video2UUID: string
|
let video2UUID: string
|
||||||
let video3UUID: string
|
let video3UUID: string
|
||||||
|
let video3WatchedDate: Date
|
||||||
|
let userAccessToken: string
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
@ -46,6 +52,13 @@ describe('Test videos history', function () {
|
||||||
const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
|
const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
|
||||||
video3UUID = res.body.video.uuid
|
video3UUID = res.body.video.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
username: 'user_1',
|
||||||
|
password: 'super password'
|
||||||
|
}
|
||||||
|
await createUser(server.url, server.accessToken, user.username, user.password)
|
||||||
|
userAccessToken = await userLogin(server, user)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should get videos, without watching history', async function () {
|
it('Should get videos, without watching history', async function () {
|
||||||
|
@ -62,8 +75,8 @@ describe('Test videos history', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should watch the first and second video', async function () {
|
it('Should watch the first and second video', async function () {
|
||||||
await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
|
|
||||||
await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
|
await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
|
||||||
|
await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should return the correct history when listing, searching and getting videos', async function () {
|
it('Should return the correct history when listing, searching and getting videos', async function () {
|
||||||
|
@ -117,6 +130,68 @@ describe('Test videos history', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should have these videos when listing my history', async function () {
|
||||||
|
video3WatchedDate = new Date()
|
||||||
|
await userWatchVideo(server.url, server.accessToken, video3UUID, 2)
|
||||||
|
|
||||||
|
const res = await listMyVideosHistory(server.url, server.accessToken)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(3)
|
||||||
|
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
expect(videos[0].name).to.equal('video 3')
|
||||||
|
expect(videos[1].name).to.equal('video 1')
|
||||||
|
expect(videos[2].name).to.equal('video 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not have videos history on another user', async function () {
|
||||||
|
const res = await listMyVideosHistory(server.url, userAccessToken)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(0)
|
||||||
|
expect(res.body.data).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should clear my history', async function () {
|
||||||
|
await removeMyVideosHistory(server.url, server.accessToken, video3WatchedDate.toISOString())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have my history cleared', async function () {
|
||||||
|
const res = await listMyVideosHistory(server.url, server.accessToken)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(1)
|
||||||
|
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
expect(videos[0].name).to.equal('video 3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should disable videos history', async function () {
|
||||||
|
await updateMyUser({
|
||||||
|
url: server.url,
|
||||||
|
accessToken: server.accessToken,
|
||||||
|
videosHistoryEnabled: false
|
||||||
|
})
|
||||||
|
|
||||||
|
await userWatchVideo(server.url, server.accessToken, video2UUID, 8, 409)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should re-enable videos history', async function () {
|
||||||
|
await updateMyUser({
|
||||||
|
url: server.url,
|
||||||
|
accessToken: server.accessToken,
|
||||||
|
videosHistoryEnabled: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await userWatchVideo(server.url, server.accessToken, video1UUID, 8)
|
||||||
|
|
||||||
|
const res = await listMyVideosHistory(server.url, server.accessToken)
|
||||||
|
|
||||||
|
expect(res.body.total).to.equal(2)
|
||||||
|
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
expect(videos[0].name).to.equal('video 1')
|
||||||
|
expect(videos[1].name).to.equal('video 3')
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
killallServers([ server ])
|
killallServers([ server ])
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,12 @@ import { NSFWPolicyType } from '../videos/nsfw-policy.type'
|
||||||
export interface UserUpdateMe {
|
export interface UserUpdateMe {
|
||||||
displayName?: string
|
displayName?: string
|
||||||
description?: string
|
description?: string
|
||||||
nsfwPolicy?: NSFWPolicyType,
|
nsfwPolicy?: NSFWPolicyType
|
||||||
webTorrentEnabled?: boolean,
|
|
||||||
|
webTorrentEnabled?: boolean
|
||||||
autoPlayVideo?: boolean
|
autoPlayVideo?: boolean
|
||||||
|
videosHistoryEnabled?: boolean
|
||||||
|
|
||||||
email?: string
|
email?: string
|
||||||
currentPassword?: string
|
currentPassword?: string
|
||||||
password?: string
|
password?: string
|
||||||
|
|
|
@ -162,14 +162,15 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
|
||||||
|
|
||||||
function updateMyUser (options: {
|
function updateMyUser (options: {
|
||||||
url: string
|
url: string
|
||||||
accessToken: string,
|
accessToken: string
|
||||||
currentPassword?: string,
|
currentPassword?: string
|
||||||
newPassword?: string,
|
newPassword?: string
|
||||||
nsfwPolicy?: NSFWPolicyType,
|
nsfwPolicy?: NSFWPolicyType
|
||||||
email?: string,
|
email?: string
|
||||||
autoPlayVideo?: boolean
|
autoPlayVideo?: boolean
|
||||||
displayName?: string,
|
displayName?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
videosHistoryEnabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const path = '/api/v1/users/me'
|
const path = '/api/v1/users/me'
|
||||||
|
|
||||||
|
@ -181,6 +182,9 @@ function updateMyUser (options: {
|
||||||
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
|
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
|
||||||
if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
|
if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
|
||||||
if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
|
if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
|
||||||
|
if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
|
||||||
|
toSend['videosHistoryEnabled'] = options.videosHistoryEnabled
|
||||||
|
}
|
||||||
|
|
||||||
return makePutBodyRequest({
|
return makePutBodyRequest({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
|
|
|
@ -1,14 +1,39 @@
|
||||||
import { makePutBodyRequest } from '../requests/requests'
|
import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
|
||||||
|
|
||||||
function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) {
|
function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number, statusCodeExpected = 204) {
|
||||||
const path = '/api/v1/videos/' + videoId + '/watching'
|
const path = '/api/v1/videos/' + videoId + '/watching'
|
||||||
const fields = { currentTime }
|
const fields = { currentTime }
|
||||||
|
|
||||||
return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 })
|
return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
|
||||||
|
}
|
||||||
|
|
||||||
|
function listMyVideosHistory (url: string, token: string) {
|
||||||
|
const path = '/api/v1/users/me/history/videos'
|
||||||
|
|
||||||
|
return makeGetRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token,
|
||||||
|
statusCodeExpected: 200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
|
||||||
|
const path = '/api/v1/users/me/history/videos/remove'
|
||||||
|
|
||||||
|
return makePostBodyRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
token,
|
||||||
|
fields: beforeDate ? { beforeDate } : {},
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
userWatchVideo
|
userWatchVideo,
|
||||||
|
listMyVideosHistory,
|
||||||
|
removeMyVideosHistory
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue