mirror of https://github.com/Chocobozzz/PeerTube
Begin to add avatar to actors
parent
8b0d42ee37
commit
c5911fd347
|
@ -7,6 +7,7 @@
|
||||||
/test6/
|
/test6/
|
||||||
/uploads/
|
/uploads/
|
||||||
/videos/
|
/videos/
|
||||||
|
/avatars/
|
||||||
/thumbnails/
|
/thumbnails/
|
||||||
/previews/
|
/previews/
|
||||||
/certs/
|
/certs/
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -51,8 +51,8 @@ export class MenuComponent implements OnInit {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserAvatarPath () {
|
getUserAvatarUrl () {
|
||||||
return this.user.getAvatarPath()
|
return this.user.getAvatarUrl()
|
||||||
}
|
}
|
||||||
|
|
||||||
isRegistrationAllowed () {
|
isRegistrationAllowed () {
|
||||||
|
|
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -144,8 +144,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.little-information {
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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))
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 * 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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ]: [
|
||||||
|
|
|
@ -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 |
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue