Merge branch 'develop' of https://github.com/Chocobozzz/PeerTube into move-utils-to-shared

This commit is contained in:
buoyantair 2018-11-18 21:55:52 +05:30
commit b9f234371b
40 changed files with 727 additions and 131 deletions

View File

@ -124,6 +124,10 @@ function sortBy (obj: any[], key1: string, key2?: string) {
}) })
} }
function scrollToTop () {
window.scroll(0, 0)
}
export { export {
sortBy, sortBy,
durationToString, durationToString,
@ -135,5 +139,6 @@ export {
immutableAssign, immutableAssign,
objectToFormData, objectToFormData,
lineFeedToHtml, lineFeedToHtml,
removeElementFromArray removeElementFromArray,
scrollToTop
} }

View File

@ -4,6 +4,7 @@
Create an account Create an account
</div> </div>
<div *ngIf="info" class="alert alert-info">{{ info }}</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div> <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<div class="d-flex justify-content-left flex-wrap"> <div class="d-flex justify-content-left flex-wrap">
@ -59,7 +60,7 @@
</div> </div>
</div> </div>
<input type="submit" i18n-value value="Signup" [disabled]="!form.valid"> <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone">
</form> </form>
<div> <div>

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { UserCreate } from '../../../../shared' import { UserCreate } from '../../../../shared'
import { FormReactive, UserService, UserValidatorsService } from '../shared' import { FormReactive, UserService, UserValidatorsService } from '../shared'
import { RedirectService, ServerService } from '@app/core' import { AuthService, RedirectService, ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@ -12,10 +12,13 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
styleUrls: [ './signup.component.scss' ] styleUrls: [ './signup.component.scss' ]
}) })
export class SignupComponent extends FormReactive implements OnInit { export class SignupComponent extends FormReactive implements OnInit {
info: string = null
error: string = null error: string = null
signupDone = false
constructor ( constructor (
protected formValidatorService: FormValidatorService, protected formValidatorService: FormValidatorService,
private authService: AuthService,
private userValidatorsService: UserValidatorsService, private userValidatorsService: UserValidatorsService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private userService: UserService, private userService: UserService,
@ -50,20 +53,29 @@ export class SignupComponent extends FormReactive implements OnInit {
this.userService.signup(userCreate).subscribe( this.userService.signup(userCreate).subscribe(
() => { () => {
this.signupDone = true
if (this.requiresEmailVerification) { if (this.requiresEmailVerification) {
this.notificationsService.alert( this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.')
this.i18n('Welcome'), return
this.i18n('Please check your email to verify your account and complete signup.') }
)
} else { // Auto login
this.authService.login(userCreate.username, userCreate.password)
.subscribe(
() => {
this.notificationsService.success( this.notificationsService.success(
this.i18n('Success'), this.i18n('Success'),
this.i18n('Registration for {{username}} complete.', { username: userCreate.username }) this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })
) )
}
this.redirectService.redirectToHomepage() this.redirectService.redirectToHomepage()
}, },
err => this.error = err.message
)
},
err => this.error = err.message err => this.error = err.message
) )
} }

View File

@ -60,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
hide () { hide () {
this.closingModal = true this.closingModal = true
this.openedModal.close() this.openedModal.close()
this.form.reset()
} }
isReplacingExistingCaption () { isReplacingExistingCaption () {

View File

@ -45,7 +45,12 @@
</div> </div>
</div> </div>
<div *ngIf="hasImportedVideo" class="alert alert-info" i18n> <div *ngIf="error" class="alert alert-danger">
<div i18n>Sorry, but something went wrong</div>
{{ error }}
</div>
<div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n>
Congratulations, the video will be imported with BitTorrent! You can already add information about this video. Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
</div> </div>

View File

@ -7,6 +7,14 @@ $width-size: 190px;
@include peertube-select-container($width-size); @include peertube-select-container($width-size);
} }
.alert.alert-danger {
text-align: center;
& > div {
font-weight: $font-semibold;
}
}
.import-video-torrent { .import-video-torrent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
import { FormValidatorService } from '@app/shared' import { FormValidatorService } from '@app/shared'
import { VideoCaptionService } from '@app/shared/video-caption' import { VideoCaptionService } from '@app/shared/video-caption'
import { VideoImportService } from '@app/shared/video-import' import { VideoImportService } from '@app/shared/video-import'
import { scrollToTop } from '@app/shared/misc/utils'
@Component({ @Component({
selector: 'my-video-import-torrent', selector: 'my-video-import-torrent',
@ -23,9 +24,9 @@ import { VideoImportService } from '@app/shared/video-import'
}) })
export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
@Output() firstStepDone = new EventEmitter<string>() @Output() firstStepDone = new EventEmitter<string>()
@Output() firstStepError = new EventEmitter<void>()
@ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
videoFileName: string
magnetUri = '' magnetUri = ''
isImportingVideo = false isImportingVideo = false
@ -33,6 +34,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
isUpdatingVideo = false isUpdatingVideo = false
video: VideoEdit video: VideoEdit
error: string
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
@ -104,6 +106,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
err => { err => {
this.loadingBar.complete() this.loadingBar.complete()
this.isImportingVideo = false this.isImportingVideo = false
this.firstStepError.emit()
this.notificationsService.error(this.i18n('Error'), err.message) this.notificationsService.error(this.i18n('Error'), err.message)
} }
) )
@ -129,8 +132,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
}, },
err => { err => {
this.isUpdatingVideo = false this.error = err.message
this.notificationsService.error(this.i18n('Error'), err.message) scrollToTop()
console.error(err) console.error(err)
} }
) )

View File

@ -37,7 +37,13 @@
</div> </div>
</div> </div>
<div *ngIf="hasImportedVideo" class="alert alert-info" i18n>
<div *ngIf="error" class="alert alert-danger">
<div i18n>Sorry, but something went wrong</div>
{{ error }}
</div>
<div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n>
Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
</div> </div>

View File

@ -7,6 +7,14 @@ $width-size: 190px;
@include peertube-select-container($width-size); @include peertube-select-container($width-size);
} }
.alert.alert-danger {
text-align: center;
& > div {
font-weight: $font-semibold;
}
}
.import-video-url { .import-video-url {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
import { FormValidatorService } from '@app/shared' import { FormValidatorService } from '@app/shared'
import { VideoCaptionService } from '@app/shared/video-caption' import { VideoCaptionService } from '@app/shared/video-caption'
import { VideoImportService } from '@app/shared/video-import' import { VideoImportService } from '@app/shared/video-import'
import { scrollToTop } from '@app/shared/misc/utils'
@Component({ @Component({
selector: 'my-video-import-url', selector: 'my-video-import-url',
@ -23,15 +24,16 @@ import { VideoImportService } from '@app/shared/video-import'
}) })
export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
@Output() firstStepDone = new EventEmitter<string>() @Output() firstStepDone = new EventEmitter<string>()
@Output() firstStepError = new EventEmitter<void>()
targetUrl = '' targetUrl = ''
videoFileName: string
isImportingVideo = false isImportingVideo = false
hasImportedVideo = false hasImportedVideo = false
isUpdatingVideo = false isUpdatingVideo = false
video: VideoEdit video: VideoEdit
error: string
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
@ -96,6 +98,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
err => { err => {
this.loadingBar.complete() this.loadingBar.complete()
this.isImportingVideo = false this.isImportingVideo = false
this.firstStepError.emit()
this.notificationsService.error(this.i18n('Error'), err.message) this.notificationsService.error(this.i18n('Error'), err.message)
} }
) )
@ -121,8 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
}, },
err => { err => {
this.isUpdatingVideo = false this.error = err.message
this.notificationsService.error(this.i18n('Error'), err.message) scrollToTop()
console.error(err) console.error(err)
} }
) )

View File

@ -21,6 +21,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
firstStepChannelId = 0 firstStepChannelId = 0
abstract firstStepDone: EventEmitter<string> abstract firstStepDone: EventEmitter<string>
abstract firstStepError: EventEmitter<void>
protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
protected loadingBar: LoadingBarService protected loadingBar: LoadingBarService

View File

@ -29,7 +29,7 @@
</div> </div>
</div> </div>
<div *ngIf="isUploadingVideo" class="upload-progress-cancel"> <div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
<p-progressBar <p-progressBar
[value]="videoUploadPercents" [value]="videoUploadPercents"
[ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }"
@ -37,6 +37,11 @@
<input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" />
</div> </div>
<div *ngIf="error" class="alert alert-danger">
<div i18n>Sorry, but something went wrong</div>
{{ error }}
</div>
<!-- Hidden because we want to load the component --> <!-- Hidden because we want to load the component -->
<form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form">
<my-video-edit <my-video-edit

View File

@ -5,6 +5,14 @@
@include peertube-select-container(190px); @include peertube-select-container(190px);
} }
.alert.alert-danger {
text-align: center;
& > div {
font-weight: $font-semibold;
}
}
.upload-video { .upload-video {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -14,6 +14,7 @@ import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-se
import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
import { FormValidatorService, UserService } from '@app/shared' import { FormValidatorService, UserService } from '@app/shared'
import { VideoCaptionService } from '@app/shared/video-caption' import { VideoCaptionService } from '@app/shared/video-caption'
import { scrollToTop } from '@app/shared/misc/utils'
@Component({ @Component({
selector: 'my-video-upload', selector: 'my-video-upload',
@ -25,6 +26,7 @@ import { VideoCaptionService } from '@app/shared/video-caption'
}) })
export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
@Output() firstStepDone = new EventEmitter<string>() @Output() firstStepDone = new EventEmitter<string>()
@Output() firstStepError = new EventEmitter<void>()
@ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
// So that it can be accessed in the template // So that it can be accessed in the template
@ -43,6 +45,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
uuid: '' uuid: ''
} }
error: string
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
constructor ( constructor (
@ -201,6 +205,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
this.isUploadingVideo = false this.isUploadingVideo = false
this.videoUploadPercents = 0 this.videoUploadPercents = 0
this.videoUploadObservable = null this.videoUploadObservable = null
this.firstStepError.emit()
this.notificationsService.error(this.i18n('Error'), err.message) this.notificationsService.error(this.i18n('Error'), err.message)
} }
) )
@ -235,8 +240,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
}, },
err => { err => {
this.isUpdatingVideo = false this.error = err.message
this.notificationsService.error(this.i18n('Error'), err.message) scrollToTop()
console.error(err) console.error(err)
} }
) )

View File

@ -6,24 +6,24 @@
<ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
<ngb-tab i18n-title title=""> <ngb-tab>
<ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template> <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template>
<ng-template ngbTabContent> <ng-template ngbTabContent>
<my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)"></my-video-upload> <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload>
</ng-template> </ng-template>
</ngb-tab> </ngb-tab>
<ngb-tab *ngIf="isVideoImportHttpEnabled()"> <ngb-tab *ngIf="isVideoImportHttpEnabled()">
<ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template> <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template>
<ng-template ngbTabContent> <ng-template ngbTabContent>
<my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)"></my-video-import-url> <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url>
</ng-template> </ng-template>
</ngb-tab> </ngb-tab>
<ngb-tab *ngIf="isVideoImportTorrentEnabled()"> <ngb-tab *ngIf="isVideoImportTorrentEnabled()">
<ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template> <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template>
<ng-template ngbTabContent> <ng-template ngbTabContent>
<my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)"></my-video-import-torrent> <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent>
</ng-template> </ng-template>
</ngb-tab> </ngb-tab>
</ngb-tabset> </ngb-tabset>

View File

@ -27,6 +27,11 @@ export class VideoAddComponent implements CanComponentDeactivate {
this.videoName = videoName this.videoName = videoName
} }
onError () {
this.videoName = undefined
this.secondStepType = undefined
}
canDeactivate () { canDeactivate () {
if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()

View File

@ -114,7 +114,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
) )
.pipe( .pipe(
// If 401, the video is private or blacklisted so redirect to 404 // If 401, the video is private or blacklisted so redirect to 404
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ])) catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ]))
) )
.subscribe(([ video, captionsResult ]) => { .subscribe(([ video, captionsResult ]) => {
const startTime = this.route.snapshot.queryParams.start const startTime = this.route.snapshot.queryParams.start

View File

Before

Width:  |  Height:  |  Size: 738 B

After

Width:  |  Height:  |  Size: 738 B

View File

@ -111,6 +111,8 @@ class PeerTubePlugin extends Plugin {
const muted = getStoredMute() const muted = getStoredMute()
if (muted !== undefined) this.player.muted(muted) if (muted !== undefined) this.player.muted(muted)
this.player.duration(options.videoDuration)
this.initializePlayer() this.initializePlayer()
this.runTorrentInfoScheduler() this.runTorrentInfoScheduler()
this.runViewAdd() this.runViewAdd()
@ -302,6 +304,9 @@ class PeerTubePlugin extends Plugin {
this.flushVideoFile(previousVideoFile) this.flushVideoFile(previousVideoFile)
// Update progress bar (just for the UI), do not wait rendering
if (options.seek) this.player.currentTime(options.seek)
const renderVideoOptions = { autoplay: false, controls: true } const renderVideoOptions = { autoplay: false, controls: true }
renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => { renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
this.renderer = renderer this.renderer = renderer

View File

@ -171,7 +171,7 @@ $setting-transition-easing: ease-out;
left: 8px; left: 8px;
content: ' '; content: ' ';
margin-top: 1px; margin-top: 1px;
background-image: url('#{$assets-path}/player/images/tick.svg'); background-image: url('#{$assets-path}/player/images/tick-white.svg');
} }
} }
} }

View File

@ -58,7 +58,10 @@ log:
level: 'info' # debug/info/warning/error level: 'info' # debug/info/warning/error
search: search:
remote_uri: # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
# If enabled, the associated group will be able to "escape" from the instance follows
# That means they will be able to follow channels, watch videos, list videos of non followed instances
remote_uri:
users: true users: true
anonymous: false anonymous: false

View File

@ -59,7 +59,10 @@ log:
level: 'info' # debug/info/warning/error level: 'info' # debug/info/warning/error
search: search:
remote_uri: # Add ability to search remote videos/actors by URI, that may not be federated with your instance # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
# If enabled, the associated group will be able to "escape" from the instance follows
# That means they will be able to follow channels, watch videos, list videos of non followed instances
remote_uri:
users: true users: true
anonymous: false anonymous: false

View File

@ -39,6 +39,7 @@ import {
import { VideoCaptionModel } from '../../models/video/video-caption' import { VideoCaptionModel } from '../../models/video/video-caption'
import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy'
import { getServerActor } from '../../helpers/utils' import { getServerActor } from '../../helpers/utils'
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
const activityPubClientRouter = express.Router() const activityPubClientRouter = express.Router()
@ -164,6 +165,8 @@ function getAccountVideoRate (rateType: VideoRateType) {
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video const video: VideoModel = res.locals.video
if (video.isOwned() === false) return res.redirect(video.url)
// We need captions to render AP object // We need captions to render AP object
video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
@ -180,6 +183,9 @@ async function videoController (req: express.Request, res: express.Response, nex
async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) {
const share = res.locals.videoShare as VideoShareModel const share = res.locals.videoShare as VideoShareModel
if (share.Actor.isOwned() === false) return res.redirect(share.url)
const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
return activityPubResponse(activityPubContextify(activity), res) return activityPubResponse(activityPubContextify(activity), res)
@ -252,6 +258,8 @@ async function videoChannelFollowingController (req: express.Request, res: expre
async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) { async function videoCommentController (req: express.Request, res: express.Response, next: express.NextFunction) {
const videoComment: VideoCommentModel = res.locals.videoComment const videoComment: VideoCommentModel = res.locals.videoComment
if (videoComment.isOwned() === false) return res.redirect(videoComment.url)
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
const isPublic = true // Comments are always public const isPublic = true // Comments are always public
const audience = getAudience(videoComment.Account.Actor, isPublic) const audience = getAudience(videoComment.Account.Actor, isPublic)
@ -267,7 +275,9 @@ async function videoCommentController (req: express.Request, res: express.Respon
} }
async function videoRedundancyController (req: express.Request, res: express.Response) { async function videoRedundancyController (req: express.Request, res: express.Response) {
const videoRedundancy = res.locals.videoRedundancy const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy
if (videoRedundancy.isOwned() === false) return res.redirect(videoRedundancy.url)
const serverActor = await getServerActor() const serverActor = await getServerActor()
const audience = getAudience(serverActor) const audience = getAudience(serverActor)
@ -288,7 +298,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
} }
return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
} }
async function actorFollowers (req: express.Request, actor: ActorModel) { async function actorFollowers (req: express.Request, actor: ActorModel) {
@ -296,7 +306,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count) return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count)
} }
return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, handler, req.query.page) return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page)
} }
function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {

View File

@ -31,6 +31,7 @@ import {
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
checkVideoFollowConstraints,
commonVideosFiltersValidator, commonVideosFiltersValidator,
optionalAuthenticate, optionalAuthenticate,
paginationValidator, paginationValidator,
@ -123,6 +124,7 @@ videosRouter.get('/:id/description',
videosRouter.get('/:id', videosRouter.get('/:id',
optionalAuthenticate, optionalAuthenticate,
asyncMiddleware(videosGetValidator), asyncMiddleware(videosGetValidator),
asyncMiddleware(checkVideoFollowConstraints),
getVideo getVideo
) )
videosRouter.post('/:id/views', videosRouter.post('/:id/views',

View File

@ -57,16 +57,16 @@ function activityPubContextify <T> (data: T) {
} }
type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
if (!page || !validator.isInt(page)) { if (!page || !validator.isInt(page)) {
// We just display the first page URL, we only need the total items // We just display the first page URL, we only need the total items
const result = await handler(0, 1) const result = await handler(0, 1)
return { return {
id: url, id: baseUrl,
type: 'OrderedCollection', type: 'OrderedCollection',
totalItems: result.total, totalItems: result.total,
first: url + '?page=1' first: baseUrl + '?page=1'
} }
} }
@ -81,19 +81,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
// There are more results // There are more results
if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
next = url + '?page=' + (page + 1) next = baseUrl + '?page=' + (page + 1)
} }
if (page > 1) { if (page > 1) {
prev = url + '?page=' + (page - 1) prev = baseUrl + '?page=' + (page - 1)
} }
return { return {
id: url + '?page=' + page, id: baseUrl + '?page=' + page,
type: 'OrderedCollectionPage', type: 'OrderedCollectionPage',
prev, prev,
next, next,
partOf: url, partOf: baseUrl,
orderedItems: result.data, orderedItems: result.data,
totalItems: result.total totalItems: result.total
} }

View File

@ -2,6 +2,7 @@ import * as Bluebird from 'bluebird'
import { createWriteStream } from 'fs-extra' import { createWriteStream } from 'fs-extra'
import * as request from 'request' import * as request from 'request'
import { ACTIVITY_PUB } from '../initializers' import { ACTIVITY_PUB } from '../initializers'
import { processImage } from './image-utils'
function doRequest <T> ( function doRequest <T> (
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
@ -27,9 +28,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
}) })
} }
async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) {
const tmpPath = destPath + '.tmp'
await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
await processImage({ path: tmpPath }, destPath, size)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
doRequest, doRequest,
doRequestAndSaveToFile doRequestAndSaveToFile,
downloadImage
} }

View File

@ -11,9 +11,9 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' import { retryTransactionWrapper, updateInstanceWithAnother } 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, doRequestAndSaveToFile } from '../../helpers/requests' import { doRequest, doRequestAndSaveToFile, downloadImage } from '../../helpers/requests'
import { getUrlFromWebfinger } from '../../helpers/webfinger' import { getUrlFromWebfinger } from '../../helpers/webfinger'
import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, PREVIEWS_SIZE, 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 { AvatarModel } from '../../models/avatar/avatar'
@ -180,10 +180,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
const avatarName = uuidv4() + extension const avatarName = uuidv4() + extension
const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
await doRequestAndSaveToFile({ await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
method: 'GET',
uri: actorJSON.icon.url
}, destPath)
return avatarName return avatarName
} }

View File

@ -10,8 +10,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' import { doRequest, downloadImage } from '../../helpers/requests'
import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers'
import { ActorModel } from '../../models/activitypub/actor' import { ActorModel } from '../../models/activitypub/actor'
import { TagModel } from '../../models/video/tag' import { TagModel } from '../../models/video/tag'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
@ -97,11 +97,7 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
const thumbnailName = video.getThumbnailName() const thumbnailName = video.getThumbnailName()
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
const options = { return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE)
method: 'GET',
uri: icon.url
}
return doRequestAndSaveToFile(options, thumbnailPath)
} }
function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {

View File

@ -6,8 +6,8 @@ import { VideoImportState } from '../../../../shared/models/videos'
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
import { extname, join } from 'path' import { extname, join } from 'path'
import { VideoFileModel } from '../../../models/video/video-file' import { VideoFileModel } from '../../../models/video/video-file'
import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
import { doRequestAndSaveToFile } from '../../../helpers/requests' import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests'
import { VideoState } from '../../../../shared' import { VideoState } from '../../../../shared'
import { JobQueue } from '../index' import { JobQueue } from '../index'
import { federateVideoIfNeeded } from '../../activitypub' import { federateVideoIfNeeded } from '../../activitypub'
@ -133,7 +133,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
videoId: videoImport.videoId videoId: videoImport.videoId
} }
videoFile = new VideoFileModel(videoFileData) videoFile = new VideoFileModel(videoFileData)
// Import if the import fails, to clean files // To clean files if the import fails
videoImport.Video.VideoFiles = [ videoFile ] videoImport.Video.VideoFiles = [ videoFile ]
// Move file // Move file
@ -145,7 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
if (options.downloadThumbnail) { if (options.downloadThumbnail) {
if (options.thumbnailUrl) { if (options.thumbnailUrl) {
const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName())
await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destThumbnailPath) await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE)
} else { } else {
await videoImport.Video.createThumbnail(videoFile) await videoImport.Video.createThumbnail(videoFile)
} }
@ -157,7 +157,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
if (options.downloadPreview) { if (options.downloadPreview) {
if (options.thumbnailUrl) { if (options.thumbnailUrl) {
const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName())
await doRequestAndSaveToFile({ method: 'GET', uri: options.thumbnailUrl }, destPreviewPath) await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE)
} else { } else {
await videoImport.Video.createPreview(videoFile) await videoImport.Video.createPreview(videoFile)
} }

View File

@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) {
logger.debug('No cached results for route %s.', req.originalUrl) logger.debug('No cached results for route %s.', req.originalUrl)
const sendSave = res.send.bind(res) const sendSave = res.send.bind(res)
const redirectSave = res.redirect.bind(res)
res.send = (body) => { res.send = (body) => {
if (res.statusCode >= 200 && res.statusCode < 400) { if (res.statusCode >= 200 && res.statusCode < 400) {
@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) {
return sendSave(body) return sendSave(body)
} }
res.redirect = url => {
done()
return redirectSave(url)
}
return next() return next()
} }

View File

@ -28,9 +28,24 @@ function authenticate (req: express.Request, res: express.Response, next: expres
}) })
} }
function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) {
return new Promise(resolve => {
// Already authenticated? (or tried to)
if (res.locals.oauth && res.locals.oauth.token.User) return resolve()
if (res.locals.authenticated === false) return res.sendStatus(401)
authenticate(req, res, () => {
return resolve()
})
})
}
function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
if (req.header('authorization')) return authenticate(req, res, next) if (req.header('authorization')) return authenticate(req, res, next)
res.locals.authenticated = false
return next() return next()
} }
@ -53,6 +68,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF
export { export {
authenticate, authenticate,
authenticatePromiseIfNeeded,
optionalAuthenticate, optionalAuthenticate,
token token
} }

View File

@ -31,8 +31,8 @@ import {
} from '../../../helpers/custom-validators/videos' } from '../../../helpers/custom-validators/videos'
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { CONSTRAINTS_FIELDS } from '../../../initializers' import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
import { authenticate } from '../../oauth' import { authenticatePromiseIfNeeded } from '../../oauth'
import { areValidationErrors } from '../utils' import { areValidationErrors } from '../utils'
import { cleanUpReqFiles } from '../../../helpers/express-utils' import { cleanUpReqFiles } from '../../../helpers/express-utils'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
@ -43,6 +43,7 @@ import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ow
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { VideoFetchType } from '../../../helpers/video' import { VideoFetchType } from '../../../helpers/video'
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
import { getServerActor } from '../../../helpers/utils'
const videosAddValidator = getCommonVideoAttributes().concat([ const videosAddValidator = getCommonVideoAttributes().concat([
body('videofile') body('videofile')
@ -127,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
} }
]) ])
async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video
// Anybody can watch local videos
if (video.isOwned() === true) return next()
// Logged user
if (res.locals.oauth) {
// Users can search or watch remote videos
if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
}
// Anybody can search or watch remote videos
if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
// Check our instance follows an actor that shared this video
const serverActor = await getServerActor()
if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
return res.status(403)
.json({
error: 'Cannot get this video regarding follow constraints.'
})
}
const videosCustomGetValidator = (fetchType: VideoFetchType) => { const videosCustomGetValidator = (fetchType: VideoFetchType) => {
return [ return [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@ -141,17 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
// Video private or blacklisted // Video private or blacklisted
if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
return authenticate(req, res, () => { await authenticatePromiseIfNeeded(req, res)
const user: UserModel = res.locals.oauth.token.User
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
// Only the owner or a user that have blacklist rights can see the video // Only the owner or a user that have blacklist rights can see the video
if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { if (
!user ||
(video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
) {
return res.status(403) return res.status(403)
.json({ error: 'Cannot get this private or blacklisted video.' }) .json({ error: 'Cannot get this private or blacklisted video.' })
} }
return next() return next()
})
} }
// Video is public, anyone can access it // Video is public, anyone can access it
@ -376,6 +405,7 @@ export {
videosAddValidator, videosAddValidator,
videosUpdateValidator, videosUpdateValidator,
videosGetValidator, videosGetValidator,
checkVideoFollowConstraints,
videosCustomGetValidator, videosCustomGetValidator,
videosRemoveValidator, videosRemoveValidator,
@ -393,6 +423,8 @@ export {
function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
if (req.body.scheduleUpdate) { if (req.body.scheduleUpdate) {
if (!req.body.scheduleUpdate.updateAt) { if (!req.body.scheduleUpdate.updateAt) {
logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
res.status(400) res.status(400)
.json({ error: 'Schedule update at is mandatory.' }) .json({ error: 'Schedule update at is mandatory.' })

View File

@ -509,12 +509,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
tasks.push(ActorFollowModel.sequelize.query(query, options)) tasks.push(ActorFollowModel.sequelize.query(query, options))
} }
const [ followers, [ { total } ] ] = await Promise.all(tasks) const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
const urls: string[] = followers.map(f => f.url) const urls: string[] = followers.map(f => f.url)
return { return {
data: urls, data: urls,
total: parseInt(total, 10) total: dataTotal ? parseInt(dataTotal.total, 10) : 0
} }
} }

View File

@ -117,8 +117,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
@BeforeDestroy @BeforeDestroy
static async removeFile (instance: VideoRedundancyModel) { static async removeFile (instance: VideoRedundancyModel) {
// Not us if (!instance.isOwned()) return
if (!instance.strategy) return
const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
@ -404,6 +403,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
})) }))
} }
isOwned () {
return !!this.strategy
}
toActivityPubObject (): CacheFileObject { toActivityPubObject (): CacheFileObject {
return { return {
id: this.url, id: this.url,

View File

@ -1253,6 +1253,23 @@ export class VideoModel extends Model<VideoModel> {
}) })
} }
static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
// Instances only share videos
const query = 'SELECT 1 FROM "videoShare" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
'LIMIT 1'
const options = {
type: Sequelize.QueryTypes.SELECT,
bind: { followerActorId, videoId },
raw: true
}
return VideoModel.sequelize.query(query, options)
.then(results => results.length === 1)
}
// threshold corresponds to how many video the field should have to be returned // threshold corresponds to how many video the field should have to be returned
static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
const serverActor = await getServerActor() const serverActor = await getServerActor()

View File

@ -14,11 +14,13 @@ import {
setAccessTokensToServers, setAccessTokensToServers,
userLogin userLogin
} from '../../../../shared/utils' } from '../../../../shared/utils'
import { import {
checkBadCountPagination, checkBadCountPagination,
checkBadSortPagination, checkBadSortPagination,
checkBadStartPagination checkBadStartPagination
} from '../../../../shared/utils/requests/check-api-params' } from '../../../../shared/utils/requests/check-api-params'
import { waitJobs } from '../../../../shared/utils/server/jobs'
describe('Test user subscriptions API validators', function () { describe('Test user subscriptions API validators', function () {
const path = '/api/v1/users/me/subscriptions' const path = '/api/v1/users/me/subscriptions'
@ -145,6 +147,8 @@ describe('Test user subscriptions API validators', function () {
}) })
it('Should succeed with the correct parameters', async function () { it('Should succeed with the correct parameters', async function () {
this.timeout(20000)
await makePostBodyRequest({ await makePostBodyRequest({
url: server.url, url: server.url,
path, path,
@ -152,6 +156,8 @@ describe('Test user subscriptions API validators', function () {
fields: { uri: 'user1_channel@localhost:9001' }, fields: { uri: 'user1_channel@localhost:9001' },
statusCodeExpected: 204 statusCodeExpected: 204
}) })
await waitJobs([ server ])
}) })
}) })

View File

@ -17,9 +17,10 @@ import {
viewVideo, viewVideo,
wait, wait,
waitUntilLog, waitUntilLog,
checkVideoFilesWereRemoved, removeVideo checkVideoFilesWereRemoved, removeVideo, getVideoWithToken
} from '../../../../shared/utils' } from '../../../../shared/utils'
import { waitJobs } from '../../../../shared/utils/server/jobs' import { waitJobs } from '../../../../shared/utils/server/jobs'
import * as magnetUtil from 'magnet-uri' import * as magnetUtil from 'magnet-uri'
import { updateRedundancy } from '../../../../shared/utils/server/redundancy' import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
import { ActorFollow } from '../../../../shared/models/actors' import { ActorFollow } from '../../../../shared/models/actors'
@ -93,7 +94,8 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str
for (const server of servers) { for (const server of servers) {
{ {
const res = await getVideo(server.url, videoUUID) // With token to avoid issues with video follow constraints
const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
const video: VideoDetails = res.body const video: VideoDetails = res.body
for (const f of video.files) { for (const f of video.files) {

View File

@ -0,0 +1,215 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import { doubleFollow, getAccountVideos, getVideo, getVideoChannelVideos, getVideoWithToken } from '../../utils'
import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index'
import { unfollow } from '../../utils/server/follows'
import { userLogin } from '../../utils/users/login'
import { createUser } from '../../utils/users/users'
const expect = chai.expect
describe('Test follow constraints', function () {
let servers: ServerInfo[] = []
let video1UUID: string
let video2UUID: string
let userAccessToken: string
before(async function () {
this.timeout(30000)
servers = await flushAndRunMultipleServers(2)
// Get the access tokens
await setAccessTokensToServers(servers)
{
const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video server 1' })
video1UUID = res.body.video.uuid
}
{
const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video server 2' })
video2UUID = res.body.video.uuid
}
const user = {
username: 'user1',
password: 'super_password'
}
await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
userAccessToken = await userLogin(servers[0], user)
await doubleFollow(servers[0], servers[1])
})
describe('With a followed instance', function () {
describe('With an unlogged user', function () {
it('Should get the local video', async function () {
await getVideo(servers[0].url, video1UUID, 200)
})
it('Should get the remote video', async function () {
await getVideo(servers[0].url, video2UUID, 200)
})
it('Should list local account videos', async function () {
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list remote account videos', async function () {
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list local channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list remote channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
})
describe('With a logged user', function () {
it('Should get the local video', async function () {
await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
})
it('Should get the remote video', async function () {
await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
})
it('Should list local account videos', async function () {
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list remote account videos', async function () {
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list local channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list remote channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
})
})
describe('With a non followed instance', function () {
before(async function () {
this.timeout(30000)
await unfollow(servers[0].url, servers[0].accessToken, servers[1])
})
describe('With an unlogged user', function () {
it('Should get the local video', async function () {
await getVideo(servers[0].url, video1UUID, 200)
})
it('Should not get the remote video', async function () {
await getVideo(servers[0].url, video2UUID, 403)
})
it('Should list local account videos', async function () {
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should not list remote account videos', async function () {
const res = await getAccountVideos(servers[0].url, undefined, 'root@localhost:9002', 0, 5)
expect(res.body.total).to.equal(0)
expect(res.body.data).to.have.lengthOf(0)
})
it('Should list local channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should not list remote channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, undefined, 'root_channel@localhost:9002', 0, 5)
expect(res.body.total).to.equal(0)
expect(res.body.data).to.have.lengthOf(0)
})
})
describe('With a logged user', function () {
it('Should get the local video', async function () {
await getVideoWithToken(servers[0].url, userAccessToken, video1UUID, 200)
})
it('Should get the remote video', async function () {
await getVideoWithToken(servers[0].url, userAccessToken, video2UUID, 200)
})
it('Should list local account videos', async function () {
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list remote account videos', async function () {
const res = await getAccountVideos(servers[0].url, userAccessToken, 'root@localhost:9002', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list local channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9001', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
it('Should list remote channel videos', async function () {
const res = await getVideoChannelVideos(servers[0].url, userAccessToken, 'root_channel@localhost:9002', 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
})
})
})
after(async function () {
killallServers(servers)
})
})

View File

@ -1,5 +1,6 @@
import './config' import './config'
import './email' import './email'
import './follow-constraints'
import './follows' import './follows'
import './handle-down' import './handle-down'
import './jobs' import './jobs'

View File

@ -10,27 +10,41 @@ info:
url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE' url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
x-logo: x-logo:
url: 'https://joinpeertube.org/img/brand.png' url: 'https://joinpeertube.org/img/brand.png'
altText: PeerTube Project Homepage
description: | description: |
# Introduction # Introduction
The PeerTube API is built on HTTP(S). Our API is RESTful. It has predictable The PeerTube API is built on HTTP(S). Our API is RESTful. It has predictable
resource URLs. It returns HTTP response codes to indicate errors. It also resource URLs. It returns HTTP response codes to indicate errors. It also
accepts and returns JSON in the HTTP body. You can use your favorite accepts and returns JSON in the HTTP body. You can use your favorite
HTTP/REST library for your programming language to use PeerTube. No official HTTP/REST library for your programming language to use PeerTube. No official
SDK is currently provided. SDK is currently provided, but the spec API is fully compatible with
[openapi-generator](https://github.com/OpenAPITools/openapi-generator/wiki/API-client-generator-HOWTO)
which generates a client SDK in the language of your choice.
# Authentication # Authentication
When you sign up for an account, you are given the possibility to generate When you sign up for an account, you are given the possibility to generate
sessions, and authenticate using this session token. One session token can sessions, and authenticate using this session token. One session token can
currently be used at a time. currently be used at a time.
# Errors
The API uses standard HTTP status codes to indicate the success or failure
of the API call. The body of the response will be JSON in the following
format.
```
{
"code": "unauthorized_request", // example inner error code
"error": "Token is invalid." // example exposed error message
}
```
externalDocs:
url: https://docs.joinpeertube.org/api.html
tags: tags:
- name: Accounts - name: Accounts
description: > description: >
Using some features of PeerTube require authentication, for which Accounts Using some features of PeerTube require authentication, for which Accounts
provide different levels of permission as well as associated user provide different levels of permission as well as associated user
information. information. Accounts also encompass remote accounts discovered across the federation.
Accounts also encompass remote accounts discovered across the federation.
- name: Config - name: Config
description: > description: >
Each server exposes public information regarding supported videos and Each server exposes public information regarding supported videos and
@ -42,23 +56,15 @@ tags:
- name: Job - name: Job
description: > description: >
Jobs are long-running tasks enqueued and processed by the instance Jobs are long-running tasks enqueued and processed by the instance
itself. itself. No additional worker registration is currently available.
- name: Server Following
No additional worker registration is currently available.
- name: ServerFollowing
description: > description: >
Managing servers which the instance interacts with is crucial to the Managing servers which the instance interacts with is crucial to the
concept concept of federation in PeerTube and external video indexation. The PeerTube
server then deals with inter-server ActivityPub operations and propagates
of federation in PeerTube and external video indexation. The PeerTube
server
then deals with inter-server ActivityPub operations and propagates
information across its social graph by posting activities to actors' inbox information across its social graph by posting activities to actors' inbox
endpoints. endpoints.
- name: VideoAbuse - name: Video Abuse
description: | description: |
Video abuses deal with reports of local or remote videos alike. Video abuses deal with reports of local or remote videos alike.
- name: Video - name: Video
@ -70,16 +76,51 @@ tags:
Videos from other instances federated by the instance (that is, instances Videos from other instances federated by the instance (that is, instances
followed by the instance) can be found via keywords and other criteria of followed by the instance) can be found via keywords and other criteria of
the advanced search. the advanced search.
- name: VideoComment - name: Video Comment
description: > description: >
Operations dealing with comments to a video. Comments are organized in Operations dealing with comments to a video. Comments are organized in
threads. threads.
- name: VideoChannel - name: Video Channel
description: > description: >
Operations dealing with creation, modification and video listing of a Operations dealing with creation, modification and video listing of a
user's user's channels.
- name: Video Blacklist
channels. description: >
Operations dealing with blacklisting videos (removing them from view and
preventing interactions).
- name: Video Rate
description: >
Voting for a video.
x-tagGroups:
- name: Accounts
tags:
- Accounts
- User
- name: Videos
tags:
- Video
- Video Channel
- Video Comment
- Video Abuse
- Video Following
- Video Rate
- name: Moderation
tags:
- Video Abuse
- Video Blacklist
- name: Public Instance Information
tags:
- Config
- Server Following
- name: Notifications
tags:
- Feeds
- name: Jobs
tags:
- Job
- name: Search
tags:
- Search
paths: paths:
'/accounts/{name}': '/accounts/{name}':
get: get:
@ -126,6 +167,37 @@ paths:
source: | source: |
# pip install httpie # pip install httpie
http -b GET https://peertube2.cpy.re/api/v1/accounts/{name}/videos http -b GET https://peertube2.cpy.re/api/v1/accounts/{name}/videos
- lang: Ruby
source: |
require 'uri'
require 'net/http'
url = URI("https://peertube2.cpy.re/api/v1/accounts/{name}/videos")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(url)
request["content-type"] = 'application/json'
response = http.request(request)
puts response.read_body
- lang: Python
source: |
import http.client
conn = http.client.HTTPSConnection("https://peertube2.cpy.re/api/v1")
headers = {
'content-type': "application/json"
}
conn.request("POST", "/accounts/{name}/videos", None, headers)
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
/accounts: /accounts:
get: get:
tags: tags:
@ -144,7 +216,7 @@ paths:
get: get:
tags: tags:
- Config - Config
summary: Get the configuration of the server summary: Get the public configuration of the server
responses: responses:
'200': '200':
description: successful operation description: successful operation
@ -152,6 +224,45 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ServerConfig' $ref: '#/components/schemas/ServerConfig'
/config/about:
get:
summary: Get the instance about page content
tags:
- Config
responses:
'200':
description: successful operation
/config/custom:
get:
summary: Get the runtime configuration of the server
tags:
- Config
security:
- OAuth2:
- admin
responses:
'200':
description: successful operation
put:
summary: Set the runtime configuration of the server
tags:
- Config
security:
- OAuth2:
- admin
responses:
'200':
description: successful operation
delete:
summary: Delete the runtime configuration of the server
tags:
- Config
security:
- OAuth2:
- admin
responses:
'200':
description: successful operation
'/feeds/videos.{format}': '/feeds/videos.{format}':
get: get:
summary: >- summary: >-
@ -223,7 +334,7 @@ paths:
- OAuth2: - OAuth2:
- admin - admin
tags: tags:
- ServerFollowing - Server Following
summary: Unfollow a server by hostname summary: Unfollow a server by hostname
parameters: parameters:
- name: host - name: host
@ -238,7 +349,7 @@ paths:
/server/followers: /server/followers:
get: get:
tags: tags:
- ServerFollowing - Server Following
summary: Get followers of the server summary: Get followers of the server
parameters: parameters:
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
@ -256,7 +367,7 @@ paths:
/server/following: /server/following:
get: get:
tags: tags:
- ServerFollowing - Server Following
summary: Get servers followed by the server summary: Get servers followed by the server
parameters: parameters:
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
@ -276,7 +387,7 @@ paths:
- OAuth2: - OAuth2:
- admin - admin
tags: tags:
- ServerFollowing - Server Following
summary: Follow a server summary: Follow a server
responses: responses:
'204': '204':
@ -701,6 +812,85 @@ paths:
responses: responses:
'204': '204':
$ref: '#/paths/~1users~1me/put/responses/204' $ref: '#/paths/~1users~1me/put/responses/204'
'/videos/{id}/watching':
put:
summary: Indicate progress of in watching the video by its id for a user
tags:
- Video
security:
- OAuth2: []
parameters:
- $ref: '#/components/parameters/id2'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserWatchingVideo'
required: true
responses:
'204':
$ref: '#/paths/~1users~1me/put/responses/204'
/videos/ownership:
get:
summary: Get list of video ownership changes requests
tags:
- Video
security:
- OAuth2: []
parameters:
- $ref: '#/components/parameters/id2'
responses:
'200':
description: successful operation
'/videos/ownership/{id}/accept':
post:
summary: Refuse ownership change request for video by its id
tags:
- Video
security:
- OAuth2: []
parameters:
- $ref: '#/components/parameters/id2'
responses:
'204':
$ref: '#/paths/~1users~1me/put/responses/204'
'/videos/ownership/{id}/refuse':
post:
summary: Accept ownership change request for video by its id
tags:
- Video
security:
- OAuth2: []
parameters:
- $ref: '#/components/parameters/id2'
responses:
'204':
$ref: '#/paths/~1users~1me/put/responses/204'
'/videos/{id}/give-ownership':
post:
summary: Request change of ownership for a video you own, by its id
tags:
- Video
security:
- OAuth2: []
parameters:
- $ref: '#/components/parameters/id2'
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
username:
type: string
required:
- username
responses:
'204':
$ref: '#/paths/~1users~1me/put/responses/204'
'400':
description: 'Changing video ownership to a remote account is not supported yet'
/videos/upload: /videos/upload:
post: post:
summary: Upload a video file with its metadata summary: Upload a video file with its metadata
@ -771,7 +961,6 @@ paths:
- videofile - videofile
- channelId - channelId
- name - name
- privacy
x-code-samples: x-code-samples:
- lang: Shell - lang: Shell
source: | source: |
@ -781,7 +970,6 @@ paths:
PASSWORD="<your_password>" PASSWORD="<your_password>"
FILE_PATH="<your_file_path>" FILE_PATH="<your_file_path>"
CHANNEL_ID="<your_channel_id>" CHANNEL_ID="<your_channel_id>"
PRIVACY="1" # public: 1, unlisted: 2, private: 3
NAME="<video_name>" NAME="<video_name>"
API_PATH="https://peertube2.cpy.re/api/v1" API_PATH="https://peertube2.cpy.re/api/v1"
@ -798,7 +986,6 @@ paths:
videofile@$FILE_PATH \ videofile@$FILE_PATH \
channelId=$CHANNEL_ID \ channelId=$CHANNEL_ID \
name=$NAME \ name=$NAME \
privacy=$PRIVACY \
"Authorization:Bearer $token" "Authorization:Bearer $token"
/videos/abuse: /videos/abuse:
get: get:
@ -806,7 +993,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoAbuse - Video Abuse
parameters: parameters:
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -826,7 +1013,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoAbuse - Video Abuse
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
responses: responses:
@ -840,7 +1027,7 @@ paths:
- admin - admin
- moderator - moderator
tags: tags:
- VideoBlacklist - Video Blacklist
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
responses: responses:
@ -853,7 +1040,7 @@ paths:
- admin - admin
- moderator - moderator
tags: tags:
- VideoBlacklist - Video Blacklist
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
responses: responses:
@ -867,7 +1054,7 @@ paths:
- admin - admin
- moderator - moderator
tags: tags:
- VideoBlacklist - Video Blacklist
parameters: parameters:
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -885,7 +1072,7 @@ paths:
get: get:
summary: Get list of video channels summary: Get list of video channels
tags: tags:
- VideoChannel - Video Channel
parameters: parameters:
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
@ -904,7 +1091,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoChannel - Video Channel
responses: responses:
'204': '204':
$ref: '#/paths/~1users~1me/put/responses/204' $ref: '#/paths/~1users~1me/put/responses/204'
@ -914,7 +1101,7 @@ paths:
get: get:
summary: Get a video channel by its id summary: Get a video channel by its id
tags: tags:
- VideoChannel - Video Channel
parameters: parameters:
- $ref: '#/components/parameters/id3' - $ref: '#/components/parameters/id3'
responses: responses:
@ -929,7 +1116,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoChannel - Video Channel
parameters: parameters:
- $ref: '#/components/parameters/id3' - $ref: '#/components/parameters/id3'
responses: responses:
@ -942,7 +1129,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoChannel - Video Channel
parameters: parameters:
- $ref: '#/components/parameters/id3' - $ref: '#/components/parameters/id3'
responses: responses:
@ -952,7 +1139,7 @@ paths:
get: get:
summary: Get videos of a video channel by its id summary: Get videos of a video channel by its id
tags: tags:
- VideoChannel - Video Channel
parameters: parameters:
- $ref: '#/components/parameters/id3' - $ref: '#/components/parameters/id3'
responses: responses:
@ -966,7 +1153,7 @@ paths:
get: get:
summary: Get video channels of an account by its name summary: Get video channels of an account by its name
tags: tags:
- VideoChannel - Video Channel
parameters: parameters:
- $ref: '#/components/parameters/name' - $ref: '#/components/parameters/name'
responses: responses:
@ -982,7 +1169,7 @@ paths:
get: get:
summary: Get the comment threads of a video by its id summary: Get the comment threads of a video by its id
tags: tags:
- VideoComment - Video Comment
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
@ -1000,7 +1187,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoComment - Video Comment
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
responses: responses:
@ -1014,7 +1201,7 @@ paths:
get: get:
summary: 'Get the comment thread by its id, of a video by its id' summary: 'Get the comment thread by its id, of a video by its id'
tags: tags:
- VideoComment - Video Comment
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
- name: threadId - name: threadId
@ -1036,7 +1223,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoComment - Video Comment
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
- $ref: '#/components/parameters/commentId' - $ref: '#/components/parameters/commentId'
@ -1052,7 +1239,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoComment - Video Comment
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
- $ref: '#/components/parameters/commentId' - $ref: '#/components/parameters/commentId'
@ -1065,7 +1252,7 @@ paths:
security: security:
- OAuth2: [] - OAuth2: []
tags: tags:
- VideoRate - Video Rate
parameters: parameters:
- $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/id2'
responses: responses:
@ -1096,8 +1283,12 @@ paths:
items: items:
$ref: '#/components/schemas/Video' $ref: '#/components/schemas/Video'
servers: servers:
- url: 'https://peertube.cpy.re/api/v1'
description: Live Test Server (live data - stable version)
- url: 'https://peertube2.cpy.re/api/v1' - url: 'https://peertube2.cpy.re/api/v1'
description: Live Server description: Live Test Server (live data - bleeding edge version)
- url: 'https://peertube3.cpy.re/api/v1'
description: Live Test Server (live data - bleeding edge version)
components: components:
parameters: parameters:
start: start:
@ -1417,6 +1608,10 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/VideoChannel' $ref: '#/components/schemas/VideoChannel'
UserWatchingVideo:
properties:
currentTime:
type: number
ServerConfig: ServerConfig:
properties: properties:
signup: signup: