Begin to add avatar to actors

pull/175/head
Chocobozzz 2017-12-29 19:10:13 +01:00
parent 8b0d42ee37
commit c5911fd347
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
41 changed files with 498 additions and 177 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@
/test6/ /test6/
/uploads/ /uploads/
/videos/ /videos/
/avatars/
/thumbnails/ /thumbnails/
/previews/ /previews/
/certs/ /certs/

View File

@ -1,5 +1,5 @@
<div class="user"> <div class="user">
<img [src]="getAvatarPath()" alt="Avatar" /> <img [src]="getAvatarUrl()" alt="Avatar" />
<div class="user-info"> <div class="user-info">
<div class="user-info-username">{{ user.username }}</div> <div class="user-info-username">{{ user.username }}</div>
@ -7,6 +7,10 @@
</div> </div>
</div> </div>
<div class="button-file">
<span>Change your avatar</span>
<input #avatarfileInput type="file" name="avatarfile" id="avatarfile" (change)="changeAvatar()" />
</div>
<div class="account-title">Account settings</div> <div class="account-title">Account settings</div>
<my-account-change-password></my-account-change-password> <my-account-change-password></my-account-change-password>

View File

@ -21,6 +21,12 @@
} }
} }
.button-file {
@include peertube-button-file(auto);
margin-top: 10px;
}
.account-title { .account-title {
text-transform: uppercase; text-transform: uppercase;
color: $orange-color; color: $orange-color;

View File

@ -1,6 +1,10 @@
import { Component, OnInit } from '@angular/core' import { HttpEventType, HttpResponse } from '@angular/common/http'
import { Component, OnInit, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { VideoPrivacy } from '../../../../../shared/models/videos'
import { User } from '../../shared' import { User } from '../../shared'
import { AuthService } from '../../core' import { AuthService } from '../../core'
import { UserService } from '../../shared/users'
@Component({ @Component({
selector: 'my-account-settings', selector: 'my-account-settings',
@ -8,15 +12,39 @@ import { AuthService } from '../../core'
styleUrls: [ './account-settings.component.scss' ] styleUrls: [ './account-settings.component.scss' ]
}) })
export class AccountSettingsComponent implements OnInit { export class AccountSettingsComponent implements OnInit {
@ViewChild('avatarfileInput') avatarfileInput
user: User = null user: User = null
constructor (private authService: AuthService) {} constructor (
private userService: UserService,
private authService: AuthService,
private notificationsService: NotificationsService
) {}
ngOnInit () { ngOnInit () {
this.user = this.authService.getUser() this.user = this.authService.getUser()
} }
getAvatarPath () { getAvatarUrl () {
return this.user.getAvatarPath() return this.user.getAvatarUrl()
}
changeAvatar () {
const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
const formData = new FormData()
formData.append('avatarfile', avatarfile)
this.userService.changeAvatar(formData)
.subscribe(
data => {
this.notificationsService.success('Success', 'Avatar changed.')
this.user.account.avatar = data.avatar
},
err => this.notificationsService.error('Error', err.message)
)
} }
} }

View File

@ -68,7 +68,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
.subscribe( .subscribe(
res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`), res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`),
err => this.notificationsService.error('Error', err.text) err => this.notificationsService.error('Error', err.message)
) )
} }
) )
@ -86,7 +86,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
this.spliceVideosById(video.id) this.spliceVideosById(video.id)
}, },
error => this.notificationsService.error('Error', error.text) error => this.notificationsService.error('Error', error.message)
) )
} }
) )

View File

@ -9,8 +9,8 @@ import 'rxjs/add/operator/mergeMap'
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import { ReplaySubject } from 'rxjs/ReplaySubject' import { ReplaySubject } from 'rxjs/ReplaySubject'
import { Subject } from 'rxjs/Subject' import { Subject } from 'rxjs/Subject'
import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared' import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared'
import { Account } from '../../../../../shared/models/actors' import { User } from '../../../../../shared/models/users'
import { UserLogin } from '../../../../../shared/models/users/user-login.model' import { UserLogin } from '../../../../../shared/models/users/user-login.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
import { RestExtractor } from '../../shared/rest' import { RestExtractor } from '../../shared/rest'
@ -25,20 +25,7 @@ interface UserLoginWithUsername extends UserLogin {
username: string username: string
} }
interface UserLoginWithUserInformation extends UserLogin { type UserLoginWithUserInformation = UserLoginWithUsername & User
access_token: string
refresh_token: string
token_type: string
username: string
id: number
role: UserRole
displayNSFW: boolean
autoPlayVideo: boolean
email: string
videoQuota: number
account: Account
videoChannels: VideoChannel[]
}
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@ -209,21 +196,7 @@ export class AuthService {
const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`) const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)
return this.http.get<UserServerModel>(AuthService.BASE_USER_INFORMATION_URL, { headers }) return this.http.get<UserServerModel>(AuthService.BASE_USER_INFORMATION_URL, { headers })
.map(res => { .map(res => Object.assign(obj, res))
const newProperties = {
id: res.id,
role: res.role,
displayNSFW: res.displayNSFW,
autoPlayVideo: res.autoPlayVideo,
email: res.email,
videoQuota: res.videoQuota,
account: res.account,
videoChannels: res.videoChannels
}
return Object.assign(obj, newProperties)
}
)
} }
private handleLogin (obj: UserLoginWithUserInformation) { private handleLogin (obj: UserLoginWithUserInformation) {

View File

@ -1,6 +1,6 @@
<menu> <menu>
<div *ngIf="isLoggedIn" class="logged-in-block"> <div *ngIf="isLoggedIn" class="logged-in-block">
<img [src]="getUserAvatarPath()" alt="Avatar" /> <img [src]="getUserAvatarUrl()" alt="Avatar" />
<div class="logged-in-info"> <div class="logged-in-info">
<a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a> <a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a>

View File

@ -51,8 +51,8 @@ export class MenuComponent implements OnInit {
) )
} }
getUserAvatarPath () { getUserAvatarUrl () {
return this.user.getAvatarPath() return this.user.getAvatarUrl()
} }
isRegistrationAllowed () { isRegistrationAllowed () {

View File

@ -1,11 +1,13 @@
import { Account as ServerAccount } from '../../../../../shared/models/actors/account.model' import { Account as ServerAccount } from '../../../../../shared/models/actors/account.model'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model' import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
import { getAbsoluteAPIUrl } from '../misc/utils'
export class Account implements ServerAccount { export class Account implements ServerAccount {
id: number id: number
uuid: string uuid: string
name: string name: string
displayName: string
host: string host: string
followingCount: number followingCount: number
followersCount: number followersCount: number
@ -13,9 +15,11 @@ export class Account implements ServerAccount {
updatedAt: Date updatedAt: Date
avatar: Avatar avatar: Avatar
static GET_ACCOUNT_AVATAR_PATH (account: Account) { static GET_ACCOUNT_AVATAR_URL (account: Account) {
if (account && account.avatar) return account.avatar.path const absoluteAPIUrl = getAbsoluteAPIUrl()
return '/client/assets/images/default-avatar.png' if (account && account.avatar) return absoluteAPIUrl + account.avatar.path
return window.location.origin + '/client/assets/images/default-avatar.png'
} }
} }

View File

@ -1,5 +1,6 @@
// Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
import { environment } from '../../../environments/environment'
import { AuthService } from '../../core/auth' import { AuthService } from '../../core/auth'
function getParameterByName (name: string, url: string) { function getParameterByName (name: string, url: string) {
@ -38,8 +39,19 @@ function populateAsyncUserVideoChannels (authService: AuthService, channel: any[
}) })
} }
function getAbsoluteAPIUrl () {
let absoluteAPIUrl = environment.apiUrl
if (!absoluteAPIUrl) {
// The API is on the same domain
absoluteAPIUrl = window.location.origin
}
return absoluteAPIUrl
}
export { export {
viewportHeight, viewportHeight,
getParameterByName, getParameterByName,
populateAsyncUserVideoChannels populateAsyncUserVideoChannels,
getAbsoluteAPIUrl
} }

View File

@ -57,7 +57,7 @@ export class User implements UserServerModel {
return hasUserRight(this.role, right) return hasUserRight(this.role, right)
} }
getAvatarPath () { getAvatarUrl () {
return Account.GET_ACCOUNT_AVATAR_PATH(this.account) return Account.GET_ACCOUNT_AVATAR_URL(this.account)
} }
} }

View File

@ -5,6 +5,7 @@ import 'rxjs/add/operator/map'
import { UserCreate, UserUpdateMe } from '../../../../../shared' import { UserCreate, UserUpdateMe } from '../../../../../shared'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
import { RestExtractor } from '../rest' import { RestExtractor } from '../rest'
import { User } from './user.model'
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -34,9 +35,24 @@ export class UserService {
.catch(res => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
changeAvatar (avatarForm: FormData) {
const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
return this.authHttp.post(url, avatarForm)
.catch(this.restExtractor.handleError)
}
signup (userCreate: UserCreate) { signup (userCreate: UserCreate) {
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch(res => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
getMyInformation () {
const url = UserService.BASE_USERS_URL + 'me'
return this.authHttp.get(url)
.map((userHash: any) => new User(userHash))
.catch(res => this.restExtractor.handleError(res))
}
} }

View File

@ -83,7 +83,7 @@ export abstract class AbstractVideoList implements OnInit {
this.videos = this.videos.concat(videos) this.videos = this.videos.concat(videos)
} }
}, },
error => this.notificationsService.error('Error', error.text) error => this.notificationsService.error('Error', error.message)
) )
} }

View File

@ -2,6 +2,7 @@ import { User } from '../'
import { Video as VideoServerModel } from '../../../../../shared' import { Video as VideoServerModel } from '../../../../../shared'
import { Account } from '../../../../../shared/models/actors' import { Account } from '../../../../../shared/models/actors'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
import { getAbsoluteAPIUrl } from '../misc/utils'
export class Video implements VideoServerModel { export class Video implements VideoServerModel {
accountName: string accountName: string
@ -48,11 +49,7 @@ export class Video implements VideoServerModel {
} }
constructor (hash: VideoServerModel) { constructor (hash: VideoServerModel) {
let absoluteAPIUrl = environment.apiUrl const absoluteAPIUrl = getAbsoluteAPIUrl()
if (!absoluteAPIUrl) {
// The API is on the same domain
absoluteAPIUrl = window.location.origin
}
this.accountName = hash.accountName this.accountName = hash.accountName
this.createdAt = new Date(hash.createdAt.toString()) this.createdAt = new Date(hash.createdAt.toString())

View File

@ -144,8 +144,3 @@
} }
} }
} }
.little-information {
font-size: 0.8em;
font-style: italic;
}

View File

@ -34,30 +34,9 @@
} }
.button-file { .button-file {
position: relative; @include peertube-button-file(190px);
overflow: hidden;
display: inline-block;
margin-bottom: 45px; margin-bottom: 45px;
width: 190px;
@include peertube-button;
@include orange-button;
input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
background: white;
cursor: inherit;
display: block;
}
} }
} }
} }

View File

@ -148,7 +148,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.router.navigate(['/videos/list']) this.router.navigate(['/videos/list'])
}, },
error => this.notificationsService.error('Error', error.text) error => this.notificationsService.error('Error', error.message)
) )
} }
) )
@ -185,7 +185,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
error => { error => {
this.descriptionLoading = false this.descriptionLoading = false
this.notificationsService.error('Error', error.text) this.notificationsService.error('Error', error.message)
} }
) )
} }
@ -217,7 +217,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
} }
getAvatarPath () { getAvatarPath () {
return Account.GET_ACCOUNT_AVATAR_PATH(this.video.account) return Account.GET_ACCOUNT_AVATAR_URL(this.video.account)
} }
getVideoTags () { getVideoTags () {
@ -247,7 +247,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.router.navigate([ '/videos/list' ]) this.router.navigate([ '/videos/list' ])
}, },
error => this.notificationsService.error('Error', error.text) error => this.notificationsService.error('Error', error.message)
) )
} }
) )

View File

@ -84,6 +84,32 @@
@include peertube-button; @include peertube-button;
} }
@mixin peertube-button-file ($width) {
position: relative;
overflow: hidden;
display: inline-block;
width: $width;
@include peertube-button;
@include orange-button;
input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
background: white;
cursor: inherit;
display: block;
}
}
@mixin avatar ($size) { @mixin avatar ($size) {
width: $size; width: $size;
height: $size; height: $size;

View File

@ -16,17 +16,17 @@ import { VideoShareModel } from '../../models/video/video-share'
const activityPubClientRouter = express.Router() const activityPubClientRouter = express.Router()
activityPubClientRouter.get('/account/:name', activityPubClientRouter.get('/accounts/:name',
executeIfActivityPub(asyncMiddleware(localAccountValidator)), executeIfActivityPub(asyncMiddleware(localAccountValidator)),
executeIfActivityPub(accountController) executeIfActivityPub(accountController)
) )
activityPubClientRouter.get('/account/:name/followers', activityPubClientRouter.get('/accounts/:name/followers',
executeIfActivityPub(asyncMiddleware(localAccountValidator)), executeIfActivityPub(asyncMiddleware(localAccountValidator)),
executeIfActivityPub(asyncMiddleware(accountFollowersController)) executeIfActivityPub(asyncMiddleware(accountFollowersController))
) )
activityPubClientRouter.get('/account/:name/following', activityPubClientRouter.get('/accounts/:name/following',
executeIfActivityPub(asyncMiddleware(localAccountValidator)), executeIfActivityPub(asyncMiddleware(localAccountValidator)),
executeIfActivityPub(asyncMiddleware(accountFollowingController)) executeIfActivityPub(asyncMiddleware(accountFollowingController))
) )

View File

@ -1,20 +1,26 @@
import * as express from 'express' import * as express from 'express'
import { extname, join } from 'path'
import * as uuidv4 from 'uuid/v4'
import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared'
import { renamePromise } from '../../helpers/core-utils'
import { retryTransactionWrapper } from '../../helpers/database-utils' import { retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { getFormattedObjects } from '../../helpers/utils' import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
import { CONFIG } from '../../initializers' import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
import { createUserAccountAndChannel } from '../../lib/user' import { createUserAccountAndChannel } from '../../lib/user'
import { import {
asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort, asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort,
setVideosSort, token, usersAddValidator, usersGetValidator, usersRegisterValidator, usersRemoveValidator, usersSortValidator, setVideosSort, token, usersAddValidator, usersGetValidator, usersRegisterValidator, usersRemoveValidator, usersSortValidator,
usersUpdateMeValidator, usersUpdateValidator, usersVideoRatingValidator usersUpdateMeValidator, usersUpdateValidator, usersVideoRatingValidator
} from '../../middlewares' } from '../../middlewares'
import { videosSortValidator } from '../../middlewares/validators' import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators'
import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { UserModel } from '../../models/account/user' import { UserModel } from '../../models/account/user'
import { AvatarModel } from '../../models/avatar/avatar'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT)
const usersRouter = express.Router() const usersRouter = express.Router()
usersRouter.get('/me', usersRouter.get('/me',
@ -71,6 +77,13 @@ usersRouter.put('/me',
asyncMiddleware(updateMe) asyncMiddleware(updateMe)
) )
usersRouter.post('/me/avatar/pick',
authenticate,
reqAvatarFile,
usersUpdateMyAvatarValidator,
asyncMiddleware(updateMyAvatar)
)
usersRouter.put('/:id', usersRouter.put('/:id',
authenticate, authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS), ensureUserHasRight(UserRight.MANAGE_USERS),
@ -216,6 +229,40 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
return res.sendStatus(204) return res.sendStatus(204)
} }
async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
const avatarPhysicalFile = req.files['avatarfile'][0]
const actor = res.locals.oauth.token.user.Account.Actor
const avatarDir = CONFIG.STORAGE.AVATARS_DIR
const source = join(avatarDir, avatarPhysicalFile.filename)
const extension = extname(avatarPhysicalFile.filename)
const avatarName = uuidv4() + extension
const destination = join(avatarDir, avatarName)
await renamePromise(source, destination)
const { avatar } = await sequelizeTypescript.transaction(async t => {
const avatar = await AvatarModel.create({
filename: avatarName
}, { transaction: t })
if (actor.Avatar) {
await actor.Avatar.destroy({ transaction: t })
}
actor.set('avatarId', avatar.id)
await actor.save({ transaction: t })
return { actor, avatar }
})
return res
.json({
avatar: avatar.toFormattedJSON()
})
.end()
}
async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) {
const body: UserUpdate = req.body const body: UserUpdate = req.body
const user = res.locals.user as UserModel const user = res.locals.user as UserModel

View File

@ -6,7 +6,7 @@ import { renamePromise } from '../../../helpers/core-utils'
import { retryTransactionWrapper } from '../../../helpers/database-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils' import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' import { createReqFiles, generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
import { import {
CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES VIDEO_PRIVACIES
@ -29,28 +29,7 @@ import { rateVideoRouter } from './rate'
const videosRouter = express.Router() const videosRouter = express.Router()
// multer configuration const reqVideoFile = createReqFiles('videofile', CONFIG.STORAGE.VIDEOS_DIR, VIDEO_MIMETYPE_EXT)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, CONFIG.STORAGE.VIDEOS_DIR)
},
filename: async (req, file, cb) => {
const extension = VIDEO_MIMETYPE_EXT[file.mimetype]
let randomString = ''
try {
randomString = await generateRandomString(16)
} catch (err) {
logger.error('Cannot generate random string for file name.', err)
randomString = 'fake-random-string'
}
cb(null, randomString + extension)
}
})
const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
videosRouter.use('/', abuseVideoRouter) videosRouter.use('/', abuseVideoRouter)
videosRouter.use('/', blacklistRouter) videosRouter.use('/', blacklistRouter)
@ -85,7 +64,7 @@ videosRouter.put('/:id',
) )
videosRouter.post('/upload', videosRouter.post('/upload',
authenticate, authenticate,
reqFiles, reqVideoFile,
asyncMiddleware(videosAddValidator), asyncMiddleware(videosAddValidator),
asyncMiddleware(addVideoRetryWrapper) asyncMiddleware(addVideoRetryWrapper)
) )

View File

@ -32,6 +32,12 @@ staticRouter.use(
express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE }) express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE })
) )
const avatarsPhysicalPath = CONFIG.STORAGE.AVATARS_DIR
staticRouter.use(
STATIC_PATHS.AVATARS,
express.static(avatarsPhysicalPath, { maxAge: STATIC_MAX_AGE })
)
// Video previews path for express // Video previews path for express
staticRouter.use( staticRouter.use(
STATIC_PATHS.PREVIEWS + ':uuid.jpg', STATIC_PATHS.PREVIEWS + ':uuid.jpg',

View File

@ -1,7 +1,7 @@
import * as validator from 'validator' import * as validator from 'validator'
import 'express-validator' import 'express-validator'
import { exists } from './misc' import { exists, isArray } from './misc'
import { CONSTRAINTS_FIELDS } from '../../initializers' import { CONSTRAINTS_FIELDS } from '../../initializers'
import { UserRole } from '../../../shared' import { UserRole } from '../../../shared'
@ -37,6 +37,22 @@ function isUserRoleValid (value: any) {
return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
} }
function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
// Should have files
if (!files) return false
if (isArray(files)) return false
// Should have videofile file
const avatarfile = files['avatarfile']
if (!avatarfile || avatarfile.length === 0) return false
// The file should exist
const file = avatarfile[0]
if (!file || !file.originalname) return false
return new RegExp('^image/(png|jpeg)$', 'i').test(file.mimetype)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -45,5 +61,6 @@ export {
isUserVideoQuotaValid, isUserVideoQuotaValid,
isUserUsernameValid, isUserUsernameValid,
isUserDisplayNSFWValid, isUserDisplayNSFWValid,
isUserAutoPlayVideoValid isUserAutoPlayVideoValid,
isAvatarFile
} }

View File

@ -1,8 +1,9 @@
import * as express from 'express' import * as express from 'express'
import * as multer from 'multer'
import { Model } from 'sequelize-typescript' import { Model } from 'sequelize-typescript'
import { ResultList } from '../../shared' import { ResultList } from '../../shared'
import { VideoResolution } from '../../shared/models/videos' import { VideoResolution } from '../../shared/models/videos'
import { CONFIG, REMOTE_SCHEME } from '../initializers' import { CONFIG, REMOTE_SCHEME, VIDEO_MIMETYPE_EXT } from '../initializers'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { ActorModel } from '../models/activitypub/actor' import { ActorModel } from '../models/activitypub/actor'
import { ApplicationModel } from '../models/application/application' import { ApplicationModel } from '../models/application/application'
@ -26,6 +27,30 @@ function badRequest (req: express.Request, res: express.Response, next: express.
return res.type('json').status(400).end() return res.type('json').status(400).end()
} }
function createReqFiles (fieldName: string, storageDir: string, mimeTypes: { [ id: string ]: string }) {
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, storageDir)
},
filename: async (req, file, cb) => {
const extension = mimeTypes[file.mimetype]
let randomString = ''
try {
randomString = await generateRandomString(16)
} catch (err) {
logger.error('Cannot generate random string for file name.', err)
randomString = 'fake-random-string'
}
cb(null, randomString + extension)
}
})
return multer({ storage }).fields([{ name: fieldName, maxCount: 1 }])
}
async function generateRandomString (size: number) { async function generateRandomString (size: number) {
const raw = await pseudoRandomBytesPromise(size) const raw = await pseudoRandomBytesPromise(size)
@ -122,5 +147,6 @@ export {
resetSequelizeInstance, resetSequelizeInstance,
getServerActor, getServerActor,
SortType, SortType,
getHostWithPort getHostWithPort,
createReqFiles
} }

View File

@ -9,7 +9,7 @@ import { isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 145 const LAST_MIGRATION_VERSION = 150
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -172,7 +172,10 @@ const CONSTRAINTS_FIELDS = {
ACTOR: { ACTOR: {
PUBLIC_KEY: { min: 10, max: 5000 }, // Length PUBLIC_KEY: { min: 10, max: 5000 }, // Length
PRIVATE_KEY: { min: 10, max: 5000 }, // Length PRIVATE_KEY: { min: 10, max: 5000 }, // Length
URL: { min: 3, max: 2000 } // Length URL: { min: 3, max: 2000 }, // Length
AVATAR: {
EXTNAME: [ '.png', '.jpeg', '.jpg' ]
}
}, },
VIDEO_EVENTS: { VIDEO_EVENTS: {
COUNT: { min: 0 } COUNT: { min: 0 }
@ -250,6 +253,12 @@ const VIDEO_MIMETYPE_EXT = {
'video/mp4': '.mp4' 'video/mp4': '.mp4'
} }
const AVATAR_MIMETYPE_EXT = {
'image/png': '.png',
'image/jpg': '.jpg',
'image/jpeg': '.jpg'
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const SERVER_ACTOR_NAME = 'peertube' const SERVER_ACTOR_NAME = 'peertube'
@ -291,7 +300,8 @@ const STATIC_PATHS = {
PREVIEWS: '/static/previews/', PREVIEWS: '/static/previews/',
THUMBNAILS: '/static/thumbnails/', THUMBNAILS: '/static/thumbnails/',
TORRENTS: '/static/torrents/', TORRENTS: '/static/torrents/',
WEBSEED: '/static/webseed/' WEBSEED: '/static/webseed/',
AVATARS: '/static/avatars/'
} }
// Cache control // Cache control
@ -376,5 +386,6 @@ export {
VIDEO_PRIVACIES, VIDEO_PRIVACIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_RATE_TYPES, VIDEO_RATE_TYPES,
VIDEO_MIMETYPE_EXT VIDEO_MIMETYPE_EXT,
AVATAR_MIMETYPE_EXT
} }

View File

@ -0,0 +1,28 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction,
queryInterface: Sequelize.QueryInterface,
sequelize: Sequelize.Sequelize
}): Promise<void> {
await utils.queryInterface.removeConstraint('actor', 'actor_avatarId_fkey')
await utils.queryInterface.addConstraint('actor', [ 'avatarId' ], {
type: 'foreign key',
references: {
table: 'avatar',
field: 'id'
},
onDelete: 'set null',
onUpdate: 'CASCADE'
})
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -1,16 +1,20 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { join } from 'path'
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import * as url from 'url' import * as url from 'url'
import * as uuidv4 from 'uuid/v4'
import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { retryTransactionWrapper } from '../../helpers/database-utils' import { retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
import { doRequest } from '../../helpers/requests' import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
import { CONFIG, sequelizeTypescript } from '../../initializers' import { CONFIG, sequelizeTypescript } from '../../initializers'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor' import { ActorModel } from '../../models/activitypub/actor'
import { AvatarModel } from '../../models/avatar/avatar'
import { ServerModel } from '../../models/server/server' import { ServerModel } from '../../models/server/server'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
@ -62,6 +66,32 @@ async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNee
return actor return actor
} }
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
return new ActorModel({
type,
url,
preferredUsername,
uuid,
publicKey: null,
privateKey: null,
followersCount: 0,
followingCount: 0,
inboxUrl: url + '/inbox',
outboxUrl: url + '/outbox',
sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
followersUrl: url + '/followers',
followingUrl: url + '/following'
})
}
export {
getOrCreateActorAndServerAndModel,
buildActorInstance,
setAsyncActorKeys
}
// ---------------------------------------------------------------------------
function saveActorAndServerAndModelIfNotExist ( function saveActorAndServerAndModelIfNotExist (
result: FetchRemoteActorResult, result: FetchRemoteActorResult,
ownerActor?: ActorModel, ownerActor?: ActorModel,
@ -90,6 +120,14 @@ function saveActorAndServerAndModelIfNotExist (
// Save our new account in database // Save our new account in database
actor.set('serverId', server.id) actor.set('serverId', server.id)
// Avatar?
if (result.avatarName) {
const avatar = await AvatarModel.create({
filename: result.avatarName
}, { transaction: t })
actor.set('avatarId', avatar.id)
}
// Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
// (which could be false in a retried query) // (which could be false in a retried query)
const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t }) const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
@ -112,6 +150,7 @@ type FetchRemoteActorResult = {
actor: ActorModel actor: ActorModel
name: string name: string
summary: string summary: string
avatarName?: string
attributedTo: ActivityPubAttributedTo[] attributedTo: ActivityPubAttributedTo[]
} }
async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> { async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
@ -151,43 +190,33 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
followingUrl: actorJSON.following followingUrl: actorJSON.following
}) })
// Fetch icon?
let avatarName: string = undefined
if (
actorJSON.icon && actorJSON.icon.type === 'Image' && actorJSON.icon.mediaType === 'image/png' &&
isActivityPubUrlValid(actorJSON.icon.url)
) {
const extension = actorJSON.icon.mediaType === 'image/png' ? '.png' : '.jpg'
avatarName = uuidv4() + extension
const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
await doRequestAndSaveToFile({
method: 'GET',
uri: actorJSON.icon.url
}, destPath)
}
const name = actorJSON.name || actorJSON.preferredUsername const name = actorJSON.name || actorJSON.preferredUsername
return { return {
actor, actor,
name, name,
avatarName,
summary: actorJSON.summary, summary: actorJSON.summary,
attributedTo: actorJSON.attributedTo attributedTo: actorJSON.attributedTo
} }
} }
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
return new ActorModel({
type,
url,
preferredUsername,
uuid,
publicKey: null,
privateKey: null,
followersCount: 0,
followingCount: 0,
inboxUrl: url + '/inbox',
outboxUrl: url + '/outbox',
sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
followersUrl: url + '/followers',
followingUrl: url + '/following'
})
}
export {
getOrCreateActorAndServerAndModel,
saveActorAndServerAndModelIfNotExist,
fetchRemoteActor,
buildActorInstance,
setAsyncActorKeys
}
// ---------------------------------------------------------------------------
async function fetchActorTotalItems (url: string) { async function fetchActorTotalItems (url: string) {
const options = { const options = {
uri: url, uri: url,

View File

@ -18,7 +18,7 @@ function getVideoChannelActivityPubUrl (videoChannelUUID: string) {
} }
function getAccountActivityPubUrl (accountName: string) { function getAccountActivityPubUrl (accountName: string) {
return CONFIG.WEBSERVER.URL + '/account/' + accountName return CONFIG.WEBSERVER.URL + '/accounts/' + accountName
} }
function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {

View File

@ -3,12 +3,14 @@ import 'express-validator'
import { body, param } from 'express-validator/check' import { body, param } from 'express-validator/check'
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
import { import {
isAvatarFile,
isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
isUserVideoQuotaValid isUserVideoQuotaValid
} from '../../helpers/custom-validators/users' } from '../../helpers/custom-validators/users'
import { isVideoExist } from '../../helpers/custom-validators/videos' import { isVideoExist, isVideoFile } from '../../helpers/custom-validators/videos'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { isSignupAllowed } from '../../helpers/utils' import { isSignupAllowed } from '../../helpers/utils'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import { UserModel } from '../../models/account/user' import { UserModel } from '../../models/account/user'
import { areValidationErrors } from './utils' import { areValidationErrors } from './utils'
@ -96,6 +98,21 @@ const usersUpdateMeValidator = [
} }
] ]
const usersUpdateMyAvatarValidator = [
body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
'This file is not supported. Please, make sure it is of the following type : '
+ CONSTRAINTS_FIELDS.ACTOR.AVATAR.EXTNAME.join(', ')
),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking usersUpdateMyAvatarValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
return next()
}
]
const usersGetValidator = [ const usersGetValidator = [
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
@ -145,7 +162,8 @@ export {
usersUpdateMeValidator, usersUpdateMeValidator,
usersVideoRatingValidator, usersVideoRatingValidator,
ensureUserRegistrationAllowed, ensureUserRegistrationAllowed,
usersGetValidator usersGetValidator,
usersUpdateMyAvatarValidator
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -13,6 +13,7 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { Account } from '../../../shared/models/actors'
import { isUserUsernameValid } from '../../helpers/custom-validators/users' import { isUserUsernameValid } from '../../helpers/custom-validators/users'
import { sendDeleteActor } from '../../lib/activitypub/send' import { sendDeleteActor } from '../../lib/activitypub/send'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
@ -165,11 +166,12 @@ export class AccountModel extends Model<AccountModel> {
return AccountModel.findOne(query) return AccountModel.findOne(query)
} }
toFormattedJSON () { toFormattedJSON (): Account {
const actor = this.Actor.toFormattedJSON() const actor = this.Actor.toFormattedJSON()
const account = { const account = {
id: this.id, id: this.id,
name: this.name, name: this.Actor.preferredUsername,
displayName: this.name,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt updatedAt: this.updatedAt
} }

View File

@ -4,6 +4,7 @@ import {
Scopes, Table, UpdatedAt Scopes, Table, UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
import { User } from '../../../shared/models/users'
import { import {
isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
isUserVideoQuotaValid isUserVideoQuotaValid
@ -210,7 +211,7 @@ export class UserModel extends Model<UserModel> {
return comparePassword(password, this.password) return comparePassword(password, this.password)
} }
toFormattedJSON () { toFormattedJSON (): User {
const json = { const json = {
id: this.id, id: this.id,
username: this.username, username: this.username,
@ -221,11 +222,12 @@ export class UserModel extends Model<UserModel> {
roleLabel: USER_ROLE_LABELS[ this.role ], roleLabel: USER_ROLE_LABELS[ this.role ],
videoQuota: this.videoQuota, videoQuota: this.videoQuota,
createdAt: this.createdAt, createdAt: this.createdAt,
account: this.Account.toFormattedJSON() account: this.Account.toFormattedJSON(),
videoChannels: []
} }
if (Array.isArray(this.Account.VideoChannels) === true) { if (Array.isArray(this.Account.VideoChannels) === true) {
json['videoChannels'] = this.Account.VideoChannels json.videoChannels = this.Account.VideoChannels
.map(c => c.toFormattedJSON()) .map(c => c.toFormattedJSON())
.sort((v1, v2) => { .sort((v1, v2) => {
if (v1.createdAt < v2.createdAt) return -1 if (v1.createdAt < v2.createdAt) return -1

View File

@ -1,5 +1,5 @@
import { values } from 'lodash' import { values } from 'lodash'
import { join } from 'path' import { extname, join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { import {
AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes,
@ -30,6 +30,10 @@ enum ScopeNames {
{ {
model: () => ServerModel, model: () => ServerModel,
required: false required: false
},
{
model: () => AvatarModel,
required: false
} }
] ]
}) })
@ -47,6 +51,10 @@ enum ScopeNames {
{ {
model: () => ServerModel, model: () => ServerModel,
required: false required: false
},
{
model: () => AvatarModel,
required: false
} }
] ]
} }
@ -141,7 +149,7 @@ export class ActorModel extends Model<ActorModel> {
foreignKey: { foreignKey: {
allowNull: true allowNull: true
}, },
onDelete: 'cascade' onDelete: 'set null'
}) })
Avatar: AvatarModel Avatar: AvatarModel
@ -253,11 +261,7 @@ export class ActorModel extends Model<ActorModel> {
toFormattedJSON () { toFormattedJSON () {
let avatar: Avatar = null let avatar: Avatar = null
if (this.Avatar) { if (this.Avatar) {
avatar = { avatar = this.Avatar.toFormattedJSON()
path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
createdAt: this.Avatar.createdAt,
updatedAt: this.Avatar.updatedAt
}
} }
let score: number let score: number
@ -286,6 +290,16 @@ export class ActorModel extends Model<ActorModel> {
activityPubType = 'Group' as 'Group' activityPubType = 'Group' as 'Group'
} }
let icon = undefined
if (this.avatarId) {
const extension = extname(this.Avatar.filename)
icon = {
type: 'Image',
mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
url: this.getAvatarUrl()
}
}
const json = { const json = {
type: activityPubType, type: activityPubType,
id: this.url, id: this.url,
@ -304,7 +318,8 @@ export class ActorModel extends Model<ActorModel> {
id: this.getPublicKeyUrl(), id: this.getPublicKeyUrl(),
owner: this.url, owner: this.url,
publicKeyPem: this.publicKey publicKeyPem: this.publicKey
} },
icon
} }
return activityPubContextify(json) return activityPubContextify(json)
@ -353,4 +368,10 @@ export class ActorModel extends Model<ActorModel> {
getHost () { getHost () {
return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
} }
getAvatarUrl () {
if (!this.avatarId) return undefined
return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath
}
} }

View File

@ -1,4 +1,10 @@
import { AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' import { join } from 'path'
import { AfterDestroy, AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { unlinkPromise } from '../../helpers/core-utils'
import { logger } from '../../helpers/logger'
import { CONFIG, STATIC_PATHS } from '../../initializers'
import { sendDeleteVideo } from '../../lib/activitypub/send'
@Table({ @Table({
tableName: 'avatar' tableName: 'avatar'
@ -14,4 +20,26 @@ export class AvatarModel extends Model<AvatarModel> {
@UpdatedAt @UpdatedAt
updatedAt: Date updatedAt: Date
@AfterDestroy
static removeFilesAndSendDelete (instance: AvatarModel) {
return instance.removeAvatar()
}
toFormattedJSON (): Avatar {
return {
path: this.getWebserverPath(),
createdAt: this.createdAt,
updatedAt: this.updatedAt
}
}
getWebserverPath () {
return join(STATIC_PATHS.AVATARS, this.filename)
}
removeAvatar () {
const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
return unlinkPromise(avatarPath)
}
} }

View File

@ -214,7 +214,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
static listThreadCommentsForApi (videoId: number, threadId: number) { static listThreadCommentsForApi (videoId: number, threadId: number) {
const query = { const query = {
order: [ [ 'id', 'ASC' ] ], order: [ [ 'createdAt', 'DESC' ] ],
where: { where: {
videoId, videoId,
[ Sequelize.Op.or ]: [ [ Sequelize.Op.or ]: [

View File

@ -2,11 +2,13 @@
import { omit } from 'lodash' import { omit } from 'lodash'
import 'mocha' import 'mocha'
import { join } from "path"
import { UserRole } from '../../../../shared' import { UserRole } from '../../../../shared'
import { import {
createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest,
makePostBodyRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, updateUser, makePostBodyRequest, makePostUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers,
updateUser,
uploadVideo, userLogin uploadVideo, userLogin
} from '../../utils' } from '../../utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
@ -266,6 +268,24 @@ describe('Test users API validators', function () {
}) })
}) })
describe('When updating my avatar', function () {
it('Should fail without an incorrect input file', async function () {
const fields = {}
const attaches = {
'avatarfile': join(__dirname, '..', 'fixtures', 'video_short.mp4')
}
await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
})
it('Should succeed with the correct params', async function () {
const fields = {}
const attaches = {
'avatarfile': join(__dirname, '..', 'fixtures', 'avatar.png')
}
await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
})
})
describe('When updating a user', function () { describe('When updating a user', function () {
before(async function () { before(async function () {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -6,7 +6,7 @@ import { UserRole } from '../../../../shared/index'
import { import {
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList, createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList,
getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo,
runServer, ServerInfo, serverLogin, updateMyUser, updateUser, uploadVideo runServer, ServerInfo, serverLogin, testVideoImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo
} from '../../utils/index' } from '../../utils/index'
import { follow } from '../../utils/server/follows' import { follow } from '../../utils/server/follows'
import { setAccessTokensToServers } from '../../utils/users/login' import { setAccessTokensToServers } from '../../utils/users/login'
@ -340,6 +340,22 @@ describe('Test users', function () {
expect(user.id).to.be.a('number') expect(user.id).to.be.a('number')
}) })
it('Should be able to update my avatar', async function () {
const fixture = 'avatar.png'
await updateMyAvatar({
url: server.url,
accessToken: accessTokenUser,
fixture
})
const res = await getMyUserInformation(server.url, accessTokenUser)
const user = res.body
const test = await testVideoImage(server.url, 'avatar', user.account.avatar.path, '.png')
expect(test).to.equal(true)
})
it('Should be able to update another user', async function () { it('Should be able to update another user', async function () {
await updateUser({ await updateUser({
url: server.url, url: server.url,

View File

@ -1,5 +1,6 @@
import { isAbsolute, join } from 'path'
import * as request from 'supertest' import * as request from 'supertest'
import { makePutBodyRequest } from '../' import { makePostUploadRequest, makePutBodyRequest } from '../'
import { UserRole } from '../../../../shared/index' import { UserRole } from '../../../../shared/index'
@ -137,6 +138,29 @@ function updateMyUser (options: {
}) })
} }
function updateMyAvatar (options: {
url: string,
accessToken: string,
fixture: string
}) {
const path = '/api/v1/users/me/avatar/pick'
let filePath = ''
if (isAbsolute(options.fixture)) {
filePath = options.fixture
} else {
filePath = join(__dirname, '..', '..', 'api', 'fixtures', options.fixture)
}
return makePostUploadRequest({
url: options.url,
path,
token: options.accessToken,
fields: {},
attaches: { avatarfile: filePath },
statusCodeExpected: 200
})
}
function updateUser (options: { function updateUser (options: {
url: string url: string
userId: number, userId: number,
@ -173,5 +197,6 @@ export {
removeUser, removeUser,
updateUser, updateUser,
updateMyUser, updateMyUser,
getUserInformation getUserInformation,
updateMyAvatar
} }

View File

@ -201,7 +201,7 @@ function searchVideoWithSort (url: string, search: string, sort: string) {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
async function testVideoImage (url: string, imageName: string, imagePath: string) { async function testVideoImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
// Don't test images if the node env is not set // Don't test images if the node env is not set
// Because we need a special ffmpeg version for this test // Because we need a special ffmpeg version for this test
if (process.env['NODE_TEST_IMAGE']) { if (process.env['NODE_TEST_IMAGE']) {
@ -209,7 +209,7 @@ async function testVideoImage (url: string, imageName: string, imagePath: string
.get(imagePath) .get(imagePath)
.expect(200) .expect(200)
const data = await readFilePromise(join(__dirname, '..', '..', 'api', 'fixtures', imageName + '.jpg')) const data = await readFilePromise(join(__dirname, '..', '..', 'api', 'fixtures', imageName + extension))
return data.equals(res.body) return data.equals(res.body)
} else { } else {

View File

@ -27,6 +27,10 @@ export interface ActivityPubActor {
} }
// Not used // Not used
// icon: string[] icon: {
type: 'Image'
mediaType: 'image/png'
url: string
}
// liked: string // liked: string
} }

View File

@ -4,6 +4,7 @@ export interface Account {
id: number id: number
uuid: string uuid: string
name: string name: string
displayName: string
host: string host: string
followingCount: number followingCount: number
followersCount: number followersCount: number