Begin to add avatar to actors
This commit is contained in:
parent
8b0d42ee37
commit
c5911fd347
|
@ -7,6 +7,7 @@
|
|||
/test6/
|
||||
/uploads/
|
||||
/videos/
|
||||
/avatars/
|
||||
/thumbnails/
|
||||
/previews/
|
||||
/certs/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="user">
|
||||
<img [src]="getAvatarPath()" alt="Avatar" />
|
||||
<img [src]="getAvatarUrl()" alt="Avatar" />
|
||||
|
||||
<div class="user-info">
|
||||
<div class="user-info-username">{{ user.username }}</div>
|
||||
|
@ -7,6 +7,10 @@
|
|||
</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>
|
||||
<my-account-change-password></my-account-change-password>
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.button-file {
|
||||
@include peertube-button-file(auto);
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.account-title {
|
||||
text-transform: uppercase;
|
||||
color: $orange-color;
|
||||
|
|
|
@ -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 { AuthService } from '../../core'
|
||||
import { UserService } from '../../shared/users'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-settings',
|
||||
|
@ -8,15 +12,39 @@ import { AuthService } from '../../core'
|
|||
styleUrls: [ './account-settings.component.scss' ]
|
||||
})
|
||||
export class AccountSettingsComponent implements OnInit {
|
||||
@ViewChild('avatarfileInput') avatarfileInput
|
||||
|
||||
user: User = null
|
||||
|
||||
constructor (private authService: AuthService) {}
|
||||
constructor (
|
||||
private userService: UserService,
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
this.user = this.authService.getUser()
|
||||
}
|
||||
|
||||
getAvatarPath () {
|
||||
return this.user.getAvatarPath()
|
||||
getAvatarUrl () {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
|
|||
.subscribe(
|
||||
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)
|
||||
},
|
||||
|
||||
error => this.notificationsService.error('Error', error.text)
|
||||
error => this.notificationsService.error('Error', error.message)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -9,8 +9,8 @@ import 'rxjs/add/operator/mergeMap'
|
|||
import { Observable } from 'rxjs/Observable'
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject'
|
||||
import { Subject } from 'rxjs/Subject'
|
||||
import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared'
|
||||
import { Account } from '../../../../../shared/models/actors'
|
||||
import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared'
|
||||
import { User } from '../../../../../shared/models/users'
|
||||
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { RestExtractor } from '../../shared/rest'
|
||||
|
@ -25,20 +25,7 @@ interface UserLoginWithUsername extends UserLogin {
|
|||
username: string
|
||||
}
|
||||
|
||||
interface UserLoginWithUserInformation extends UserLogin {
|
||||
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[]
|
||||
}
|
||||
type UserLoginWithUserInformation = UserLoginWithUsername & User
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
@ -209,21 +196,7 @@ export class AuthService {
|
|||
const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)
|
||||
|
||||
return this.http.get<UserServerModel>(AuthService.BASE_USER_INFORMATION_URL, { headers })
|
||||
.map(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)
|
||||
}
|
||||
)
|
||||
.map(res => Object.assign(obj, res))
|
||||
}
|
||||
|
||||
private handleLogin (obj: UserLoginWithUserInformation) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<menu>
|
||||
<div *ngIf="isLoggedIn" class="logged-in-block">
|
||||
<img [src]="getUserAvatarPath()" alt="Avatar" />
|
||||
<img [src]="getUserAvatarUrl()" alt="Avatar" />
|
||||
|
||||
<div class="logged-in-info">
|
||||
<a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a>
|
||||
|
|
|
@ -51,8 +51,8 @@ export class MenuComponent implements OnInit {
|
|||
)
|
||||
}
|
||||
|
||||
getUserAvatarPath () {
|
||||
return this.user.getAvatarPath()
|
||||
getUserAvatarUrl () {
|
||||
return this.user.getAvatarUrl()
|
||||
}
|
||||
|
||||
isRegistrationAllowed () {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { Account as ServerAccount } from '../../../../../shared/models/actors/account.model'
|
||||
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { getAbsoluteAPIUrl } from '../misc/utils'
|
||||
|
||||
export class Account implements ServerAccount {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
displayName: string
|
||||
host: string
|
||||
followingCount: number
|
||||
followersCount: number
|
||||
|
@ -13,9 +15,11 @@ export class Account implements ServerAccount {
|
|||
updatedAt: Date
|
||||
avatar: Avatar
|
||||
|
||||
static GET_ACCOUNT_AVATAR_PATH (account: Account) {
|
||||
if (account && account.avatar) return account.avatar.path
|
||||
static GET_ACCOUNT_AVATAR_URL (account: Account) {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// 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'
|
||||
|
||||
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 {
|
||||
viewportHeight,
|
||||
getParameterByName,
|
||||
populateAsyncUserVideoChannels
|
||||
populateAsyncUserVideoChannels,
|
||||
getAbsoluteAPIUrl
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export class User implements UserServerModel {
|
|||
return hasUserRight(this.role, right)
|
||||
}
|
||||
|
||||
getAvatarPath () {
|
||||
return Account.GET_ACCOUNT_AVATAR_PATH(this.account)
|
||||
getAvatarUrl () {
|
||||
return Account.GET_ACCOUNT_AVATAR_URL(this.account)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'rxjs/add/operator/map'
|
|||
import { UserCreate, UserUpdateMe } from '../../../../../shared'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { RestExtractor } from '../rest'
|
||||
import { User } from './user.model'
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
|
@ -34,9 +35,24 @@ export class UserService {
|
|||
.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) {
|
||||
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export abstract class AbstractVideoList implements OnInit {
|
|||
this.videos = this.videos.concat(videos)
|
||||
}
|
||||
},
|
||||
error => this.notificationsService.error('Error', error.text)
|
||||
error => this.notificationsService.error('Error', error.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { User } from '../'
|
|||
import { Video as VideoServerModel } from '../../../../../shared'
|
||||
import { Account } from '../../../../../shared/models/actors'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { getAbsoluteAPIUrl } from '../misc/utils'
|
||||
|
||||
export class Video implements VideoServerModel {
|
||||
accountName: string
|
||||
|
@ -48,11 +49,7 @@ export class Video implements VideoServerModel {
|
|||
}
|
||||
|
||||
constructor (hash: VideoServerModel) {
|
||||
let absoluteAPIUrl = environment.apiUrl
|
||||
if (!absoluteAPIUrl) {
|
||||
// The API is on the same domain
|
||||
absoluteAPIUrl = window.location.origin
|
||||
}
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
|
||||
this.accountName = hash.accountName
|
||||
this.createdAt = new Date(hash.createdAt.toString())
|
||||
|
|
|
@ -144,8 +144,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.little-information {
|
||||
font-size: 0.8em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
@ -34,30 +34,9 @@
|
|||
}
|
||||
|
||||
.button-file {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
@include peertube-button-file(190px);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
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 => {
|
||||
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 () {
|
||||
return Account.GET_ACCOUNT_AVATAR_PATH(this.video.account)
|
||||
return Account.GET_ACCOUNT_AVATAR_URL(this.video.account)
|
||||
}
|
||||
|
||||
getVideoTags () {
|
||||
|
@ -247,7 +247,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.router.navigate([ '/videos/list' ])
|
||||
},
|
||||
|
||||
error => this.notificationsService.error('Error', error.text)
|
||||
error => this.notificationsService.error('Error', error.message)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -84,6 +84,32 @@
|
|||
@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) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
|
|
|
@ -16,17 +16,17 @@ import { VideoShareModel } from '../../models/video/video-share'
|
|||
|
||||
const activityPubClientRouter = express.Router()
|
||||
|
||||
activityPubClientRouter.get('/account/:name',
|
||||
activityPubClientRouter.get('/accounts/:name',
|
||||
executeIfActivityPub(asyncMiddleware(localAccountValidator)),
|
||||
executeIfActivityPub(accountController)
|
||||
)
|
||||
|
||||
activityPubClientRouter.get('/account/:name/followers',
|
||||
activityPubClientRouter.get('/accounts/:name/followers',
|
||||
executeIfActivityPub(asyncMiddleware(localAccountValidator)),
|
||||
executeIfActivityPub(asyncMiddleware(accountFollowersController))
|
||||
)
|
||||
|
||||
activityPubClientRouter.get('/account/:name/following',
|
||||
activityPubClientRouter.get('/accounts/:name/following',
|
||||
executeIfActivityPub(asyncMiddleware(localAccountValidator)),
|
||||
executeIfActivityPub(asyncMiddleware(accountFollowingController))
|
||||
)
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
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 { renamePromise } from '../../helpers/core-utils'
|
||||
import { retryTransactionWrapper } from '../../helpers/database-utils'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { getFormattedObjects } from '../../helpers/utils'
|
||||
import { CONFIG } from '../../initializers'
|
||||
import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
|
||||
import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
|
||||
import { createUserAccountAndChannel } from '../../lib/user'
|
||||
import {
|
||||
asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort,
|
||||
setVideosSort, token, usersAddValidator, usersGetValidator, usersRegisterValidator, usersRemoveValidator, usersSortValidator,
|
||||
usersUpdateMeValidator, usersUpdateValidator, usersVideoRatingValidator
|
||||
} from '../../middlewares'
|
||||
import { videosSortValidator } from '../../middlewares/validators'
|
||||
import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators'
|
||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||
import { UserModel } from '../../models/account/user'
|
||||
import { AvatarModel } from '../../models/avatar/avatar'
|
||||
import { VideoModel } from '../../models/video/video'
|
||||
|
||||
const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT)
|
||||
|
||||
const usersRouter = express.Router()
|
||||
|
||||
usersRouter.get('/me',
|
||||
|
@ -71,6 +77,13 @@ usersRouter.put('/me',
|
|||
asyncMiddleware(updateMe)
|
||||
)
|
||||
|
||||
usersRouter.post('/me/avatar/pick',
|
||||
authenticate,
|
||||
reqAvatarFile,
|
||||
usersUpdateMyAvatarValidator,
|
||||
asyncMiddleware(updateMyAvatar)
|
||||
)
|
||||
|
||||
usersRouter.put('/:id',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.MANAGE_USERS),
|
||||
|
@ -216,6 +229,40 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
|
|||
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) {
|
||||
const body: UserUpdate = req.body
|
||||
const user = res.locals.user as UserModel
|
||||
|
|
|
@ -6,7 +6,7 @@ import { renamePromise } from '../../../helpers/core-utils'
|
|||
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||
import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
|
||||
import { createReqFiles, generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
|
||||
import {
|
||||
CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT,
|
||||
VIDEO_PRIVACIES
|
||||
|
@ -29,28 +29,7 @@ import { rateVideoRouter } from './rate'
|
|||
|
||||
const videosRouter = express.Router()
|
||||
|
||||
// multer configuration
|
||||
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 }])
|
||||
const reqVideoFile = createReqFiles('videofile', CONFIG.STORAGE.VIDEOS_DIR, VIDEO_MIMETYPE_EXT)
|
||||
|
||||
videosRouter.use('/', abuseVideoRouter)
|
||||
videosRouter.use('/', blacklistRouter)
|
||||
|
@ -85,7 +64,7 @@ videosRouter.put('/:id',
|
|||
)
|
||||
videosRouter.post('/upload',
|
||||
authenticate,
|
||||
reqFiles,
|
||||
reqVideoFile,
|
||||
asyncMiddleware(videosAddValidator),
|
||||
asyncMiddleware(addVideoRetryWrapper)
|
||||
)
|
||||
|
|
|
@ -32,6 +32,12 @@ staticRouter.use(
|
|||
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
|
||||
staticRouter.use(
|
||||
STATIC_PATHS.PREVIEWS + ':uuid.jpg',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as validator from 'validator'
|
||||
import 'express-validator'
|
||||
|
||||
import { exists } from './misc'
|
||||
import { exists, isArray } from './misc'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
import { UserRole } from '../../../shared'
|
||||
|
||||
|
@ -37,6 +37,22 @@ function isUserRoleValid (value: any) {
|
|||
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 {
|
||||
|
@ -45,5 +61,6 @@ export {
|
|||
isUserVideoQuotaValid,
|
||||
isUserUsernameValid,
|
||||
isUserDisplayNSFWValid,
|
||||
isUserAutoPlayVideoValid
|
||||
isUserAutoPlayVideoValid,
|
||||
isAvatarFile
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as express from 'express'
|
||||
import * as multer from 'multer'
|
||||
import { Model } from 'sequelize-typescript'
|
||||
import { ResultList } from '../../shared'
|
||||
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 { ActorModel } from '../models/activitypub/actor'
|
||||
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()
|
||||
}
|
||||
|
||||
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) {
|
||||
const raw = await pseudoRandomBytesPromise(size)
|
||||
|
||||
|
@ -122,5 +147,6 @@ export {
|
|||
resetSequelizeInstance,
|
||||
getServerActor,
|
||||
SortType,
|
||||
getHostWithPort
|
||||
getHostWithPort,
|
||||
createReqFiles
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
PUBLIC_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: {
|
||||
COUNT: { min: 0 }
|
||||
|
@ -250,6 +253,12 @@ const VIDEO_MIMETYPE_EXT = {
|
|||
'video/mp4': '.mp4'
|
||||
}
|
||||
|
||||
const AVATAR_MIMETYPE_EXT = {
|
||||
'image/png': '.png',
|
||||
'image/jpg': '.jpg',
|
||||
'image/jpeg': '.jpg'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SERVER_ACTOR_NAME = 'peertube'
|
||||
|
@ -291,7 +300,8 @@ const STATIC_PATHS = {
|
|||
PREVIEWS: '/static/previews/',
|
||||
THUMBNAILS: '/static/thumbnails/',
|
||||
TORRENTS: '/static/torrents/',
|
||||
WEBSEED: '/static/webseed/'
|
||||
WEBSEED: '/static/webseed/',
|
||||
AVATARS: '/static/avatars/'
|
||||
}
|
||||
|
||||
// Cache control
|
||||
|
@ -376,5 +386,6 @@ export {
|
|||
VIDEO_PRIVACIES,
|
||||
VIDEO_LICENCES,
|
||||
VIDEO_RATE_TYPES,
|
||||
VIDEO_MIMETYPE_EXT
|
||||
VIDEO_MIMETYPE_EXT,
|
||||
AVATAR_MIMETYPE_EXT
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,16 +1,20 @@
|
|||
import * as Bluebird from 'bluebird'
|
||||
import { join } from 'path'
|
||||
import { Transaction } from 'sequelize'
|
||||
import * as url from 'url'
|
||||
import * as uuidv4 from 'uuid/v4'
|
||||
import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
|
||||
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
|
||||
import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { retryTransactionWrapper } from '../../helpers/database-utils'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
|
||||
import { doRequest } from '../../helpers/requests'
|
||||
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
|
||||
import { CONFIG, sequelizeTypescript } from '../../initializers'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { ActorModel } from '../../models/activitypub/actor'
|
||||
import { AvatarModel } from '../../models/avatar/avatar'
|
||||
import { ServerModel } from '../../models/server/server'
|
||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||
|
||||
|
@ -62,6 +66,32 @@ async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNee
|
|||
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 (
|
||||
result: FetchRemoteActorResult,
|
||||
ownerActor?: ActorModel,
|
||||
|
@ -90,6 +120,14 @@ function saveActorAndServerAndModelIfNotExist (
|
|||
// Save our new account in database
|
||||
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
|
||||
// (which could be false in a retried query)
|
||||
const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
|
||||
|
@ -112,6 +150,7 @@ type FetchRemoteActorResult = {
|
|||
actor: ActorModel
|
||||
name: string
|
||||
summary: string
|
||||
avatarName?: string
|
||||
attributedTo: ActivityPubAttributedTo[]
|
||||
}
|
||||
async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
|
||||
|
@ -151,43 +190,33 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
|
|||
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
|
||||
return {
|
||||
actor,
|
||||
name,
|
||||
avatarName,
|
||||
summary: actorJSON.summary,
|
||||
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) {
|
||||
const options = {
|
||||
uri: url,
|
||||
|
|
|
@ -18,7 +18,7 @@ function getVideoChannelActivityPubUrl (videoChannelUUID: string) {
|
|||
}
|
||||
|
||||
function getAccountActivityPubUrl (accountName: string) {
|
||||
return CONFIG.WEBSERVER.URL + '/account/' + accountName
|
||||
return CONFIG.WEBSERVER.URL + '/accounts/' + accountName
|
||||
}
|
||||
|
||||
function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
|
||||
|
|
|
@ -3,12 +3,14 @@ import 'express-validator'
|
|||
import { body, param } from 'express-validator/check'
|
||||
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
||||
import {
|
||||
isAvatarFile,
|
||||
isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
|
||||
isUserVideoQuotaValid
|
||||
} 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 { isSignupAllowed } from '../../helpers/utils'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||
import { UserModel } from '../../models/account/user'
|
||||
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 = [
|
||||
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
|
@ -145,7 +162,8 @@ export {
|
|||
usersUpdateMeValidator,
|
||||
usersVideoRatingValidator,
|
||||
ensureUserRegistrationAllowed,
|
||||
usersGetValidator
|
||||
usersGetValidator,
|
||||
usersUpdateMyAvatarValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { Account } from '../../../shared/models/actors'
|
||||
import { isUserUsernameValid } from '../../helpers/custom-validators/users'
|
||||
import { sendDeleteActor } from '../../lib/activitypub/send'
|
||||
import { ActorModel } from '../activitypub/actor'
|
||||
|
@ -165,11 +166,12 @@ export class AccountModel extends Model<AccountModel> {
|
|||
return AccountModel.findOne(query)
|
||||
}
|
||||
|
||||
toFormattedJSON () {
|
||||
toFormattedJSON (): Account {
|
||||
const actor = this.Actor.toFormattedJSON()
|
||||
const account = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
name: this.Actor.preferredUsername,
|
||||
displayName: this.name,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
Scopes, Table, UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
|
||||
import { User } from '../../../shared/models/users'
|
||||
import {
|
||||
isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
|
||||
isUserVideoQuotaValid
|
||||
|
@ -210,7 +211,7 @@ export class UserModel extends Model<UserModel> {
|
|||
return comparePassword(password, this.password)
|
||||
}
|
||||
|
||||
toFormattedJSON () {
|
||||
toFormattedJSON (): User {
|
||||
const json = {
|
||||
id: this.id,
|
||||
username: this.username,
|
||||
|
@ -221,11 +222,12 @@ export class UserModel extends Model<UserModel> {
|
|||
roleLabel: USER_ROLE_LABELS[ this.role ],
|
||||
videoQuota: this.videoQuota,
|
||||
createdAt: this.createdAt,
|
||||
account: this.Account.toFormattedJSON()
|
||||
account: this.Account.toFormattedJSON(),
|
||||
videoChannels: []
|
||||
}
|
||||
|
||||
if (Array.isArray(this.Account.VideoChannels) === true) {
|
||||
json['videoChannels'] = this.Account.VideoChannels
|
||||
json.videoChannels = this.Account.VideoChannels
|
||||
.map(c => c.toFormattedJSON())
|
||||
.sort((v1, v2) => {
|
||||
if (v1.createdAt < v2.createdAt) return -1
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { values } from 'lodash'
|
||||
import { join } from 'path'
|
||||
import { extname, join } from 'path'
|
||||
import * as Sequelize from 'sequelize'
|
||||
import {
|
||||
AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes,
|
||||
|
@ -30,6 +30,10 @@ enum ScopeNames {
|
|||
{
|
||||
model: () => ServerModel,
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: () => AvatarModel,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -47,6 +51,10 @@ enum ScopeNames {
|
|||
{
|
||||
model: () => ServerModel,
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: () => AvatarModel,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -141,7 +149,7 @@ export class ActorModel extends Model<ActorModel> {
|
|||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
onDelete: 'set null'
|
||||
})
|
||||
Avatar: AvatarModel
|
||||
|
||||
|
@ -253,11 +261,7 @@ export class ActorModel extends Model<ActorModel> {
|
|||
toFormattedJSON () {
|
||||
let avatar: Avatar = null
|
||||
if (this.Avatar) {
|
||||
avatar = {
|
||||
path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
|
||||
createdAt: this.Avatar.createdAt,
|
||||
updatedAt: this.Avatar.updatedAt
|
||||
}
|
||||
avatar = this.Avatar.toFormattedJSON()
|
||||
}
|
||||
|
||||
let score: number
|
||||
|
@ -286,6 +290,16 @@ export class ActorModel extends Model<ActorModel> {
|
|||
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 = {
|
||||
type: activityPubType,
|
||||
id: this.url,
|
||||
|
@ -304,7 +318,8 @@ export class ActorModel extends Model<ActorModel> {
|
|||
id: this.getPublicKeyUrl(),
|
||||
owner: this.url,
|
||||
publicKeyPem: this.publicKey
|
||||
}
|
||||
},
|
||||
icon
|
||||
}
|
||||
|
||||
return activityPubContextify(json)
|
||||
|
@ -353,4 +368,10 @@ export class ActorModel extends Model<ActorModel> {
|
|||
getHost () {
|
||||
return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
|
||||
}
|
||||
|
||||
getAvatarUrl () {
|
||||
if (!this.avatarId) return undefined
|
||||
|
||||
return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
tableName: 'avatar'
|
||||
|
@ -14,4 +20,26 @@ export class AvatarModel extends Model<AvatarModel> {
|
|||
|
||||
@UpdatedAt
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
|||
|
||||
static listThreadCommentsForApi (videoId: number, threadId: number) {
|
||||
const query = {
|
||||
order: [ [ 'id', 'ASC' ] ],
|
||||
order: [ [ 'createdAt', 'DESC' ] ],
|
||||
where: {
|
||||
videoId,
|
||||
[ Sequelize.Op.or ]: [
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
import { omit } from 'lodash'
|
||||
import 'mocha'
|
||||
import { join } from "path"
|
||||
import { UserRole } from '../../../../shared'
|
||||
|
||||
import {
|
||||
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
|
||||
} from '../../utils'
|
||||
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 () {
|
||||
|
||||
before(async function () {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -6,7 +6,7 @@ import { UserRole } from '../../../../shared/index'
|
|||
import {
|
||||
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList,
|
||||
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'
|
||||
import { follow } from '../../utils/server/follows'
|
||||
import { setAccessTokensToServers } from '../../utils/users/login'
|
||||
|
@ -340,6 +340,22 @@ describe('Test users', function () {
|
|||
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 () {
|
||||
await updateUser({
|
||||
url: server.url,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { isAbsolute, join } from 'path'
|
||||
import * as request from 'supertest'
|
||||
import { makePutBodyRequest } from '../'
|
||||
import { makePostUploadRequest, makePutBodyRequest } from '../'
|
||||
|
||||
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: {
|
||||
url: string
|
||||
userId: number,
|
||||
|
@ -173,5 +197,6 @@ export {
|
|||
removeUser,
|
||||
updateUser,
|
||||
updateMyUser,
|
||||
getUserInformation
|
||||
getUserInformation,
|
||||
updateMyAvatar
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ function searchVideoWithSort (url: string, search: string, sort: string) {
|
|||
.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
|
||||
// Because we need a special ffmpeg version for this test
|
||||
if (process.env['NODE_TEST_IMAGE']) {
|
||||
|
@ -209,7 +209,7 @@ async function testVideoImage (url: string, imageName: string, imagePath: string
|
|||
.get(imagePath)
|
||||
.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)
|
||||
} else {
|
||||
|
|
|
@ -27,6 +27,10 @@ export interface ActivityPubActor {
|
|||
}
|
||||
|
||||
// Not used
|
||||
// icon: string[]
|
||||
icon: {
|
||||
type: 'Image'
|
||||
mediaType: 'image/png'
|
||||
url: string
|
||||
}
|
||||
// liked: string
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ export interface Account {
|
|||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
displayName: string
|
||||
host: string
|
||||
followingCount: number
|
||||
followersCount: number
|
||||
|
|
Loading…
Reference in New Issue