diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 78be2e5dd..78e8e9682 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -124,6 +124,10 @@ function sortBy (obj: any[], key1: string, key2?: string) {
})
}
+function scrollToTop () {
+ window.scroll(0, 0)
+}
+
export {
sortBy,
durationToString,
@@ -135,5 +139,6 @@ export {
immutableAssign,
objectToFormData,
lineFeedToHtml,
- removeElementFromArray
+ removeElementFromArray,
+ scrollToTop
}
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html
index 531a97814..0207a166e 100644
--- a/client/src/app/signup/signup.component.html
+++ b/client/src/app/signup/signup.component.html
@@ -4,6 +4,7 @@
Create an account
+
{{ info }}
{{ error }}
@@ -59,7 +60,7 @@
-
+
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts
index cf2657b85..3341d4e09 100644
--- a/client/src/app/signup/signup.component.ts
+++ b/client/src/app/signup/signup.component.ts
@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { UserCreate } 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 { 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' ]
})
export class SignupComponent extends FormReactive implements OnInit {
+ info: string = null
error: string = null
+ signupDone = false
constructor (
protected formValidatorService: FormValidatorService,
+ private authService: AuthService,
private userValidatorsService: UserValidatorsService,
private notificationsService: NotificationsService,
private userService: UserService,
@@ -50,18 +53,27 @@ export class SignupComponent extends FormReactive implements OnInit {
this.userService.signup(userCreate).subscribe(
() => {
+ this.signupDone = true
+
if (this.requiresEmailVerification) {
- this.notificationsService.alert(
- this.i18n('Welcome'),
- this.i18n('Please check your email to verify your account and complete signup.')
- )
- } else {
- this.notificationsService.success(
- this.i18n('Success'),
- this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
- )
+ this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.')
+ return
}
- this.redirectService.redirectToHomepage()
+
+ // Auto login
+ this.authService.login(userCreate.username, userCreate.password)
+ .subscribe(
+ () => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })
+ )
+
+ this.redirectService.redirectToHomepage()
+ },
+
+ err => this.error = err.message
+ )
},
err => this.error = err.message
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
index 796fbe531..eaf819726 100644
--- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
+++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts
@@ -60,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
hide () {
this.closingModal = true
this.openedModal.close()
+ this.form.reset()
}
isReplacingExistingCaption () {
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index a933a64f0..11a81ad66 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -45,7 +45,12 @@
-
+
+
Sorry, but something went wrong
+ {{ error }}
+
+
+
Congratulations, the video will be imported with BitTorrent! You can already add information about this video.
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
index 262b0b68e..00626cd7b 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -7,6 +7,14 @@ $width-size: 190px;
@include peertube-select-container($width-size);
}
+.alert.alert-danger {
+ text-align: center;
+
+ & > div {
+ font-weight: $font-semibold;
+ }
+}
+
.import-video-torrent {
display: flex;
flex-direction: column;
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
index e13c06ce9..13776ae36 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
import { FormValidatorService } from '@app/shared'
import { VideoCaptionService } from '@app/shared/video-caption'
import { VideoImportService } from '@app/shared/video-import'
+import { scrollToTop } from '@app/shared/misc/utils'
@Component({
selector: 'my-video-import-torrent',
@@ -23,9 +24,9 @@ import { VideoImportService } from '@app/shared/video-import'
})
export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
@Output() firstStepDone = new EventEmitter
()
+ @Output() firstStepError = new EventEmitter()
@ViewChild('torrentfileInput') torrentfileInput: ElementRef
- videoFileName: string
magnetUri = ''
isImportingVideo = false
@@ -33,6 +34,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
isUpdatingVideo = false
video: VideoEdit
+ error: string
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
@@ -104,6 +106,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
err => {
this.loadingBar.complete()
this.isImportingVideo = false
+ this.firstStepError.emit()
this.notificationsService.error(this.i18n('Error'), err.message)
}
)
@@ -129,8 +132,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
},
err => {
- this.isUpdatingVideo = false
- this.notificationsService.error(this.i18n('Error'), err.message)
+ this.error = err.message
+ scrollToTop()
console.error(err)
}
)
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index 9f5fc6d22..533446672 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -37,7 +37,13 @@
-
+
+
+
Sorry, but something went wrong
+ {{ error }}
+
+
+
Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video.
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
index 7c6deda1d..e907edc70 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss
@@ -7,6 +7,14 @@ $width-size: 190px;
@include peertube-select-container($width-size);
}
+.alert.alert-danger {
+ text-align: center;
+
+ & > div {
+ font-weight: $font-semibold;
+ }
+}
+
.import-video-url {
display: flex;
flex-direction: column;
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
index 031e557ed..9cdface75 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -12,6 +12,7 @@ import { VideoEdit } from '@app/shared/video/video-edit.model'
import { FormValidatorService } from '@app/shared'
import { VideoCaptionService } from '@app/shared/video-caption'
import { VideoImportService } from '@app/shared/video-import'
+import { scrollToTop } from '@app/shared/misc/utils'
@Component({
selector: 'my-video-import-url',
@@ -23,15 +24,16 @@ import { VideoImportService } from '@app/shared/video-import'
})
export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
@Output() firstStepDone = new EventEmitter
()
+ @Output() firstStepError = new EventEmitter()
targetUrl = ''
- videoFileName: string
isImportingVideo = false
hasImportedVideo = false
isUpdatingVideo = false
video: VideoEdit
+ error: string
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
@@ -96,6 +98,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
err => {
this.loadingBar.complete()
this.isImportingVideo = false
+ this.firstStepError.emit()
this.notificationsService.error(this.i18n('Error'), err.message)
}
)
@@ -121,8 +124,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
},
err => {
- this.isUpdatingVideo = false
- this.notificationsService.error(this.i18n('Error'), err.message)
+ this.error = err.message
+ scrollToTop()
console.error(err)
}
)
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
index 1bf22e1a9..71d2544d8 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts
@@ -21,6 +21,7 @@ export abstract class VideoSend extends FormReactive implements OnInit {
firstStepChannelId = 0
abstract firstStepDone: EventEmitter
+ abstract firstStepError: EventEmitter
protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy
protected loadingBar: LoadingBarService
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
index fa57c8cb5..a09f54dfc 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html
@@ -29,7 +29,7 @@
-
+
+
+
Sorry, but something went wrong
+ {{ error }}
+
+
-
\ No newline at end of file
+
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
index dbae5230d..cf1725ef9 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss
@@ -5,6 +5,14 @@
@include peertube-select-container(190px);
}
+.alert.alert-danger {
+ text-align: center;
+
+ & > div {
+ font-weight: $font-semibold;
+ }
+}
+
.upload-video {
display: flex;
flex-direction: column;
@@ -82,4 +90,4 @@
margin-left: 10px;
}
-}
\ No newline at end of file
+}
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
index 8e2d0deaf..3fcb71ac3 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts
@@ -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 { FormValidatorService, UserService } from '@app/shared'
import { VideoCaptionService } from '@app/shared/video-caption'
+import { scrollToTop } from '@app/shared/misc/utils'
@Component({
selector: 'my-video-upload',
@@ -25,6 +26,7 @@ import { VideoCaptionService } from '@app/shared/video-caption'
})
export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
@Output() firstStepDone = new EventEmitter()
+ @Output() firstStepError = new EventEmitter()
@ViewChild('videofileInput') videofileInput: ElementRef
// So that it can be accessed in the template
@@ -43,6 +45,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
uuid: ''
}
+ error: string
+
protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC
constructor (
@@ -201,6 +205,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
this.isUploadingVideo = false
this.videoUploadPercents = 0
this.videoUploadObservable = null
+ this.firstStepError.emit()
this.notificationsService.error(this.i18n('Error'), err.message)
}
)
@@ -235,8 +240,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
},
err => {
- this.isUpdatingVideo = false
- this.notificationsService.error(this.i18n('Error'), err.message)
+ this.error = err.message
+ scrollToTop()
console.error(err)
}
)
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html
index e14e23aed..72a233b72 100644
--- a/client/src/app/videos/+video-edit/video-add.component.html
+++ b/client/src/app/videos/+video-edit/video-add.component.html
@@ -6,24 +6,24 @@
-
+
Upload a file
-
+
Import with URL
-
+
Import with torrent
-
+
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts
index 1a9247dbe..57a9d0ca7 100644
--- a/client/src/app/videos/+video-edit/video-add.component.ts
+++ b/client/src/app/videos/+video-edit/video-add.component.ts
@@ -27,6 +27,11 @@ export class VideoAddComponent implements CanComponentDeactivate {
this.videoName = videoName
}
+ onError () {
+ this.videoName = undefined
+ this.secondStepType = undefined
+ }
+
canDeactivate () {
if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index d0151ceb1..09ee96bdc 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -114,7 +114,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
)
.pipe(
// 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 ]) => {
const startTime = this.route.snapshot.queryParams.start
diff --git a/client/src/assets/player/images/tick.svg b/client/src/assets/player/images/tick-white.svg
similarity index 100%
rename from client/src/assets/player/images/tick.svg
rename to client/src/assets/player/images/tick-white.svg
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts
index 40da5f1f7..4fd5a9be2 100644
--- a/client/src/assets/player/peertube-videojs-plugin.ts
+++ b/client/src/assets/player/peertube-videojs-plugin.ts
@@ -111,6 +111,8 @@ class PeerTubePlugin extends Plugin {
const muted = getStoredMute()
if (muted !== undefined) this.player.muted(muted)
+ this.player.duration(options.videoDuration)
+
this.initializePlayer()
this.runTorrentInfoScheduler()
this.runViewAdd()
@@ -302,6 +304,9 @@ class PeerTubePlugin extends Plugin {
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 }
renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => {
this.renderer = renderer
diff --git a/client/src/sass/player/settings-menu.scss b/client/src/sass/player/settings-menu.scss
index d065e72fb..61965c85e 100644
--- a/client/src/sass/player/settings-menu.scss
+++ b/client/src/sass/player/settings-menu.scss
@@ -171,7 +171,7 @@ $setting-transition-easing: ease-out;
left: 8px;
content: ' ';
margin-top: 1px;
- background-image: url('#{$assets-path}/player/images/tick.svg');
+ background-image: url('#{$assets-path}/player/images/tick-white.svg');
}
}
}
@@ -197,4 +197,4 @@ $setting-transition-easing: ease-out;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/config/default.yaml b/config/default.yaml
index 0d7d948c2..257ec7ed1 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -58,7 +58,10 @@ log:
level: 'info' # debug/info/warning/error
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
anonymous: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index f9da8e0dd..ac15fc736 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -59,7 +59,10 @@ log:
level: 'info' # debug/info/warning/error
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
anonymous: false
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index ffbf1ba19..d9d385460 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -39,6 +39,7 @@ import {
import { VideoCaptionModel } from '../../models/video/video-caption'
import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy'
import { getServerActor } from '../../helpers/utils'
+import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
const activityPubClientRouter = express.Router()
@@ -164,6 +165,8 @@ function getAccountVideoRate (rateType: VideoRateType) {
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video
+ if (video.isOwned() === false) return res.redirect(video.url)
+
// We need captions to render AP object
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) {
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)
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) {
const videoComment: VideoCommentModel = res.locals.videoComment
+ if (videoComment.isOwned() === false) return res.redirect(videoComment.url)
+
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
const isPublic = true // Comments are always public
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) {
- 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 audience = getAudience(serverActor)
@@ -288,7 +298,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
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) {
@@ -296,7 +306,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
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) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index e654bdd09..89fd0432f 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -31,6 +31,7 @@ import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
+ checkVideoFollowConstraints,
commonVideosFiltersValidator,
optionalAuthenticate,
paginationValidator,
@@ -123,6 +124,7 @@ videosRouter.get('/:id/description',
videosRouter.get('/:id',
optionalAuthenticate,
asyncMiddleware(videosGetValidator),
+ asyncMiddleware(checkVideoFollowConstraints),
getVideo
)
videosRouter.post('/:id/views',
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 4bf6e387d..bcbd9be59 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -57,16 +57,16 @@ function activityPubContextify (data: T) {
}
type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird> | Promise>
-async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
+async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
if (!page || !validator.isInt(page)) {
// We just display the first page URL, we only need the total items
const result = await handler(0, 1)
return {
- id: url,
+ id: baseUrl,
type: 'OrderedCollection',
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
if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
- next = url + '?page=' + (page + 1)
+ next = baseUrl + '?page=' + (page + 1)
}
if (page > 1) {
- prev = url + '?page=' + (page - 1)
+ prev = baseUrl + '?page=' + (page - 1)
}
return {
- id: url + '?page=' + page,
+ id: baseUrl + '?page=' + page,
type: 'OrderedCollectionPage',
prev,
next,
- partOf: url,
+ partOf: baseUrl,
orderedItems: result.data,
totalItems: result.total
}
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 51facc9e0..805930a9f 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -2,6 +2,7 @@ import * as Bluebird from 'bluebird'
import { createWriteStream } from 'fs-extra'
import * as request from 'request'
import { ACTIVITY_PUB } from '../initializers'
+import { processImage } from './image-utils'
function doRequest (
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 {
doRequest,
- doRequestAndSaveToFile
+ doRequestAndSaveToFile,
+ downloadImage
}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index b16a00669..218dbc6a7 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -11,9 +11,9 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
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 { 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 { ActorModel } from '../../models/activitypub/actor'
import { AvatarModel } from '../../models/avatar/avatar'
@@ -180,10 +180,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
const avatarName = uuidv4() + extension
const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
- await doRequestAndSaveToFile({
- method: 'GET',
- uri: actorJSON.icon.url
- }, destPath)
+ await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE)
return avatarName
}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 5bd03c8c6..80de92f24 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -10,8 +10,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
-import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
-import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
+import { doRequest, downloadImage } from '../../helpers/requests'
+import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_MIMETYPE_EXT } from '../../initializers'
import { ActorModel } from '../../models/activitypub/actor'
import { TagModel } from '../../models/video/tag'
import { VideoModel } from '../../models/video/video'
@@ -97,11 +97,7 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
const thumbnailName = video.getThumbnailName()
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
- const options = {
- method: 'GET',
- uri: icon.url
- }
- return doRequestAndSaveToFile(options, thumbnailPath)
+ return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE)
}
function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index e3f2a276c..4de901c0c 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -6,8 +6,8 @@ import { VideoImportState } from '../../../../shared/models/videos'
import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
import { extname, join } from 'path'
import { VideoFileModel } from '../../../models/video/video-file'
-import { CONFIG, sequelizeTypescript, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
-import { doRequestAndSaveToFile } from '../../../helpers/requests'
+import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers'
+import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests'
import { VideoState } from '../../../../shared'
import { JobQueue } from '../index'
import { federateVideoIfNeeded } from '../../activitypub'
@@ -133,7 +133,7 @@ async function processFile (downloader: () => Promise, videoImport: Vide
videoId: videoImport.videoId
}
videoFile = new VideoFileModel(videoFileData)
- // Import if the import fails, to clean files
+ // To clean files if the import fails
videoImport.Video.VideoFiles = [ videoFile ]
// Move file
@@ -145,7 +145,7 @@ async function processFile (downloader: () => Promise, videoImport: Vide
if (options.downloadThumbnail) {
if (options.thumbnailUrl) {
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 {
await videoImport.Video.createThumbnail(videoFile)
}
@@ -157,7 +157,7 @@ async function processFile (downloader: () => Promise, videoImport: Vide
if (options.downloadPreview) {
if (options.thumbnailUrl) {
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 {
await videoImport.Video.createPreview(videoFile)
}
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index 1e00fc731..8ffe75700 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) {
logger.debug('No cached results for route %s.', req.originalUrl)
const sendSave = res.send.bind(res)
+ const redirectSave = res.redirect.bind(res)
res.send = (body) => {
if (res.statusCode >= 200 && res.statusCode < 400) {
@@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) {
return sendSave(body)
}
+ res.redirect = url => {
+ done()
+
+ return redirectSave(url)
+ }
+
return next()
}
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 5233b66bd..8c1df2c3e 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -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) {
if (req.header('authorization')) return authenticate(req, res, next)
+ res.locals.authenticated = false
+
return next()
}
@@ -53,6 +68,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF
export {
authenticate,
+ authenticatePromiseIfNeeded,
optionalAuthenticate,
token
}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 656d161d8..051a19e16 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -31,8 +31,8 @@ import {
} from '../../../helpers/custom-validators/videos'
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger'
-import { CONSTRAINTS_FIELDS } from '../../../initializers'
-import { authenticate } from '../../oauth'
+import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
+import { authenticatePromiseIfNeeded } from '../../oauth'
import { areValidationErrors } from '../utils'
import { cleanUpReqFiles } from '../../../helpers/express-utils'
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 { VideoFetchType } from '../../../helpers/video'
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
+import { getServerActor } from '../../../helpers/utils'
const videosAddValidator = getCommonVideoAttributes().concat([
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) => {
return [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -141,17 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
// Video private or blacklisted
if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
- return authenticate(req, res, () => {
- const user: UserModel = res.locals.oauth.token.User
+ await authenticatePromiseIfNeeded(req, res)
- // 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)) {
- return res.status(403)
- .json({ error: 'Cannot get this private or blacklisted video.' })
- }
+ const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
- return next()
- })
+ // Only the owner or a user that have blacklist rights can see the video
+ if (
+ !user ||
+ (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
+ ) {
+ return res.status(403)
+ .json({ error: 'Cannot get this private or blacklisted video.' })
+ }
+
+ return next()
}
// Video is public, anyone can access it
@@ -376,6 +405,7 @@ export {
videosAddValidator,
videosUpdateValidator,
videosGetValidator,
+ checkVideoFollowConstraints,
videosCustomGetValidator,
videosRemoveValidator,
@@ -393,6 +423,8 @@ export {
function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
if (req.body.scheduleUpdate) {
if (!req.body.scheduleUpdate.updateAt) {
+ logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
+
res.status(400)
.json({ error: 'Schedule update at is mandatory.' })
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 3373355ef..0a6935083 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -509,12 +509,12 @@ export class ActorFollowModel extends Model {
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)
return {
data: urls,
- total: parseInt(total, 10)
+ total: dataTotal ? parseInt(dataTotal.total, 10) : 0
}
}
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 35e0cd3b1..9de4356b4 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -117,8 +117,7 @@ export class VideoRedundancyModel extends Model {
@BeforeDestroy
static async removeFile (instance: VideoRedundancyModel) {
- // Not us
- if (!instance.strategy) return
+ if (!instance.isOwned()) return
const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
@@ -404,6 +403,10 @@ export class VideoRedundancyModel extends Model {
}))
}
+ isOwned () {
+ return !!this.strategy
+ }
+
toActivityPubObject (): CacheFileObject {
return {
id: this.url,
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c183933b..1e68b380c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1253,6 +1253,23 @@ export class VideoModel extends Model {
})
}
+ 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
static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
const serverActor = await getServerActor()
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts
index 2cf5a2415..8a9ced7c1 100644
--- a/server/tests/api/check-params/user-subscriptions.ts
+++ b/server/tests/api/check-params/user-subscriptions.ts
@@ -14,11 +14,13 @@ import {
setAccessTokensToServers,
userLogin
} from '../../../../shared/utils'
+
import {
checkBadCountPagination,
checkBadSortPagination,
checkBadStartPagination
} from '../../../../shared/utils/requests/check-api-params'
+import { waitJobs } from '../../../../shared/utils/server/jobs'
describe('Test user subscriptions API validators', function () {
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 () {
+ this.timeout(20000)
+
await makePostBodyRequest({
url: server.url,
path,
@@ -152,6 +156,8 @@ describe('Test user subscriptions API validators', function () {
fields: { uri: 'user1_channel@localhost:9001' },
statusCodeExpected: 204
})
+
+ await waitJobs([ server ])
})
})
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 663e31ead..2bc1b60ce 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -17,9 +17,10 @@ import {
viewVideo,
wait,
waitUntilLog,
- checkVideoFilesWereRemoved, removeVideo
+ checkVideoFilesWereRemoved, removeVideo, getVideoWithToken
} from '../../../../shared/utils'
import { waitJobs } from '../../../../shared/utils/server/jobs'
+
import * as magnetUtil from 'magnet-uri'
import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
import { ActorFollow } from '../../../../shared/models/actors'
@@ -93,7 +94,8 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str
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
for (const f of video.files) {
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts
new file mode 100644
index 000000000..3135fc568
--- /dev/null
+++ b/server/tests/api/server/follow-constraints.ts
@@ -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)
+ })
+})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index 78ab7e18b..6afcab1f9 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -1,5 +1,6 @@
import './config'
import './email'
+import './follow-constraints'
import './follows'
import './handle-down'
import './jobs'
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 666e48a41..af829cc62 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -10,27 +10,41 @@ info:
url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
x-logo:
url: 'https://joinpeertube.org/img/brand.png'
+ altText: PeerTube Project Homepage
description: |
# Introduction
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
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
- 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
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
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:
- name: Accounts
description: >
Using some features of PeerTube require authentication, for which Accounts
-
provide different levels of permission as well as associated user
- information.
-
- Accounts also encompass remote accounts discovered across the federation.
+ information. Accounts also encompass remote accounts discovered across the federation.
- name: Config
description: >
Each server exposes public information regarding supported videos and
@@ -42,23 +56,15 @@ tags:
- name: Job
description: >
Jobs are long-running tasks enqueued and processed by the instance
- itself.
-
- No additional worker registration is currently available.
- - name: ServerFollowing
+ itself. No additional worker registration is currently available.
+ - name: Server Following
description: >
Managing servers which the instance interacts with is crucial to the
- concept
-
- of federation in PeerTube and external video indexation. The PeerTube
- server
-
- then deals with inter-server ActivityPub operations and propagates
-
+ concept 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
-
endpoints.
- - name: VideoAbuse
+ - name: Video Abuse
description: |
Video abuses deal with reports of local or remote videos alike.
- name: Video
@@ -70,16 +76,51 @@ tags:
Videos from other instances federated by the instance (that is, instances
followed by the instance) can be found via keywords and other criteria of
the advanced search.
- - name: VideoComment
+ - name: Video Comment
description: >
Operations dealing with comments to a video. Comments are organized in
threads.
- - name: VideoChannel
+ - name: Video Channel
description: >
Operations dealing with creation, modification and video listing of a
- user's
-
- channels.
+ user's channels.
+ - name: Video Blacklist
+ 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:
'/accounts/{name}':
get:
@@ -126,6 +167,37 @@ paths:
source: |
# pip install httpie
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:
get:
tags:
@@ -144,7 +216,7 @@ paths:
get:
tags:
- Config
- summary: Get the configuration of the server
+ summary: Get the public configuration of the server
responses:
'200':
description: successful operation
@@ -152,6 +224,45 @@ paths:
application/json:
schema:
$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}':
get:
summary: >-
@@ -223,7 +334,7 @@ paths:
- OAuth2:
- admin
tags:
- - ServerFollowing
+ - Server Following
summary: Unfollow a server by hostname
parameters:
- name: host
@@ -238,7 +349,7 @@ paths:
/server/followers:
get:
tags:
- - ServerFollowing
+ - Server Following
summary: Get followers of the server
parameters:
- $ref: '#/components/parameters/start'
@@ -256,7 +367,7 @@ paths:
/server/following:
get:
tags:
- - ServerFollowing
+ - Server Following
summary: Get servers followed by the server
parameters:
- $ref: '#/components/parameters/start'
@@ -276,7 +387,7 @@ paths:
- OAuth2:
- admin
tags:
- - ServerFollowing
+ - Server Following
summary: Follow a server
responses:
'204':
@@ -701,6 +812,85 @@ paths:
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:
post:
summary: Upload a video file with its metadata
@@ -771,7 +961,6 @@ paths:
- videofile
- channelId
- name
- - privacy
x-code-samples:
- lang: Shell
source: |
@@ -781,7 +970,6 @@ paths:
PASSWORD=""
FILE_PATH=""
CHANNEL_ID=""
- PRIVACY="1" # public: 1, unlisted: 2, private: 3
NAME=""
API_PATH="https://peertube2.cpy.re/api/v1"
@@ -798,7 +986,6 @@ paths:
videofile@$FILE_PATH \
channelId=$CHANNEL_ID \
name=$NAME \
- privacy=$PRIVACY \
"Authorization:Bearer $token"
/videos/abuse:
get:
@@ -806,7 +993,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoAbuse
+ - Video Abuse
parameters:
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -826,7 +1013,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoAbuse
+ - Video Abuse
parameters:
- $ref: '#/components/parameters/id2'
responses:
@@ -840,7 +1027,7 @@ paths:
- admin
- moderator
tags:
- - VideoBlacklist
+ - Video Blacklist
parameters:
- $ref: '#/components/parameters/id2'
responses:
@@ -853,7 +1040,7 @@ paths:
- admin
- moderator
tags:
- - VideoBlacklist
+ - Video Blacklist
parameters:
- $ref: '#/components/parameters/id2'
responses:
@@ -867,7 +1054,7 @@ paths:
- admin
- moderator
tags:
- - VideoBlacklist
+ - Video Blacklist
parameters:
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -885,7 +1072,7 @@ paths:
get:
summary: Get list of video channels
tags:
- - VideoChannel
+ - Video Channel
parameters:
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
@@ -904,7 +1091,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoChannel
+ - Video Channel
responses:
'204':
$ref: '#/paths/~1users~1me/put/responses/204'
@@ -914,7 +1101,7 @@ paths:
get:
summary: Get a video channel by its id
tags:
- - VideoChannel
+ - Video Channel
parameters:
- $ref: '#/components/parameters/id3'
responses:
@@ -929,7 +1116,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoChannel
+ - Video Channel
parameters:
- $ref: '#/components/parameters/id3'
responses:
@@ -942,7 +1129,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoChannel
+ - Video Channel
parameters:
- $ref: '#/components/parameters/id3'
responses:
@@ -952,7 +1139,7 @@ paths:
get:
summary: Get videos of a video channel by its id
tags:
- - VideoChannel
+ - Video Channel
parameters:
- $ref: '#/components/parameters/id3'
responses:
@@ -966,7 +1153,7 @@ paths:
get:
summary: Get video channels of an account by its name
tags:
- - VideoChannel
+ - Video Channel
parameters:
- $ref: '#/components/parameters/name'
responses:
@@ -982,7 +1169,7 @@ paths:
get:
summary: Get the comment threads of a video by its id
tags:
- - VideoComment
+ - Video Comment
parameters:
- $ref: '#/components/parameters/id2'
- $ref: '#/components/parameters/start'
@@ -1000,7 +1187,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoComment
+ - Video Comment
parameters:
- $ref: '#/components/parameters/id2'
responses:
@@ -1014,7 +1201,7 @@ paths:
get:
summary: 'Get the comment thread by its id, of a video by its id'
tags:
- - VideoComment
+ - Video Comment
parameters:
- $ref: '#/components/parameters/id2'
- name: threadId
@@ -1036,7 +1223,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoComment
+ - Video Comment
parameters:
- $ref: '#/components/parameters/id2'
- $ref: '#/components/parameters/commentId'
@@ -1052,7 +1239,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoComment
+ - Video Comment
parameters:
- $ref: '#/components/parameters/id2'
- $ref: '#/components/parameters/commentId'
@@ -1065,7 +1252,7 @@ paths:
security:
- OAuth2: []
tags:
- - VideoRate
+ - Video Rate
parameters:
- $ref: '#/components/parameters/id2'
responses:
@@ -1096,8 +1283,12 @@ paths:
items:
$ref: '#/components/schemas/Video'
servers:
+ - url: 'https://peertube.cpy.re/api/v1'
+ description: Live Test Server (live data - stable version)
- 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:
parameters:
start:
@@ -1417,6 +1608,10 @@ components:
type: array
items:
$ref: '#/components/schemas/VideoChannel'
+ UserWatchingVideo:
+ properties:
+ currentTime:
+ type: number
ServerConfig:
properties:
signup: