From fe05c3acbd48c72ac7e503bebde91830121a0bf1 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 09:16:41 +0100 Subject: [PATCH 01/13] Fix player progress bar when changing resolution --- client/src/assets/player/images/{tick.svg => tick-white.svg} | 0 client/src/assets/player/peertube-videojs-plugin.ts | 5 +++++ client/src/sass/player/settings-menu.scss | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) rename client/src/assets/player/images/{tick.svg => tick-white.svg} (100%) 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 +} From 7373507fa830b0f18cb4cd95dfd923b1600e501d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 10:05:25 +0100 Subject: [PATCH 02/13] Improve video upload error handling --- client/src/app/shared/misc/utils.ts | 7 ++++++- .../video-import-torrent.component.html | 7 ++++++- .../video-import-torrent.component.scss | 8 ++++++++ .../video-import-torrent.component.ts | 9 ++++++--- .../video-import-url.component.html | 8 +++++++- .../video-import-url.component.scss | 8 ++++++++ .../video-add-components/video-import-url.component.ts | 9 ++++++--- .../+video-edit/video-add-components/video-send.ts | 1 + .../video-add-components/video-upload.component.html | 9 +++++++-- .../video-add-components/video-upload.component.scss | 10 +++++++++- .../video-add-components/video-upload.component.ts | 9 +++++++-- .../app/videos/+video-edit/video-add.component.html | 8 ++++---- .../src/app/videos/+video-edit/video-add.component.ts | 5 +++++ server/middlewares/validators/videos/videos.ts | 2 ++ 14 files changed, 82 insertions(+), 18 deletions(-) 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/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/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 656d161d8..bf21bca8c 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -393,6 +393,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.' }) From 8d1fa36ad22a21a9b0fb6bf51a27d09954220013 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 11:18:13 +0100 Subject: [PATCH 03/13] Do not host remote AP objects --- server/controllers/activitypub/client.ts | 12 +++++++++++- server/middlewares/cache.ts | 7 +++++++ server/models/redundancy/video-redundancy.ts | 7 +++++-- server/tests/api/check-params/user-subscriptions.ts | 5 +++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index ffbf1ba19..a342a48d4 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) 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/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/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts index 9fba99ac8..6af7ed43b 100644 --- a/server/tests/api/check-params/user-subscriptions.ts +++ b/server/tests/api/check-params/user-subscriptions.ts @@ -15,6 +15,7 @@ import { userLogin } from '../../utils' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' +import { waitJobs } from '../../utils/server/jobs' describe('Test user subscriptions API validators', function () { const path = '/api/v1/users/me/subscriptions' @@ -141,6 +142,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, @@ -148,6 +151,8 @@ describe('Test user subscriptions API validators', function () { fields: { uri: 'user1_channel@localhost:9001' }, statusCodeExpected: 204 }) + + await waitJobs([ server ]) }) }) From cfd140abd6b748b4307d64fc33ea5aac73f94262 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Fri, 16 Nov 2018 11:05:28 +0100 Subject: [PATCH 04/13] remove superfluous privacy field for upload --- support/doc/api/openapi.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 666e48a41..9f2997774 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -771,7 +771,6 @@ paths: - videofile - channelId - name - - privacy x-code-samples: - lang: Shell source: | @@ -781,7 +780,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 +796,6 @@ paths: videofile@$FILE_PATH \ channelId=$CHANNEL_ID \ name=$NAME \ - privacy=$PRIVACY \ "Authorization:Bearer $token" /videos/abuse: get: From 6441981bc6e5063dd09e742e4e34ab848ab00ea8 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Fri, 16 Nov 2018 12:11:00 +0100 Subject: [PATCH 05/13] adding ownership and watching video APIs to the spec --- support/doc/api/openapi.yaml | 132 ++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 9f2997774..8f5f886a1 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -22,6 +22,8 @@ info: 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. +externalDocs: + url: https://docs.joinpeertube.org/api.html tags: - name: Accounts description: > @@ -144,7 +146,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 +154,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: >- @@ -701,6 +742,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 @@ -1093,8 +1213,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: @@ -1414,6 +1538,10 @@ components: type: array items: $ref: '#/components/schemas/VideoChannel' + UserWatchingVideo: + properties: + currentTime: + type: number ServerConfig: properties: signup: From 5776f78e3b3f3a371ec30c7fcb11e7ca17f2f65e Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Fri, 16 Nov 2018 14:33:48 +0100 Subject: [PATCH 06/13] grouping tags by main category in the spec --- support/doc/api/openapi.yaml | 159 +++++++++++++++++++++++++---------- 1 file changed, 114 insertions(+), 45 deletions(-) diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 8f5f886a1..5764a0e30 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -10,29 +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 @@ -44,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 @@ -72,16 +76,50 @@ 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 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: @@ -128,6 +166,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: @@ -264,7 +333,7 @@ paths: - OAuth2: - admin tags: - - ServerFollowing + - Server Following summary: Unfollow a server by hostname parameters: - name: host @@ -279,7 +348,7 @@ paths: /server/followers: get: tags: - - ServerFollowing + - Server Following summary: Get followers of the server parameters: - $ref: '#/components/parameters/start' @@ -297,7 +366,7 @@ paths: /server/following: get: tags: - - ServerFollowing + - Server Following summary: Get servers followed by the server parameters: - $ref: '#/components/parameters/start' @@ -317,7 +386,7 @@ paths: - OAuth2: - admin tags: - - ServerFollowing + - Server Following summary: Follow a server responses: '204': @@ -923,7 +992,7 @@ paths: security: - OAuth2: [] tags: - - VideoAbuse + - Video Abuse parameters: - $ref: '#/components/parameters/start' - $ref: '#/components/parameters/count' @@ -943,7 +1012,7 @@ paths: security: - OAuth2: [] tags: - - VideoAbuse + - Video Abuse parameters: - $ref: '#/components/parameters/id2' responses: @@ -957,7 +1026,7 @@ paths: - admin - moderator tags: - - VideoBlacklist + - Video Blacklist parameters: - $ref: '#/components/parameters/id2' responses: @@ -970,7 +1039,7 @@ paths: - admin - moderator tags: - - VideoBlacklist + - Video Blacklist parameters: - $ref: '#/components/parameters/id2' responses: @@ -984,7 +1053,7 @@ paths: - admin - moderator tags: - - VideoBlacklist + - Video Blacklist parameters: - $ref: '#/components/parameters/start' - $ref: '#/components/parameters/count' @@ -1002,7 +1071,7 @@ paths: get: summary: Get list of video channels tags: - - VideoChannel + - Video Channel parameters: - $ref: '#/components/parameters/start' - $ref: '#/components/parameters/count' @@ -1021,7 +1090,7 @@ paths: security: - OAuth2: [] tags: - - VideoChannel + - Video Channel responses: '204': $ref: '#/paths/~1users~1me/put/responses/204' @@ -1031,7 +1100,7 @@ paths: get: summary: Get a video channel by its id tags: - - VideoChannel + - Video Channel parameters: - $ref: '#/components/parameters/id3' responses: @@ -1046,7 +1115,7 @@ paths: security: - OAuth2: [] tags: - - VideoChannel + - Video Channel parameters: - $ref: '#/components/parameters/id3' responses: @@ -1059,7 +1128,7 @@ paths: security: - OAuth2: [] tags: - - VideoChannel + - Video Channel parameters: - $ref: '#/components/parameters/id3' responses: @@ -1069,7 +1138,7 @@ paths: get: summary: Get videos of a video channel by its id tags: - - VideoChannel + - Video Channel parameters: - $ref: '#/components/parameters/id3' responses: @@ -1083,7 +1152,7 @@ paths: get: summary: Get video channels of an account by its name tags: - - VideoChannel + - Video Channel parameters: - $ref: '#/components/parameters/name' responses: @@ -1099,7 +1168,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' @@ -1117,7 +1186,7 @@ paths: security: - OAuth2: [] tags: - - VideoComment + - Video Comment parameters: - $ref: '#/components/parameters/id2' responses: @@ -1131,7 +1200,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 @@ -1153,7 +1222,7 @@ paths: security: - OAuth2: [] tags: - - VideoComment + - Video Comment parameters: - $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/commentId' @@ -1169,7 +1238,7 @@ paths: security: - OAuth2: [] tags: - - VideoComment + - Video Comment parameters: - $ref: '#/components/parameters/id2' - $ref: '#/components/parameters/commentId' @@ -1182,7 +1251,7 @@ paths: security: - OAuth2: [] tags: - - VideoRate + - Video Rate parameters: - $ref: '#/components/parameters/id2' responses: From 8d4273463fb19d503b1aa0a32dc289f292ed614e Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 15:02:48 +0100 Subject: [PATCH 07/13] Check follow constraints when getting a video --- .../+video-watch/video-watch.component.ts | 2 +- config/default.yaml | 5 +- config/production.yaml.example | 5 +- server/controllers/api/videos/index.ts | 2 + server/middlewares/oauth.ts | 16 ++ .../middlewares/validators/videos/videos.ts | 52 ++++- server/models/video/video.ts | 17 ++ server/tests/api/server/follow-constraints.ts | 215 ++++++++++++++++++ server/tests/api/server/index.ts | 1 + 9 files changed, 301 insertions(+), 14 deletions(-) create mode 100644 server/tests/api/server/follow-constraints.ts 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/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/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/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 bf21bca8c..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, 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/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' From babecc3c09cd4ed06fe643a97fff4bcc31c5a9be Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 15:38:09 +0100 Subject: [PATCH 08/13] Fix AP collections pagination --- server/controllers/activitypub/client.ts | 4 ++-- server/helpers/activitypub.ts | 14 +++++++------- server/models/activitypub/actor-follow.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index a342a48d4..d9d385460 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -298,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) { @@ -306,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/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/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 } } From 58d515e32fe1d0133435b3a5e550c6ff24906fff Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 16:48:17 +0100 Subject: [PATCH 09/13] Fix images size when downloading them --- server/helpers/requests.ts | 12 +++++++++++- server/lib/activitypub/actor.ts | 9 +++------ server/lib/activitypub/videos.ts | 10 +++------- server/lib/job-queue/handlers/video-import.ts | 10 +++++----- server/tests/api/redundancy/redundancy.ts | 5 +++-- 5 files changed, 25 insertions(+), 21 deletions(-) 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/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 47f4e59fc..a8a2f305f 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts @@ -17,7 +17,7 @@ import { viewVideo, wait, waitUntilLog, - checkVideoFilesWereRemoved, removeVideo + checkVideoFilesWereRemoved, removeVideo, getVideoWithToken } from '../../utils' import { waitJobs } from '../../utils/server/jobs' import * as magnetUtil from 'magnet-uri' @@ -93,7 +93,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) { From d8c9996ce2b4de3ef1f2d36f63e461006bab58ed Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 17:02:21 +0100 Subject: [PATCH 10/13] Improve message visibility on signup --- client/src/app/signup/signup.component.html | 3 ++- client/src/app/signup/signup.component.ts | 20 ++++++++++--------- .../video-caption-add-modal.component.ts | 1 + 3 files changed, 14 insertions(+), 10 deletions(-) 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..607d64893 100644 --- a/client/src/app/signup/signup.component.ts +++ b/client/src/app/signup/signup.component.ts @@ -12,7 +12,9 @@ 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, @@ -50,17 +52,17 @@ 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.notificationsService.success( + this.i18n('Success'), + this.i18n('Registration for {{username}} complete.', { username: userCreate.username }) + ) this.redirectService.redirectToHomepage() }, 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 () { From 43e9d2af7d3525bc709e4c6d15fe85f65795f5fa Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Nov 2018 17:06:19 +0100 Subject: [PATCH 11/13] Auto login user on signup --- client/src/app/signup/signup.component.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts index 607d64893..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' @@ -18,6 +18,7 @@ export class SignupComponent extends FormReactive implements OnInit { constructor ( protected formValidatorService: FormValidatorService, + private authService: AuthService, private userValidatorsService: UserValidatorsService, private notificationsService: NotificationsService, private userService: UserService, @@ -59,11 +60,20 @@ export class SignupComponent extends FormReactive implements OnInit { return } - this.notificationsService.success( - this.i18n('Success'), - this.i18n('Registration for {{username}} complete.', { username: userCreate.username }) - ) - 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 From 9ab81fc4a91c84b5276a269d5913008a40f365ce Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Sat, 17 Nov 2018 15:15:45 +0100 Subject: [PATCH 12/13] =?UTF-8?q?grouping=20moderation=20endpoints=20in=20?= =?UTF-8?q?the=20REST=C2=A0API=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- support/doc/api/openapi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 5764a0e30..ea524d8d2 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -106,6 +106,7 @@ x-tagGroups: - Video Rate - name: Moderation tags: + - Vdieo Abuse - Video Blacklist - name: Public Instance Information tags: From 9d0b856e930ee1c676d16a56408a3e4a18f8f978 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Sat, 17 Nov 2018 15:17:33 +0100 Subject: [PATCH 13/13] (quickfix) typo in openapi spec groups --- support/doc/api/openapi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index ea524d8d2..af829cc62 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -106,7 +106,7 @@ x-tagGroups: - Video Rate - name: Moderation tags: - - Vdieo Abuse + - Video Abuse - Video Blacklist - name: Public Instance Information tags: