API: Add ability to update video channel avatar
This commit is contained in:
parent
3ff5a19b4c
commit
4bbfc6c606
12 changed files with 243 additions and 68 deletions
|
@ -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({
|
||||
|
|
|
@ -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
34
server/lib/avatar.ts
Normal 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
|
||||
}
|
25
server/middlewares/validators/avatar.ts
Normal file
25
server/middlewares/validators/avatar.ts
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue