API: Add ability to update video channel avatar

pull/758/head
Chocobozzz 2018-06-29 11:29:23 +02:00
parent 3ff5a19b4c
commit 4bbfc6c606
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
12 changed files with 243 additions and 68 deletions

View File

@ -1,14 +1,10 @@
import * as express from 'express'
import 'multer'
import { extname, join } from 'path'
import * as uuidv4 from 'uuid/v4'
import * as RateLimit from 'express-rate-limit'
import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared'
import { processImage } from '../../helpers/image-utils'
import { logger } from '../../helpers/logger'
import { getFormattedObjects } from '../../helpers/utils'
import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, RATES_LIMIT, sequelizeTypescript } from '../../initializers'
import { updateActorAvatarInstance } from '../../lib/activitypub'
import { CONFIG, IMAGE_MIMETYPE_EXT, RATES_LIMIT, sequelizeTypescript } from '../../initializers'
import { sendUpdateActor } from '../../lib/activitypub/send'
import { Emailer } from '../../lib/emailer'
import { Redis } from '../../lib/redis'
@ -33,12 +29,7 @@ import {
usersUpdateValidator,
usersVideoRatingValidator
} from '../../middlewares'
import {
usersAskResetPasswordValidator,
usersResetPasswordValidator,
usersUpdateMyAvatarValidator,
videosSortValidator
} from '../../middlewares/validators'
import { usersAskResetPasswordValidator, usersResetPasswordValidator, videosSortValidator } from '../../middlewares/validators'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { UserModel } from '../../models/account/user'
import { OAuthTokenModel } from '../../models/oauth/oauth-token'
@ -46,6 +37,8 @@ import { VideoModel } from '../../models/video/video'
import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type'
import { createReqFiles } from '../../helpers/express-utils'
import { UserVideoQuota } from '../../../shared/models/users/user-video-quota.model'
import { updateAvatarValidator } from '../../middlewares/validators/avatar'
import { updateActorAvatarFile } from '../../lib/avatar'
const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
const loginRateLimiter = new RateLimit({
@ -121,7 +114,7 @@ usersRouter.put('/me',
usersRouter.post('/me/avatar/pick',
authenticate,
reqAvatarFile,
usersUpdateMyAvatarValidator,
updateAvatarValidator,
asyncMiddleware(updateMyAvatar)
)
@ -304,22 +297,9 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
const user = res.locals.oauth.token.user
const actor = user.Account.Actor
const account = res.locals.oauth.token.user.Account
const extension = extname(avatarPhysicalFile.filename)
const avatarName = uuidv4() + extension
const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
await processImage(avatarPhysicalFile, destination, AVATARS_SIZE)
const avatar = await sequelizeTypescript.transaction(async t => {
const updatedActor = await updateActorAvatarInstance(actor, avatarName, t)
await updatedActor.save({ transaction: t })
await sendUpdateActor(user.Account, t)
return updatedActor.Avatar
})
const avatar = await updateActorAvatarFile(avatarPhysicalFile, account.Actor, account)
return res
.json({

View File

@ -19,12 +19,16 @@ import { videosSortValidator } from '../../middlewares/validators'
import { sendUpdateActor } from '../../lib/activitypub/send'
import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
import { createVideoChannel } from '../../lib/video-channel'
import { isNSFWHidden } from '../../helpers/express-utils'
import { createReqFiles, isNSFWHidden } from '../../helpers/express-utils'
import { setAsyncActorKeys } from '../../lib/activitypub'
import { AccountModel } from '../../models/account/account'
import { sequelizeTypescript } from '../../initializers'
import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
import { logger } from '../../helpers/logger'
import { VideoModel } from '../../models/video/video'
import { updateAvatarValidator } from '../../middlewares/validators/avatar'
import { updateActorAvatarFile } from '../../lib/avatar'
const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
const videoChannelRouter = express.Router()
@ -42,6 +46,15 @@ videoChannelRouter.post('/',
asyncRetryTransactionMiddleware(addVideoChannel)
)
videoChannelRouter.post('/:id/avatar/pick',
authenticate,
reqAvatarFile,
// Check the rights
asyncMiddleware(videoChannelsUpdateValidator),
updateAvatarValidator,
asyncMiddleware(updateVideoChannelAvatar)
)
videoChannelRouter.put('/:id',
authenticate,
asyncMiddleware(videoChannelsUpdateValidator),
@ -83,6 +96,19 @@ async function listVideoChannels (req: express.Request, res: express.Response, n
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function updateVideoChannelAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
const videoChannel = res.locals.videoChannel
const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel.Actor, videoChannel)
return res
.json({
avatar: avatar.toFormattedJSON()
})
.end()
}
async function addVideoChannel (req: express.Request, res: express.Response) {
const videoChannelInfo: VideoChannelCreate = req.body
const account: AccountModel = res.locals.oauth.token.User.Account

34
server/lib/avatar.ts Normal file
View File

@ -0,0 +1,34 @@
import 'multer'
import * as uuidv4 from 'uuid'
import { sendUpdateActor } from './activitypub/send'
import { AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../initializers'
import { updateActorAvatarInstance } from './activitypub'
import { processImage } from '../helpers/image-utils'
import { ActorModel } from '../models/activitypub/actor'
import { AccountModel } from '../models/account/account'
import { VideoChannelModel } from '../models/video/video-channel'
import { extname, join } from 'path'
async function updateActorAvatarFile (
avatarPhysicalFile: Express.Multer.File,
actor: ActorModel,
accountOrChannel: AccountModel | VideoChannelModel
) {
const extension = extname(avatarPhysicalFile.filename)
const avatarName = uuidv4() + extension
const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
await processImage(avatarPhysicalFile, destination, AVATARS_SIZE)
return sequelizeTypescript.transaction(async t => {
const updatedActor = await updateActorAvatarInstance(actor, avatarName, t)
await updatedActor.save({ transaction: t })
await sendUpdateActor(accountOrChannel, t)
return updatedActor.Avatar
})
}
export {
updateActorAvatarFile
}

View File

@ -0,0 +1,25 @@
import * as express from 'express'
import { body } from 'express-validator/check'
import { isAvatarFile } from '../../helpers/custom-validators/users'
import { areValidationErrors } from './utils'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import { logger } from '../../helpers/logger'
const updateAvatarValidator = [
body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
'This file is not supported or too large. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ')
),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking updateAvatarValidator parameters', { files: req.files })
if (areValidationErrors(req, res)) return
return next()
}
]
export {
updateAvatarValidator
}

View File

@ -5,9 +5,9 @@ import { body, param } from 'express-validator/check'
import { omit } from 'lodash'
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
import {
isAvatarFile,
isUserAutoPlayVideoValid,
isUserDescriptionValid, isUserDisplayNameValid,
isUserDescriptionValid,
isUserDisplayNameValid,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
@ -17,7 +17,6 @@ import {
import { isVideoExist } from '../../helpers/custom-validators/videos'
import { logger } from '../../helpers/logger'
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/utils'
import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { Redis } from '../../lib/redis'
import { UserModel } from '../../models/account/user'
import { areValidationErrors } from './utils'
@ -116,21 +115,6 @@ const usersUpdateMeValidator = [
}
]
const usersUpdateMyAvatarValidator = [
body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
'This file is not supported or too large. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ')
),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking usersUpdateMyAvatarValidator parameters', { files: req.files })
if (areValidationErrors(req, res)) return
return next()
}
]
const usersGetValidator = [
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
@ -239,7 +223,6 @@ export {
ensureUserRegistrationAllowed,
ensureUserRegistrationAllowedForIP,
usersGetValidator,
usersUpdateMyAvatarValidator,
usersAskResetPasswordValidator,
usersResetPasswordValidator
}

View File

@ -13,6 +13,8 @@ import { logger } from '../../helpers/logger'
import { UserModel } from '../../models/account/user'
import { VideoChannelModel } from '../../models/video/video-channel'
import { areValidationErrors } from './utils'
import { isAvatarFile } from '../../helpers/custom-validators/users'
import { CONSTRAINTS_FIELDS } from '../../initializers'
const listVideoAccountChannelsValidator = [
param('accountName').exists().withMessage('Should have a valid account name'),

View File

@ -304,6 +304,20 @@ describe('Test users API validators', function () {
await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
})
it('Should fail with an unauthenticated user', async function () {
const fields = {}
const attaches = {
'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png')
}
await makeUploadRequest({
url: server.url,
path: path + '/me/avatar/pick',
fields,
attaches,
statusCodeExpected: 401
})
})
it('Should succeed with the correct params', async function () {
const fields = {}
const attaches = {

View File

@ -14,7 +14,7 @@ import {
killallServers,
makeGetRequest,
makePostBodyRequest,
makePutBodyRequest,
makePutBodyRequest, makeUploadRequest,
runServer,
ServerInfo,
setAccessTokensToServers,
@ -22,6 +22,7 @@ import {
} from '../../utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
import { User } from '../../../../shared/models/users'
import { join } from "path"
const expect = chai.expect
@ -189,6 +190,59 @@ describe('Test video channels API validator', function () {
})
})
describe('When updating video channel avatar', function () {
let path: string
before(async function () {
path = videoChannelPath + '/' + videoChannelUUID
})
it('Should fail with an incorrect input file', async function () {
const fields = {}
const attaches = {
'avatarfile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4')
}
await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
})
it('Should fail with a big file', async function () {
const fields = {}
const attaches = {
'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
}
await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
})
it('Should fail with an unauthenticated user', async function () {
const fields = {}
const attaches = {
'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png')
}
await makeUploadRequest({
url: server.url,
path: path + '/avatar/pick',
fields,
attaches,
statusCodeExpected: 401
})
})
it('Should succeed with the correct params', async function () {
const fields = {}
const attaches = {
'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png')
}
await makeUploadRequest({
url: server.url,
path: path + '/avatar/pick',
token: server.accessToken,
fields,
attaches,
statusCodeExpected: 200
})
})
})
describe('When getting a video channel', function () {
it('Should return the list of the video channels with nothing', async function () {
const res = await makeGetRequest({

View File

@ -3,7 +3,14 @@
import * as chai from 'chai'
import 'mocha'
import { User, Video } from '../../../../shared/index'
import { doubleFollow, flushAndRunMultipleServers, getVideoChannelVideos, updateVideo, uploadVideo } from '../../utils'
import {
doubleFollow,
flushAndRunMultipleServers,
getVideoChannelVideos, testImage,
updateVideo,
updateVideoChannelAvatar,
uploadVideo, wait
} from '../../utils'
import {
addVideoChannel,
deleteVideoChannel,
@ -159,6 +166,31 @@ describe('Test video channels', function () {
}
})
it('Should update video channel avatar', async function () {
this.timeout(5000)
const fixture = 'avatar.png'
await updateVideoChannelAvatar({
url: servers[0].url,
accessToken: servers[0].accessToken,
videoChannelId: secondVideoChannelId,
fixture
})
await waitJobs(servers)
})
it('Should have video channel avatar updated', async function () {
for (const server of servers) {
const res = await getVideoChannelsList(server.url, 0, 1, '-name')
const videoChannel = res.body.data.find(c => c.id === secondVideoChannelId)
await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png')
}
})
it('Should get video channel', async function () {
const res = await getVideoChannel(servers[0].url, secondVideoChannelId)

View File

@ -1,5 +1,6 @@
import * as request from 'supertest'
import { buildAbsoluteFixturePath } from '../miscs/miscs'
import { isAbsolute, join } from 'path'
function makeGetRequest (options: {
url: string,
@ -45,7 +46,7 @@ function makeUploadRequest (options: {
url: string,
method?: 'POST' | 'PUT',
path: string,
token: string,
token?: string,
fields: { [ fieldName: string ]: any },
attaches: { [ attachName: string ]: any },
statusCodeExpected?: number
@ -122,6 +123,29 @@ function makePutBodyRequest (options: {
.expect(options.statusCodeExpected)
}
function updateAvatarRequest (options: {
url: string,
path: string,
accessToken: string,
fixture: string
}) {
let filePath = ''
if (isAbsolute(options.fixture)) {
filePath = options.fixture
} else {
filePath = join(__dirname, '..', '..', 'fixtures', options.fixture)
}
return makeUploadRequest({
url: options.url,
path: options.path,
token: options.accessToken,
fields: {},
attaches: { avatarfile: filePath },
statusCodeExpected: 200
})
}
// ---------------------------------------------------------------------------
export {
@ -129,5 +153,6 @@ export {
makeUploadRequest,
makePostBodyRequest,
makePutBodyRequest,
makeDeleteRequest
makeDeleteRequest,
updateAvatarRequest
}

View File

@ -1,6 +1,5 @@
import { isAbsolute, join } from 'path'
import * as request from 'supertest'
import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../'
import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../'
import { UserRole } from '../../../../shared/index'
import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type'
@ -160,21 +159,8 @@ function updateMyAvatar (options: {
fixture: string
}) {
const path = '/api/v1/users/me/avatar/pick'
let filePath = ''
if (isAbsolute(options.fixture)) {
filePath = options.fixture
} else {
filePath = join(__dirname, '..', '..', 'fixtures', options.fixture)
}
return makeUploadRequest({
url: options.url,
path,
token: options.accessToken,
fields: {},
attaches: { avatarfile: filePath },
statusCodeExpected: 200
})
return updateAvatarRequest(Object.assign(options, { path }))
}
function updateUser (options: {

View File

@ -1,5 +1,6 @@
import * as request from 'supertest'
import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared/models/videos'
import { updateAvatarRequest } from '../index'
function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
const path = '/api/v1/video-channels'
@ -92,9 +93,22 @@ function getVideoChannel (url: string, channelId: number | string) {
.expect('Content-Type', /json/)
}
function updateVideoChannelAvatar (options: {
url: string,
accessToken: string,
fixture: string,
videoChannelId: string | number
}) {
const path = '/api/v1/video-channels/' + options.videoChannelId + '/avatar/pick'
return updateAvatarRequest(Object.assign(options, { path }))
}
// ---------------------------------------------------------------------------
export {
updateVideoChannelAvatar,
getVideoChannelsList,
getAccountVideoChannelsList,
addVideoChannel,