From a9bfa85d2cdf13670aaced740da5b493fbeddfce Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 15 Dec 2021 15:58:10 +0100 Subject: [PATCH] Add ability for admins to set default p2p policy --- client/e2e/src/po/anonymous-settings.po.ts | 21 ++ .../po/{my-account.ts => my-account.po.ts} | 17 +- client/e2e/src/po/video-upload.po.ts | 8 +- client/e2e/src/po/video-watch.po.ts | 13 +- client/e2e/src/suites-all/videos.e2e-spec.ts | 2 +- .../custom-server-defaults.e2e-spec.ts | 75 +++++-- .../suites-local/user-settings.e2e-spec.ts | 82 ++++++++ .../src/suites-local/videos-list.e2e-spec.ts | 2 +- client/e2e/src/utils/elements.ts | 21 +- client/e2e/src/utils/hooks.ts | 3 + .../information/privacy-concerns.component.ts | 26 ++- .../video-watch-playlist.component.ts | 39 ++-- .../recommended-videos.component.ts | 27 +-- .../+video-watch/video-watch.component.ts | 64 +++--- client/src/app/app.component.ts | 17 +- client/src/app/core/auth/auth-user.model.ts | 44 +---- client/src/app/core/auth/auth.service.ts | 16 +- client/src/app/core/core.module.ts | 3 +- client/src/app/core/users/index.ts | 1 + .../core/users/user-local-storage.service.ts | 186 ++++++++++++++++++ client/src/app/core/users/user.model.ts | 8 +- client/src/app/core/users/user.service.ts | 104 ++-------- client/src/app/menu/menu.component.html | 2 +- client/src/app/menu/menu.component.ts | 4 +- .../shared/shared-search/search.service.ts | 7 +- .../user-video-settings.component.html | 2 +- .../user-video-settings.component.ts | 8 +- .../player/peertube-player-local-storage.ts | 9 - .../assets/player/peertube-player-manager.ts | 6 +- .../assets/player/peertube-videojs-typings.ts | 2 + client/src/assets/player/utils.ts | 10 +- .../player/webtorrent/webtorrent-plugin.ts | 13 +- client/src/root-helpers/index.ts | 1 + .../src/root-helpers/local-storage-utils.ts | 10 + client/src/root-helpers/users/index.ts | 1 - .../users/user-local-storage-keys.ts | 16 +- .../users/user-local-storage-manager.ts | 55 ------ client/src/root-helpers/users/user-tokens.ts | 69 +++---- client/src/standalone/videos/embed.ts | 28 ++- config/default.yaml | 7 +- config/production.yaml.example | 5 + server/controllers/api/users/index.ts | 2 + server/controllers/api/users/me.ts | 8 +- server/helpers/custom-validators/users.ts | 4 +- server/initializers/config.ts | 3 + server/initializers/constants.ts | 2 +- server/initializers/installer.ts | 1 + .../migrations/0675-p2p-enabled.ts | 21 ++ server/lib/auth/oauth-model.ts | 1 + server/lib/server-config-manager.ts | 3 + server/middlewares/validators/users.ts | 4 + server/models/user/user.ts | 13 +- server/tests/api/server/config-defaults.ts | 69 +++++-- server/tests/api/server/follows.ts | 2 +- server/tests/api/users/users.ts | 22 +++ shared/models/server/server-config.model.ts | 4 + shared/models/users/user-update-me.model.ts | 3 + shared/models/users/user.model.ts | 4 + support/doc/api/openapi.yaml | 4 +- 59 files changed, 789 insertions(+), 415 deletions(-) create mode 100644 client/e2e/src/po/anonymous-settings.po.ts rename client/e2e/src/po/{my-account.ts => my-account.po.ts} (89%) create mode 100644 client/e2e/src/suites-local/user-settings.e2e-spec.ts create mode 100644 client/src/app/core/users/user-local-storage.service.ts create mode 100644 client/src/root-helpers/local-storage-utils.ts delete mode 100644 client/src/root-helpers/users/user-local-storage-manager.ts create mode 100644 server/initializers/migrations/0675-p2p-enabled.ts diff --git a/client/e2e/src/po/anonymous-settings.po.ts b/client/e2e/src/po/anonymous-settings.po.ts new file mode 100644 index 000000000..180d371fa --- /dev/null +++ b/client/e2e/src/po/anonymous-settings.po.ts @@ -0,0 +1,21 @@ +import { getCheckbox } from '../utils' + +export class AnonymousSettingsPage { + + async openSettings () { + const link = await $$('.menu-link').filter(async i => { + return await i.getText() === 'My settings' + }).then(links => links[0]) + + await link.click() + + await $('my-user-video-settings').waitForDisplayed() + } + + async clickOnP2PCheckbox () { + const p2p = getCheckbox('p2pEnabled') + await p2p.waitForClickable() + + await p2p.click() + } +} diff --git a/client/e2e/src/po/my-account.ts b/client/e2e/src/po/my-account.po.ts similarity index 89% rename from client/e2e/src/po/my-account.ts rename to client/e2e/src/po/my-account.po.ts index b51614fd9..13a764e87 100644 --- a/client/e2e/src/po/my-account.ts +++ b/client/e2e/src/po/my-account.po.ts @@ -1,4 +1,4 @@ -import { go } from '../utils' +import { getCheckbox, go } from '../utils' export class MyAccountPage { @@ -27,6 +27,21 @@ export class MyAccountPage { await nsfw.scrollIntoView(false) // Avoid issues with fixed header on firefox await nsfw.selectByAttribute('value', newValue) + await this.submitVideoSettings() + } + + async clickOnP2PCheckbox () { + const p2p = getCheckbox('p2pEnabled') + + await p2p.waitForClickable() + await p2p.scrollIntoView(false) // Avoid issues with fixed header on firefox + + await p2p.click() + + await this.submitVideoSettings() + } + + private async submitVideoSettings () { const submit = $('my-user-video-settings input[type=submit]') await submit.scrollIntoView(false) await submit.click() diff --git a/client/e2e/src/po/video-upload.po.ts b/client/e2e/src/po/video-upload.po.ts index dd437c390..2206b56c3 100644 --- a/client/e2e/src/po/video-upload.po.ts +++ b/client/e2e/src/po/video-upload.po.ts @@ -1,5 +1,5 @@ import { join } from 'path' -import { clickOnCheckbox } from '../utils' +import { getCheckbox, selectCustomSelect } from '../utils' export class VideoUploadPage { async navigateTo () { @@ -32,7 +32,7 @@ export class VideoUploadPage { } setAsNSFW () { - return clickOnCheckbox('nsfw') + return getCheckbox('nsfw').click() } async validSecondUploadStep (videoName: string) { @@ -47,6 +47,10 @@ export class VideoUploadPage { }) } + setAsPublic () { + return selectCustomSelect('privacy', 'Public') + } + private getSecondStepSubmitButton () { return $('.submit-container my-button') } diff --git a/client/e2e/src/po/video-watch.po.ts b/client/e2e/src/po/video-watch.po.ts index 1406c971a..cecda3a8b 100644 --- a/client/e2e/src/po/video-watch.po.ts +++ b/client/e2e/src/po/video-watch.po.ts @@ -39,12 +39,23 @@ export class VideoWatchPage { return $('my-video-comment-add').isExisting() } + isPrivacyWarningDisplayed () { + return $('my-privacy-concerns').isDisplayed() + } + async goOnAssociatedEmbed () { let url = await browser.getUrl() url = url.replace('/w/', '/videos/embed/') url = url.replace(':3333', ':9001') - return go(url) + await go(url) + await $('.vjs-big-play-button').waitForDisplayed() + } + + async isEmbedWarningDisplayed () { + const text = await $('.vjs-dock-description').getText() + + return !!text.trim() } goOnP2PMediaLoaderEmbed () { diff --git a/client/e2e/src/suites-all/videos.e2e-spec.ts b/client/e2e/src/suites-all/videos.e2e-spec.ts index 3b8305a25..b3a87c8e2 100644 --- a/client/e2e/src/suites-all/videos.e2e-spec.ts +++ b/client/e2e/src/suites-all/videos.e2e-spec.ts @@ -1,5 +1,5 @@ import { LoginPage } from '../po/login.po' -import { MyAccountPage } from '../po/my-account' +import { MyAccountPage } from '../po/my-account.po' import { PlayerPage } from '../po/player.po' import { VideoListPage } from '../po/video-list.po' import { VideoUpdatePage } from '../po/video-update.po' diff --git a/client/e2e/src/suites-local/custom-server-defaults.e2e-spec.ts b/client/e2e/src/suites-local/custom-server-defaults.e2e-spec.ts index c2c8edcc9..e060d382f 100644 --- a/client/e2e/src/suites-local/custom-server-defaults.e2e-spec.ts +++ b/client/e2e/src/suites-local/custom-server-defaults.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginPage } from '../po/login.po' import { VideoUploadPage } from '../po/video-upload.po' import { VideoWatchPage } from '../po/video-watch.po' -import { isMobileDevice, isSafari, waitServerUp } from '../utils' +import { go, isMobileDevice, isSafari, waitServerUp } from '../utils' describe('Custom server defaults', () => { let videoUploadPage: VideoUploadPage @@ -10,9 +10,7 @@ describe('Custom server defaults', () => { before(async () => { await waitServerUp() - }) - beforeEach(async () => { loginPage = new LoginPage() videoUploadPage = new VideoUploadPage() videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari()) @@ -20,18 +18,69 @@ describe('Custom server defaults', () => { await browser.maximizeWindow() }) - it('Should upload a video with custom default values', async function () { - await loginPage.loginAsRootUser() - await videoUploadPage.navigateTo() - await videoUploadPage.uploadVideo() - await videoUploadPage.validSecondUploadStep('video') + describe('Publish default values', function () { + before(async function () { + await loginPage.loginAsRootUser() + }) + + it('Should upload a video with custom default values', async function () { + await videoUploadPage.navigateTo() + await videoUploadPage.uploadVideo() + await videoUploadPage.validSecondUploadStep('video') - await videoWatchPage.waitWatchVideoName('video') + await videoWatchPage.waitWatchVideoName('video') - expect(await videoWatchPage.getPrivacy()).toBe('Internal') - expect(await videoWatchPage.getLicence()).toBe('Attribution - Non Commercial') - expect(await videoWatchPage.isDownloadEnabled()).toBeFalsy() - expect(await videoWatchPage.areCommentsEnabled()).toBeFalsy() + expect(await videoWatchPage.getPrivacy()).toBe('Internal') + expect(await videoWatchPage.getLicence()).toBe('Attribution - Non Commercial') + expect(await videoWatchPage.isDownloadEnabled()).toBeFalsy() + expect(await videoWatchPage.areCommentsEnabled()).toBeFalsy() + }) + + after(async function () { + await loginPage.logout() + }) }) + describe('P2P', function () { + let videoUrl: string + + async function goOnVideoWatchPage () { + await go(videoUrl) + await videoWatchPage.waitWatchVideoName('video') + } + + async function checkP2P (enabled: boolean) { + await goOnVideoWatchPage() + expect(await videoWatchPage.isPrivacyWarningDisplayed()).toEqual(enabled) + + await videoWatchPage.goOnAssociatedEmbed() + expect(await videoWatchPage.isEmbedWarningDisplayed()).toEqual(enabled) + } + + before(async () => { + await loginPage.loginAsRootUser() + await videoUploadPage.navigateTo() + await videoUploadPage.uploadVideo() + await videoUploadPage.setAsPublic() + await videoUploadPage.validSecondUploadStep('video') + + await videoWatchPage.waitWatchVideoName('video') + + videoUrl = await browser.getUrl() + }) + + beforeEach(async function () { + await goOnVideoWatchPage() + }) + + it('Should have P2P disabled for a logged in user', async function () { + await checkP2P(false) + }) + + it('Should have P2P disabled for anonymous users', async function () { + await loginPage.logout() + + await checkP2P(false) + }) + }) }) diff --git a/client/e2e/src/suites-local/user-settings.e2e-spec.ts b/client/e2e/src/suites-local/user-settings.e2e-spec.ts new file mode 100644 index 000000000..b87501cd1 --- /dev/null +++ b/client/e2e/src/suites-local/user-settings.e2e-spec.ts @@ -0,0 +1,82 @@ +import { AnonymousSettingsPage } from '../po/anonymous-settings.po' +import { LoginPage } from '../po/login.po' +import { MyAccountPage } from '../po/my-account.po' +import { VideoUploadPage } from '../po/video-upload.po' +import { VideoWatchPage } from '../po/video-watch.po' +import { go, isMobileDevice, isSafari, waitServerUp } from '../utils' + +describe('User settings', () => { + let videoUploadPage: VideoUploadPage + let loginPage: LoginPage + let videoWatchPage: VideoWatchPage + let myAccountPage: MyAccountPage + let anonymousSettingsPage: AnonymousSettingsPage + + before(async () => { + await waitServerUp() + + loginPage = new LoginPage() + videoUploadPage = new VideoUploadPage() + videoWatchPage = new VideoWatchPage(isMobileDevice(), isSafari()) + myAccountPage = new MyAccountPage() + anonymousSettingsPage = new AnonymousSettingsPage() + + await browser.maximizeWindow() + }) + + describe('P2P', function () { + let videoUrl: string + + async function goOnVideoWatchPage () { + await go(videoUrl) + await videoWatchPage.waitWatchVideoName('video') + } + + async function checkP2P (enabled: boolean) { + await goOnVideoWatchPage() + expect(await videoWatchPage.isPrivacyWarningDisplayed()).toEqual(enabled) + + await videoWatchPage.goOnAssociatedEmbed() + expect(await videoWatchPage.isEmbedWarningDisplayed()).toEqual(enabled) + } + + before(async () => { + await loginPage.loginAsRootUser() + await videoUploadPage.navigateTo() + await videoUploadPage.uploadVideo() + await videoUploadPage.validSecondUploadStep('video') + + await videoWatchPage.waitWatchVideoName('video') + + videoUrl = await browser.getUrl() + }) + + beforeEach(async function () { + await goOnVideoWatchPage() + }) + + it('Should have P2P enabled for a logged in user', async function () { + await checkP2P(true) + }) + + it('Should disable P2P for a logged in user', async function () { + await myAccountPage.navigateToMySettings() + await myAccountPage.clickOnP2PCheckbox() + + await checkP2P(false) + }) + + it('Should have P2P enabled for anonymous users', async function () { + await loginPage.logout() + + await checkP2P(true) + }) + + it('Should disable P2P for an anonymous user', async function () { + await anonymousSettingsPage.openSettings() + await anonymousSettingsPage.clickOnP2PCheckbox() + + await checkP2P(false) + }) + }) +}) diff --git a/client/e2e/src/suites-local/videos-list.e2e-spec.ts b/client/e2e/src/suites-local/videos-list.e2e-spec.ts index bca6018b9..ce57261b9 100644 --- a/client/e2e/src/suites-local/videos-list.e2e-spec.ts +++ b/client/e2e/src/suites-local/videos-list.e2e-spec.ts @@ -1,6 +1,6 @@ import { AdminConfigPage } from '../po/admin-config.po' import { LoginPage } from '../po/login.po' -import { MyAccountPage } from '../po/my-account' +import { MyAccountPage } from '../po/my-account.po' import { VideoListPage } from '../po/video-list.po' import { VideoSearchPage } from '../po/video-search.po' import { VideoUploadPage } from '../po/video-upload.po' diff --git a/client/e2e/src/utils/elements.ts b/client/e2e/src/utils/elements.ts index cadc46cce..315718879 100644 --- a/client/e2e/src/utils/elements.ts +++ b/client/e2e/src/utils/elements.ts @@ -1,7 +1,22 @@ -function clickOnCheckbox (name: string) { - return $(`my-peertube-checkbox[inputname=${name}] label`).click() +function getCheckbox (name: string) { + return $(`my-peertube-checkbox[inputname=${name}] label`) +} + +async function selectCustomSelect (id: string, valueLabel: string) { + await $(`[formcontrolname=${id}] .ng-arrow-wrapper`).click() + + const option = await $$(`[formcontrolname=${id}] .ng-option`).filter(async o => { + const text = await o.getText() + + return text.trimStart().startsWith(valueLabel) + }).then(options => options[0]) + + await option.waitForDisplayed() + + return option.click() } export { - clickOnCheckbox + getCheckbox, + selectCustomSelect } diff --git a/client/e2e/src/utils/hooks.ts b/client/e2e/src/utils/hooks.ts index e42c6a5d8..2f3d10fe3 100644 --- a/client/e2e/src/utils/hooks.ts +++ b/client/e2e/src/utils/hooks.ts @@ -55,6 +55,9 @@ function buildConfig (suiteFile: string = undefined) { comments_enabled: false, privacy: 4, licence: 4 + }, + p2p: { + enabled: false } } } diff --git a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.ts b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.ts index bbc81d46d..24030df3e 100644 --- a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.ts +++ b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.ts @@ -1,9 +1,8 @@ import { Component, Input, OnInit } from '@angular/core' -import { ServerService } from '@app/core' +import { ServerService, User, UserService } from '@app/core' import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' import { HTMLServerConfig, Video } from '@shared/models' -import { getStoredP2PEnabled } from '../../../../../assets/player/peertube-player-local-storage' -import { isWebRTCDisabled } from '../../../../../assets/player/utils' +import { isP2PEnabled } from '../../../../../assets/player/utils' @Component({ selector: 'my-privacy-concerns', @@ -15,33 +14,32 @@ export class PrivacyConcernsComponent implements OnInit { @Input() video: Video - display = true + display = false private serverConfig: HTMLServerConfig constructor ( - private serverService: ServerService + private serverService: ServerService, + private userService: UserService ) { } ngOnInit () { this.serverConfig = this.serverService.getHTMLConfig() - if (isWebRTCDisabled() || this.isTrackerDisabled() || this.isP2PDisabled() || this.alreadyAccepted()) { - this.display = false - } + this.userService.getAnonymousOrLoggedUser() + .subscribe(user => this.updateDisplay(user)) } acceptedPrivacyConcern () { peertubeLocalStorage.setItem(PrivacyConcernsComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true') - this.display = false - } - private isTrackerDisabled () { - return this.video.isLocal && this.serverConfig.tracker.enabled === false + this.display = false } - private isP2PDisabled () { - return getStoredP2PEnabled() === false + private updateDisplay (user: User) { + if (isP2PEnabled(this.video, this.serverConfig, user.p2pEnabled) && !this.alreadyAccepted()) { + this.display = true + } } private alreadyAccepted () { diff --git a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts index b2863fed6..fbf9a3687 100644 --- a/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts +++ b/client/src/app/+videos/+video-watch/shared/playlist/video-watch-playlist.component.ts @@ -1,16 +1,9 @@ import { Component, EventEmitter, Input, Output } from '@angular/core' import { Router } from '@angular/router' -import { - AuthService, - ComponentPagination, - HooksService, - LocalStorageService, - Notifier, - SessionStorageService, - UserService -} from '@app/core' +import { AuthService, ComponentPagination, HooksService, Notifier, SessionStorageService, UserService } from '@app/core' import { VideoPlaylist, VideoPlaylistElement, VideoPlaylistService } from '@app/shared/shared-video-playlist' -import { peertubeLocalStorage, peertubeSessionStorage } from '@root-helpers/peertube-web-storage' +import { peertubeSessionStorage } from '@root-helpers/peertube-web-storage' +import { getBoolOrDefault } from '@root-helpers/local-storage-utils' import { VideoPlaylistPrivacy } from '@shared/models' @Component({ @@ -19,8 +12,7 @@ import { VideoPlaylistPrivacy } from '@shared/models' styleUrls: [ './video-watch-playlist.component.scss' ] }) export class VideoWatchPlaylistComponent { - static LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'auto_play_video_playlist' - static SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST = 'loop_playlist' + static SESSION_STORAGE_LOOP_PLAYLIST = 'loop_playlist' @Input() playlist: VideoPlaylist @@ -47,19 +39,15 @@ export class VideoWatchPlaylistComponent { private auth: AuthService, private notifier: Notifier, private videoPlaylist: VideoPlaylistService, - private localStorageService: LocalStorageService, private sessionStorage: SessionStorageService, private router: Router ) { - // defaults to true - this.autoPlayNextVideoPlaylist = this.auth.isLoggedIn() - ? this.auth.getUser().autoPlayNextVideoPlaylist - : this.localStorageService.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false' + this.userService.getAnonymousOrLoggedUser() + .subscribe(user => this.autoPlayNextVideoPlaylist = user.autoPlayNextVideoPlaylist) this.setAutoPlayNextVideoPlaylistSwitchText() - // defaults to false - this.loopPlaylist = this.sessionStorage.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true' + this.loopPlaylist = getBoolOrDefault(this.sessionStorage.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_LOOP_PLAYLIST), false) this.setLoopPlaylistSwitchText() } @@ -201,16 +189,9 @@ export class VideoWatchPlaylistComponent { this.autoPlayNextVideoPlaylist = !this.autoPlayNextVideoPlaylist this.setAutoPlayNextVideoPlaylistSwitchText() - peertubeLocalStorage.setItem( - VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST, - this.autoPlayNextVideoPlaylist.toString() - ) + const details = { autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist } if (this.auth.isLoggedIn()) { - const details = { - autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist - } - this.userService.updateMyProfile(details) .subscribe({ next: () => { @@ -219,6 +200,8 @@ export class VideoWatchPlaylistComponent { error: err => this.notifier.error(err.message) }) + } else { + this.userService.updateMyAnonymousProfile(details) } } @@ -227,7 +210,7 @@ export class VideoWatchPlaylistComponent { this.setLoopPlaylistSwitchText() peertubeSessionStorage.setItem( - VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST, + VideoWatchPlaylistComponent.SESSION_STORAGE_LOOP_PLAYLIST, this.loopPlaylist.toString() ) } diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts index dfc296d15..97f742499 100644 --- a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts @@ -1,10 +1,9 @@ import { Observable } from 'rxjs' import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' -import { AuthService, Notifier, SessionStorageService, User, UserService } from '@app/core' +import { AuthService, Notifier, User, UserService } from '@app/core' import { Video } from '@app/shared/shared-main' import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' import { VideoPlaylist } from '@app/shared/shared-video-playlist' -import { UserLocalStorageKeys } from '@root-helpers/users' import { RecommendationInfo } from './recommendation-info.model' import { RecommendedVideosStore } from './recommended-videos.store' @@ -39,24 +38,14 @@ export class RecommendedVideosComponent implements OnInit, OnChanges { private userService: UserService, private authService: AuthService, private notifier: Notifier, - private store: RecommendedVideosStore, - private sessionStorageService: SessionStorageService + private store: RecommendedVideosStore ) { this.videos$ = this.store.recommendations$ this.hasVideos$ = this.store.hasRecommendations$ this.videos$.subscribe(videos => this.gotRecommendations.emit(videos)) - if (this.authService.isLoggedIn()) { - this.autoPlayNextVideo = this.authService.getUser().autoPlayNextVideo - } else { - this.autoPlayNextVideo = this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' - - this.sessionStorageService.watch([ UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO ]).subscribe( - () => { - this.autoPlayNextVideo = this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' - } - ) - } + this.userService.getAnonymousOrLoggedUser() + .subscribe(user => this.autoPlayNextVideo = user.autoPlayNextVideo) this.autoPlayNextVideoTooltip = $localize`When active, the next video is automatically played after the current one.` } @@ -77,13 +66,9 @@ export class RecommendedVideosComponent implements OnInit, OnChanges { } switchAutoPlayNextVideo () { - this.sessionStorageService.setItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString()) + const details = { autoPlayNextVideo: this.autoPlayNextVideo } if (this.authService.isLoggedIn()) { - const details = { - autoPlayNextVideo: this.autoPlayNextVideo - } - this.userService.updateMyProfile(details) .subscribe({ next: () => { @@ -92,6 +77,8 @@ export class RecommendedVideosComponent implements OnInit, OnChanges { error: err => this.notifier.error(err.message) }) + } else { + this.userService.updateMyAnonymousProfile(details) } } } 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 fd61bcbf0..d542f243c 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts @@ -1,5 +1,6 @@ import { Hotkey, HotkeysService } from 'angular2-hotkeys' import { forkJoin, Subscription } from 'rxjs' +import { isP2PEnabled } from 'src/assets/player/utils' import { PlatformLocation } from '@angular/common' import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' @@ -14,6 +15,7 @@ import { RestExtractor, ScreenService, ServerService, + User, UserService } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' @@ -237,31 +239,34 @@ export class VideoWatchComponent implements OnInit, OnDestroy { 'filter:api.video-watch.video.get.result' ) - forkJoin([ videoObs, this.videoCaptionService.listCaptions(videoId) ]) - .subscribe({ - next: ([ video, captionsResult ]) => { - const queryParams = this.route.snapshot.queryParams + forkJoin([ + videoObs, + this.videoCaptionService.listCaptions(videoId), + this.userService.getAnonymousOrLoggedUser() + ]).subscribe({ + next: ([ video, captionsResult, loggedInOrAnonymousUser ]) => { + const queryParams = this.route.snapshot.queryParams - const urlOptions = { - resume: queryParams.resume, + const urlOptions = { + resume: queryParams.resume, - startTime: queryParams.start, - stopTime: queryParams.stop, + startTime: queryParams.start, + stopTime: queryParams.stop, - muted: queryParams.muted, - loop: queryParams.loop, - subtitle: queryParams.subtitle, + muted: queryParams.muted, + loop: queryParams.loop, + subtitle: queryParams.subtitle, - playerMode: queryParams.mode, - peertubeLink: false - } + playerMode: queryParams.mode, + peertubeLink: false + } - this.onVideoFetched(video, captionsResult.data, urlOptions) - .catch(err => this.handleGlobalError(err)) - }, + this.onVideoFetched({ video, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) + .catch(err => this.handleGlobalError(err)) + }, - error: err => this.handleRequestError(err) - }) + error: err => this.handleRequestError(err) + }) } private loadPlaylist (playlistId: string) { @@ -323,11 +328,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.notifier.error(errorMessage) } - private async onVideoFetched ( - video: VideoDetails, - videoCaptions: VideoCaption[], + private async onVideoFetched (options: { + video: VideoDetails + videoCaptions: VideoCaption[] urlOptions: URLOptions - ) { + loggedInOrAnonymousUser: User + }) { + const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options + this.subscribeToLiveEventsIfNeeded(this.video, video) this.video = video @@ -346,7 +354,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { if (res === false) return this.location.back() } - this.buildPlayer(urlOptions) + this.buildPlayer(urlOptions, loggedInOrAnonymousUser) .catch(err => console.error('Cannot build the player', err)) this.setOpenGraphTags() @@ -359,7 +367,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) } - private async buildPlayer (urlOptions: URLOptions) { + private async buildPlayer (urlOptions: URLOptions, loggedInOrAnonymousUser: User) { // Flush old player if needed this.flushPlayer() @@ -380,6 +388,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: this.video, videoCaptions: this.videoCaptions, urlOptions, + loggedInOrAnonymousUser, user: this.user } const { playerMode, playerOptions } = await this.hooks.wrapFun( @@ -517,9 +526,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: VideoDetails videoCaptions: VideoCaption[] urlOptions: CustomizationOptions & { playerMode: PlayerMode } + loggedInOrAnonymousUser: User user?: AuthUser }) { - const { video, videoCaptions, urlOptions, user } = params + const { video, videoCaptions, urlOptions, loggedInOrAnonymousUser, user } = params const getStartTime = () => { const byUrl = urlOptions.startTime !== undefined @@ -547,6 +557,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { const options: PeertubePlayerManagerOptions = { common: { autoplay: this.isAutoplay(), + p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), + nextVideo: () => this.playNextVideoInAngularZone(), playerElement: this.playerElement, diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 95af89b19..a60138af9 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -14,7 +14,8 @@ import { ScrollService, ServerService, ThemeService, - User + User, + UserLocalStorageService } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' import { PluginService } from '@app/core/plugins/plugin.service' @@ -70,6 +71,7 @@ export class AppComponent implements OnInit, AfterViewInit { private ngbConfig: NgbConfig, private loadingBar: LoadingBarService, private scrollService: ScrollService, + private userLocalStorage: UserLocalStorageService, public menu: MenuService ) { this.ngbConfig.animation = false @@ -86,6 +88,8 @@ export class AppComponent implements OnInit, AfterViewInit { ngOnInit () { document.getElementById('incompatible-browser').className += ' browser-ok' + this.loadUser() + this.serverConfig = this.serverService.getHTMLConfig() this.hooks.runAction('action:application.init', 'common') @@ -300,4 +304,15 @@ export class AppComponent implements OnInit, AfterViewInit { }, undefined, $localize`Go to the videos upload page`) ]) } + + private loadUser () { + const tokens = this.userLocalStorage.getTokens() + if (!tokens) return + + const user = this.userLocalStorage.getLoggedInUser() + if (!user) return + + // Initialize user + this.authService.buildAuthUser(user, tokens) + } } diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index f10b37e5a..cd9665e37 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts @@ -1,13 +1,7 @@ import { Observable, of } from 'rxjs' import { map } from 'rxjs/operators' import { User } from '@app/core/users/user.model' -import { - flushUserInfoFromLocalStorage, - getUserInfoFromLocalStorage, - saveUserInfoIntoLocalStorage, - TokenOptions, - Tokens -} from '@root-helpers/users' +import { UserTokens } from '@root-helpers/users' import { hasUserRight } from '@shared/core-utils/users' import { MyUser as ServerMyUserModel, @@ -19,31 +13,15 @@ import { } from '@shared/models' export class AuthUser extends User implements ServerMyUserModel { - tokens: Tokens + tokens: UserTokens specialPlaylists: MyUserSpecialPlaylist[] canSeeVideosLink = true - static load () { - const tokens = Tokens.load() - if (!tokens) return null - - const userInfo = getUserInfoFromLocalStorage() - if (!userInfo) return null - - return new AuthUser(userInfo, tokens) - } - - static flush () { - flushUserInfoFromLocalStorage() - - Tokens.flush() - } - - constructor (userHash: Partial, hashTokens: TokenOptions) { + constructor (userHash: Partial, hashTokens: Partial) { super(userHash) - this.tokens = new Tokens(hashTokens) + this.tokens = new UserTokens(hashTokens) this.specialPlaylists = userHash.specialPlaylists } @@ -77,20 +55,6 @@ export class AuthUser extends User implements ServerMyUserModel { return user.role === UserRole.USER } - save () { - saveUserInfoIntoLocalStorage({ - id: this.id, - username: this.username, - email: this.email, - role: this.role, - nsfwPolicy: this.nsfwPolicy, - webTorrentEnabled: this.webTorrentEnabled, - autoPlayVideo: this.autoPlayVideo - }) - - this.tokens.save() - } - computeCanSeeVideosLink (quotaObservable: Observable): Observable { if (!this.isUploadDisabled()) { this.canSeeVideosLink = true diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 79239a17a..2ac88c185 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -5,7 +5,7 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { Router } from '@angular/router' import { Notifier } from '@app/core/notification/notifier.service' -import { objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' +import { objectToUrlEncoded, peertubeLocalStorage, UserTokens } from '@root-helpers/index' import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' import { environment } from '../../../environments/environment' import { RestExtractor } from '../rest/rest-extractor.service' @@ -34,6 +34,7 @@ export class AuthService { loginChangedSource: Observable userInformationLoaded = new ReplaySubject(1) + tokensRefreshed = new ReplaySubject(1) hotkeys: Hotkey[] private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) @@ -52,9 +53,6 @@ export class AuthService { this.loginChanged = new Subject() this.loginChangedSource = this.loginChanged.asObservable() - // Return null if there is nothing to load - this.user = AuthUser.load() - // Set HotKeys this.hotkeys = [ new Hotkey('m s', (event: KeyboardEvent): boolean => { @@ -76,6 +74,10 @@ export class AuthService { ] } + buildAuthUser (userInfo: Partial, tokens: UserTokens) { + this.user = new AuthUser(userInfo, tokens) + } + loadClientCredentials () { // Fetch the client_id/client_secret this.http.get(AuthService.BASE_CLIENT_URL) @@ -180,8 +182,6 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular this.user = null - AuthUser.flush() - this.setStatus(AuthStatus.LoggedOut) this.hotkeysService.remove(this.hotkeys) @@ -239,7 +239,6 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular .subscribe({ next: res => { this.user.patch(res) - this.user.save() this.userInformationLoaded.next(true) } @@ -262,7 +261,6 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular } this.user = new AuthUser(obj, hashTokens) - this.user.save() this.setStatus(AuthStatus.LoggedIn) this.userInformationLoaded.next(true) @@ -272,7 +270,7 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular private handleRefreshToken (obj: UserRefreshToken) { this.user.refreshTokens(obj.access_token, obj.refresh_token) - this.user.save() + this.tokensRefreshed.next() } private setStatus (status: AuthStatus) { diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 04be0671c..d80f95ed6 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts @@ -30,7 +30,7 @@ import { ServerConfigResolver } from './routing/server-config-resolver.service' import { ScopedTokensService } from './scoped-tokens' import { ServerService } from './server' import { ThemeService } from './theme' -import { UserService } from './users' +import { UserLocalStorageService, UserService } from './users' import { LocalStorageService, ScreenService, SessionStorageService } from './wrappers' @NgModule({ @@ -79,6 +79,7 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra RestService, UserService, + UserLocalStorageService, ScreenService, LocalStorageService, diff --git a/client/src/app/core/users/index.ts b/client/src/app/core/users/index.ts index 7b5a67bc7..e235a875b 100644 --- a/client/src/app/core/users/index.ts +++ b/client/src/app/core/users/index.ts @@ -1,2 +1,3 @@ +export * from './user-local-storage.service' export * from './user.model' export * from './user.service' diff --git a/client/src/app/core/users/user-local-storage.service.ts b/client/src/app/core/users/user-local-storage.service.ts new file mode 100644 index 000000000..85da46e0d --- /dev/null +++ b/client/src/app/core/users/user-local-storage.service.ts @@ -0,0 +1,186 @@ + +import { filter, throttleTime } from 'rxjs' +import { Injectable } from '@angular/core' +import { AuthService, AuthStatus } from '@app/core/auth' +import { UserLocalStorageKeys, UserTokens } from '@root-helpers/users' +import { getBoolOrDefault } from '@root-helpers/local-storage-utils' +import { UserRole, UserUpdateMe } from '@shared/models' +import { NSFWPolicyType } from '@shared/models/videos' +import { ServerService } from '../server' +import { LocalStorageService } from '../wrappers/storage.service' + +@Injectable() +export class UserLocalStorageService { + + constructor ( + private authService: AuthService, + private server: ServerService, + private localStorageService: LocalStorageService + ) { + this.authService.userInformationLoaded.subscribe({ + next: () => { + const user = this.authService.getUser() + + this.setLoggedInUser(user) + this.setUserInfo(user) + this.setTokens(user.tokens) + } + }) + + this.authService.loginChangedSource + .pipe(filter(status => status === AuthStatus.LoggedOut)) + .subscribe({ + next: () => { + this.flushLoggedInUser() + this.flushUserInfo() + this.flushTokens() + } + }) + + this.authService.tokensRefreshed + .subscribe({ + next: () => { + const user = this.authService.getUser() + + this.setTokens(user.tokens) + } + }) + } + + // --------------------------------------------------------------------------- + + getLoggedInUser () { + const usernameLocalStorage = this.localStorageService.getItem(UserLocalStorageKeys.USERNAME) + + if (!usernameLocalStorage) return undefined + + return { + id: parseInt(this.localStorageService.getItem(UserLocalStorageKeys.ID), 10), + username: this.localStorageService.getItem(UserLocalStorageKeys.USERNAME), + email: this.localStorageService.getItem(UserLocalStorageKeys.EMAIL), + role: parseInt(this.localStorageService.getItem(UserLocalStorageKeys.ROLE), 10) as UserRole, + + ...this.getUserInfo() + } + } + + setLoggedInUser (user: { + id: number + username: string + email: string + role: UserRole + }) { + this.localStorageService.setItem(UserLocalStorageKeys.ID, user.id.toString()) + this.localStorageService.setItem(UserLocalStorageKeys.USERNAME, user.username) + this.localStorageService.setItem(UserLocalStorageKeys.EMAIL, user.email) + this.localStorageService.setItem(UserLocalStorageKeys.ROLE, user.role.toString()) + } + + flushLoggedInUser () { + this.localStorageService.removeItem(UserLocalStorageKeys.ID) + this.localStorageService.removeItem(UserLocalStorageKeys.USERNAME) + this.localStorageService.removeItem(UserLocalStorageKeys.EMAIL) + this.localStorageService.removeItem(UserLocalStorageKeys.ROLE) + } + + // --------------------------------------------------------------------------- + + getUserInfo () { + let videoLanguages: string[] + + try { + const languagesString = this.localStorageService.getItem(UserLocalStorageKeys.VIDEO_LANGUAGES) + videoLanguages = languagesString && languagesString !== 'undefined' + ? JSON.parse(languagesString) + : null + } catch (err) { + videoLanguages = null + console.error('Cannot parse desired video languages from localStorage.', err) + } + + const htmlConfig = this.server.getHTMLConfig() + + const defaultNSFWPolicy = htmlConfig.instance.defaultNSFWPolicy + const defaultP2PEnabled = htmlConfig.defaults.p2p.enabled + + return { + nsfwPolicy: this.localStorageService.getItem(UserLocalStorageKeys.NSFW_POLICY) || defaultNSFWPolicy, + p2pEnabled: getBoolOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.P2P_ENABLED), defaultP2PEnabled), + theme: this.localStorageService.getItem(UserLocalStorageKeys.THEME) || 'instance-default', + videoLanguages, + + autoPlayVideo: getBoolOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO), true), + autoPlayNextVideo: getBoolOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_NEXT_VIDEO), false), + autoPlayNextVideoPlaylist: getBoolOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST), true) + } + } + + setUserInfo (profile: UserUpdateMe) { + const localStorageKeys: { [ id in keyof UserUpdateMe ]: string } = { + nsfwPolicy: UserLocalStorageKeys.NSFW_POLICY, + p2pEnabled: UserLocalStorageKeys.P2P_ENABLED, + autoPlayNextVideo: UserLocalStorageKeys.AUTO_PLAY_VIDEO, + autoPlayNextVideoPlaylist: UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST, + theme: UserLocalStorageKeys.THEME, + videoLanguages: UserLocalStorageKeys.VIDEO_LANGUAGES + } + + const obj = Object.keys(localStorageKeys) + .filter(key => key in profile) + .map(key => ([ localStorageKeys[key], profile[key] ])) + + for (const [ key, value ] of obj) { + try { + if (value === undefined) { + this.localStorageService.removeItem(key) + continue + } + + const localStorageValue = typeof value === 'string' + ? value + : JSON.stringify(value) + + this.localStorageService.setItem(key, localStorageValue) + } catch (err) { + console.error(`Cannot set ${key}->${value} in localStorage. Likely due to a value impossible to stringify.`, err) + } + } + } + + flushUserInfo () { + this.localStorageService.removeItem(UserLocalStorageKeys.NSFW_POLICY) + this.localStorageService.removeItem(UserLocalStorageKeys.P2P_ENABLED) + this.localStorageService.removeItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO) + this.localStorageService.removeItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST) + this.localStorageService.removeItem(UserLocalStorageKeys.THEME) + this.localStorageService.removeItem(UserLocalStorageKeys.VIDEO_LANGUAGES) + } + + listenUserInfoChange () { + return this.localStorageService.watch([ + UserLocalStorageKeys.NSFW_POLICY, + UserLocalStorageKeys.P2P_ENABLED, + UserLocalStorageKeys.AUTO_PLAY_VIDEO, + UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST, + UserLocalStorageKeys.THEME, + UserLocalStorageKeys.VIDEO_LANGUAGES + ]).pipe( + throttleTime(200), + filter(() => this.authService.isLoggedIn() !== true) + ) + } + + // --------------------------------------------------------------------------- + + getTokens () { + return UserTokens.getUserTokens(this.localStorageService) + } + + setTokens (tokens: UserTokens) { + UserTokens.saveToLocalStorage(this.localStorageService, tokens) + } + + flushTokens () { + UserTokens.flushLocalStorage(this.localStorageService) + } +} diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts index c0e5d3169..f211051ce 100644 --- a/client/src/app/core/users/user.model.ts +++ b/client/src/app/core/users/user.model.ts @@ -26,7 +26,11 @@ export class User implements UserServerModel { autoPlayVideo: boolean autoPlayNextVideo: boolean autoPlayNextVideoPlaylist: boolean - webTorrentEnabled: boolean + + p2pEnabled: boolean + // FIXME: deprecated in 4.1 + webTorrentEnabled: never + videosHistoryEnabled: boolean videoLanguages: string[] @@ -84,7 +88,7 @@ export class User implements UserServerModel { this.videoCommentsCount = hash.videoCommentsCount this.nsfwPolicy = hash.nsfwPolicy - this.webTorrentEnabled = hash.webTorrentEnabled + this.p2pEnabled = hash.p2pEnabled this.autoPlayVideo = hash.autoPlayVideo this.autoPlayNextVideo = hash.autoPlayNextVideo this.autoPlayNextVideoPlaylist = hash.autoPlayNextVideoPlaylist diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 632361e9b..a6a0474ab 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts @@ -1,11 +1,10 @@ import { SortMeta } from 'primeng/api' import { from, Observable, of } from 'rxjs' -import { catchError, concatMap, filter, first, map, shareReplay, tap, throttleTime, toArray } from 'rxjs/operators' +import { catchError, concatMap, first, map, shareReplay, tap, toArray } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { AuthService } from '@app/core/auth' import { getBytes } from '@root-helpers/bytes' -import { UserLocalStorageKeys } from '@root-helpers/users' import { ActorImage, ResultList, @@ -17,10 +16,9 @@ import { UserUpdateMe, UserVideoQuota } from '@shared/models' -import { ServerService } from '../' import { environment } from '../../../environments/environment' import { RestExtractor, RestPagination, RestService } from '../rest' -import { LocalStorageService, SessionStorageService } from '../wrappers/storage.service' +import { UserLocalStorageService } from './' import { User } from './user.model' @Injectable() @@ -33,12 +31,10 @@ export class UserService { constructor ( private authHttp: HttpClient, - private server: ServerService, private authService: AuthService, private restExtractor: RestExtractor, private restService: RestService, - private localStorageService: LocalStorageService, - private sessionStorageService: SessionStorageService + private userLocalStorageService: UserLocalStorageService ) { } hasSignupInThisSession () { @@ -73,6 +69,23 @@ export class UserService { ) } + // --------------------------------------------------------------------------- + + updateMyAnonymousProfile (profile: UserUpdateMe) { + this.userLocalStorageService.setUserInfo(profile) + } + + listenAnonymousUpdate () { + return this.userLocalStorageService.listenUserInfoChange() + .pipe(map(() => this.getAnonymousUser())) + } + + getAnonymousUser () { + return new User(this.userLocalStorageService.getUserInfo()) + } + + // --------------------------------------------------------------------------- + updateMyProfile (profile: UserUpdateMe) { const url = UserService.BASE_USERS_URL + 'me' @@ -83,53 +96,6 @@ export class UserService { ) } - updateMyAnonymousProfile (profile: UserUpdateMe) { - const localStorageKeys: { [ id in keyof UserUpdateMe ]: string } = { - nsfwPolicy: UserLocalStorageKeys.NSFW_POLICY, - webTorrentEnabled: UserLocalStorageKeys.WEBTORRENT_ENABLED, - autoPlayNextVideo: UserLocalStorageKeys.AUTO_PLAY_VIDEO, - autoPlayNextVideoPlaylist: UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST, - theme: UserLocalStorageKeys.THEME, - videoLanguages: UserLocalStorageKeys.VIDEO_LANGUAGES - } - - const obj = Object.keys(localStorageKeys) - .filter(key => key in profile) - .map(key => ([ localStorageKeys[key], profile[key] ])) - - for (const [ key, value ] of obj) { - try { - if (value === undefined) { - this.localStorageService.removeItem(key) - continue - } - - const localStorageValue = typeof value === 'string' - ? value - : JSON.stringify(value) - - this.localStorageService.setItem(key, localStorageValue) - } catch (err) { - console.error(`Cannot set ${key}->${value} in localStorage. Likely due to a value impossible to stringify.`, err) - } - } - } - - listenAnonymousUpdate () { - return this.localStorageService.watch([ - UserLocalStorageKeys.NSFW_POLICY, - UserLocalStorageKeys.WEBTORRENT_ENABLED, - UserLocalStorageKeys.AUTO_PLAY_VIDEO, - UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST, - UserLocalStorageKeys.THEME, - UserLocalStorageKeys.VIDEO_LANGUAGES - ]).pipe( - throttleTime(200), - filter(() => this.authService.isLoggedIn() !== true), - map(() => this.getAnonymousUser()) - ) - } - deleteMe () { const url = UserService.BASE_USERS_URL + 'me' @@ -287,36 +253,6 @@ export class UserService { .pipe(catchError(err => this.restExtractor.handleError(err))) } - getAnonymousUser () { - let videoLanguages: string[] - - try { - const languagesString = this.localStorageService.getItem(UserLocalStorageKeys.VIDEO_LANGUAGES) - videoLanguages = languagesString && languagesString !== 'undefined' - ? JSON.parse(languagesString) - : null - } catch (err) { - videoLanguages = null - console.error('Cannot parse desired video languages from localStorage.', err) - } - - const defaultNSFWPolicy = this.server.getHTMLConfig().instance.defaultNSFWPolicy - - return new User({ - // local storage keys - nsfwPolicy: this.localStorageService.getItem(UserLocalStorageKeys.NSFW_POLICY) || defaultNSFWPolicy, - webTorrentEnabled: this.localStorageService.getItem(UserLocalStorageKeys.WEBTORRENT_ENABLED) !== 'false', - theme: this.localStorageService.getItem(UserLocalStorageKeys.THEME) || 'instance-default', - videoLanguages, - - autoPlayNextVideoPlaylist: this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST) !== 'false', - autoPlayVideo: this.localStorageService.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO) === 'true', - - // session storage keys - autoPlayNextVideo: this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' - }) - } - getUsers (parameters: { pagination: RestPagination sort: SortMeta diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 9ea991042..48b3fdc85 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -60,7 +60,7 @@ Help share videos - + diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index d5ddc29cb..983f0a938 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts @@ -196,9 +196,9 @@ export class MenuComponent implements OnInit { toggleUseP2P () { if (!this.user) return - this.user.webTorrentEnabled = !this.user.webTorrentEnabled + this.user.p2pEnabled = !this.user.p2pEnabled - this.userService.updateMyProfile({ webTorrentEnabled: this.user.webTorrentEnabled }) + this.userService.updateMyProfile({ p2pEnabled: this.user.p2pEnabled }) .subscribe(() => this.authService.refreshUserInformation()) } diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts index 61acfb466..ad2de0f37 100644 --- a/client/src/app/shared/shared-search/search.service.ts +++ b/client/src/app/shared/shared-search/search.service.ts @@ -4,7 +4,6 @@ import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core' import { Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' -import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' import { ResultList, Video as VideoServerModel, @@ -25,11 +24,7 @@ export class SearchService { private restService: RestService, private videoService: VideoService, private playlistService: VideoPlaylistService - ) { - // Add ability to override search endpoint if the user updated this local storage key - const searchUrl = peertubeLocalStorage.getItem('search-url') - if (searchUrl) SearchService.BASE_SEARCH_URL = searchUrl - } + ) { } searchVideos (parameters: { search?: string diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.html b/client/src/app/shared/shared-user-settings/user-video-settings.component.html index bc9dd0f7f..4843f65b9 100644 --- a/client/src/app/shared/shared-user-settings/user-video-settings.component.html +++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.html @@ -38,7 +38,7 @@
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts index 0cd889a8a..7d6b69469 100644 --- a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts +++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts @@ -34,7 +34,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit, ngOnInit () { this.buildForm({ nsfwPolicy: null, - webTorrentEnabled: null, + p2pEnabled: null, autoPlayVideo: null, autoPlayNextVideo: null, videoLanguages: null @@ -48,7 +48,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit, this.form.patchValue({ nsfwPolicy: this.user.nsfwPolicy || this.defaultNSFWPolicy, - webTorrentEnabled: this.user.webTorrentEnabled, + p2pEnabled: this.user.p2pEnabled, autoPlayVideo: this.user.autoPlayVideo === true, autoPlayNextVideo: this.user.autoPlayNextVideo, videoLanguages: this.user.videoLanguages @@ -65,7 +65,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit, updateDetails (onlyKeys?: string[]) { const nsfwPolicy = this.form.value['nsfwPolicy'] - const webTorrentEnabled = this.form.value['webTorrentEnabled'] + const p2pEnabled = this.form.value['p2pEnabled'] const autoPlayVideo = this.form.value['autoPlayVideo'] const autoPlayNextVideo = this.form.value['autoPlayNextVideo'] @@ -80,7 +80,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit, let details: UserUpdateMe = { nsfwPolicy, - webTorrentEnabled, + p2pEnabled, autoPlayVideo, autoPlayNextVideo, videoLanguages diff --git a/client/src/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts index d4cbda3a9..d9dacfba5 100644 --- a/client/src/assets/player/peertube-player-local-storage.ts +++ b/client/src/assets/player/peertube-player-local-storage.ts @@ -10,14 +10,6 @@ function getStoredVolume () { return undefined } -function getStoredP2PEnabled (): boolean { - const value = getLocalStorage('webtorrent_enabled') - if (value !== null && value !== undefined) return value === 'true' - - // By default webtorrent is enabled - return true -} - function getStoredMute () { const value = getLocalStorage('mute') if (value !== null && value !== undefined) return value === 'true' @@ -123,7 +115,6 @@ function cleanupVideoWatch () { export { getStoredVolume, - getStoredP2PEnabled, getStoredMute, getStoredTheater, saveVolumeInStore, diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 3e6398f50..c27024beb 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -31,7 +31,7 @@ import { copyToClipboard } from '../../root-helpers/utils' import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' -import { getAverageBandwidthInStore, getStoredP2PEnabled, saveAverageBandwidth } from './peertube-player-local-storage' +import { getAverageBandwidthInStore, saveAverageBandwidth } from './peertube-player-local-storage' import { NextPreviousVideoButtonOptions, P2PMediaLoaderPluginOptions, @@ -86,6 +86,7 @@ export interface CommonOptions extends CustomizationOptions { onPlayerElementChange: (element: HTMLVideoElement) => void autoplay: boolean + p2pEnabled: boolean nextVideo?: () => void hasNextVideo?: () => boolean @@ -374,7 +375,7 @@ export class PeertubePlayerManager { requiredSegmentsPriority: 1, simultaneousHttpDownloads: 1, segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager, 1), - useP2P: getStoredP2PEnabled(), + useP2P: commonOptions.p2pEnabled, consumeOnly }, segments: { @@ -437,6 +438,7 @@ export class PeertubePlayerManager { const webtorrent = { autoplay, + playerRefusedP2P: commonOptions.p2pEnabled === false, videoDuration: commonOptions.videoDuration, playerElement: commonOptions.playerElement, videoFiles: webtorrentOptions.videoFiles.length !== 0 diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index ea39ac44d..824ea058b 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts @@ -137,6 +137,8 @@ type WebtorrentPluginOptions = { videoFiles: VideoFile[] startTime: number | string + + playerRefusedP2P: boolean } type P2PMediaLoaderPluginOptions = { diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index 18380d950..7e25e3067 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts @@ -1,4 +1,4 @@ -import { VideoFile } from '@shared/models' +import { HTMLServerConfig, Video, VideoFile } from '@shared/models' function toTitleCase (str: string) { return str.charAt(0).toUpperCase() + str.slice(1) @@ -8,6 +8,13 @@ function isWebRTCDisabled () { return !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false } +function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: boolean) { + if (video.isLocal && config.tracker.enabled === false) return false + if (isWebRTCDisabled()) return false + + return userP2PEnabled +} + function isIOS () { if (/iPad|iPhone|iPod/.test(navigator.platform)) { return true @@ -97,6 +104,7 @@ export { getRtcConfig, toTitleCase, isWebRTCDisabled, + isP2PEnabled, buildVideoOrPlaylistEmbed, videoFileMaxByResolution, diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index 1a1cd7f1a..2b9390206 100644 --- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts @@ -2,13 +2,7 @@ import videojs from 'video.js' import * as WebTorrent from 'webtorrent' import { timeToInt } from '@shared/core-utils' import { VideoFile } from '@shared/models' -import { - getAverageBandwidthInStore, - getStoredMute, - getStoredP2PEnabled, - getStoredVolume, - saveAverageBandwidth -} from '../peertube-player-local-storage' +import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../peertube-player-local-storage' import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings' import { getRtcConfig, isIOS, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' import { PeertubeChunkStore } from './peertube-chunk-store' @@ -74,9 +68,10 @@ class WebTorrentPlugin extends Plugin { this.startTime = timeToInt(options.startTime) - // Disable auto play on iOS + // Custom autoplay handled by webtorrent because we lazy play the video this.autoplay = options.autoplay - this.playerRefusedP2P = !getStoredP2PEnabled() + + this.playerRefusedP2P = options.playerRefusedP2P this.videoFiles = options.videoFiles this.videoDuration = options.videoDuration diff --git a/client/src/root-helpers/index.ts b/client/src/root-helpers/index.ts index 63d55d7ef..aa3b442dd 100644 --- a/client/src/root-helpers/index.ts +++ b/client/src/root-helpers/index.ts @@ -1,6 +1,7 @@ export * from './users' export * from './bytes' export * from './images' +export * from './local-storage-utils' export * from './peertube-web-storage' export * from './utils' export * from './plugins-manager' diff --git a/client/src/root-helpers/local-storage-utils.ts b/client/src/root-helpers/local-storage-utils.ts new file mode 100644 index 000000000..c2b3f9035 --- /dev/null +++ b/client/src/root-helpers/local-storage-utils.ts @@ -0,0 +1,10 @@ +function getBoolOrDefault (value: string, defaultValue: boolean) { + if (value === 'true') return true + if (value === 'false') return false + + return defaultValue +} + +export { + getBoolOrDefault +} diff --git a/client/src/root-helpers/users/index.ts b/client/src/root-helpers/users/index.ts index 8fbaca9e3..2b11d0b7e 100644 --- a/client/src/root-helpers/users/index.ts +++ b/client/src/root-helpers/users/index.ts @@ -1,3 +1,2 @@ export * from './user-local-storage-keys' -export * from './user-local-storage-manager' export * from './user-tokens' diff --git a/client/src/root-helpers/users/user-local-storage-keys.ts b/client/src/root-helpers/users/user-local-storage-keys.ts index 5f915899c..c3934ae3c 100644 --- a/client/src/root-helpers/users/user-local-storage-keys.ts +++ b/client/src/root-helpers/users/user-local-storage-keys.ts @@ -1,15 +1,25 @@ export const UserLocalStorageKeys = { ID: 'id', + USERNAME: 'username', ROLE: 'role', EMAIL: 'email', + VIDEOS_HISTORY_ENABLED: 'videos-history-enabled', - USERNAME: 'username', NSFW_POLICY: 'nsfw_policy', - WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled', + P2P_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled', + AUTO_PLAY_VIDEO: 'auto_play_video', - SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO: 'auto_play_next_video', + AUTO_PLAY_NEXT_VIDEO: 'auto_play_next_video', AUTO_PLAY_VIDEO_PLAYLIST: 'auto_play_video_playlist', + THEME: 'theme', LAST_ACTIVE_THEME: 'last_active_theme', + VIDEO_LANGUAGES: 'video_languages' } + +export const UserTokenLocalStorageKeys = { + ACCESS_TOKEN: 'access_token', + REFRESH_TOKEN: 'refresh_token', + TOKEN_TYPE: 'token_type' +} diff --git a/client/src/root-helpers/users/user-local-storage-manager.ts b/client/src/root-helpers/users/user-local-storage-manager.ts deleted file mode 100644 index c75cea127..000000000 --- a/client/src/root-helpers/users/user-local-storage-manager.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { NSFWPolicyType, UserRole } from '@shared/models' -import { peertubeLocalStorage } from '../peertube-web-storage' -import { UserLocalStorageKeys } from './user-local-storage-keys' - -function getUserInfoFromLocalStorage () { - const usernameLocalStorage = peertubeLocalStorage.getItem(UserLocalStorageKeys.USERNAME) - - if (!usernameLocalStorage) return undefined - - return { - id: parseInt(peertubeLocalStorage.getItem(UserLocalStorageKeys.ID), 10), - username: peertubeLocalStorage.getItem(UserLocalStorageKeys.USERNAME), - email: peertubeLocalStorage.getItem(UserLocalStorageKeys.EMAIL), - role: parseInt(peertubeLocalStorage.getItem(UserLocalStorageKeys.ROLE), 10) as UserRole, - nsfwPolicy: peertubeLocalStorage.getItem(UserLocalStorageKeys.NSFW_POLICY) as NSFWPolicyType, - webTorrentEnabled: peertubeLocalStorage.getItem(UserLocalStorageKeys.WEBTORRENT_ENABLED) === 'true', - autoPlayVideo: peertubeLocalStorage.getItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO) === 'true', - videosHistoryEnabled: peertubeLocalStorage.getItem(UserLocalStorageKeys.VIDEOS_HISTORY_ENABLED) === 'true' - } -} - -function flushUserInfoFromLocalStorage () { - peertubeLocalStorage.removeItem(UserLocalStorageKeys.ID) - peertubeLocalStorage.removeItem(UserLocalStorageKeys.USERNAME) - peertubeLocalStorage.removeItem(UserLocalStorageKeys.EMAIL) - peertubeLocalStorage.removeItem(UserLocalStorageKeys.ROLE) - peertubeLocalStorage.removeItem(UserLocalStorageKeys.NSFW_POLICY) - peertubeLocalStorage.removeItem(UserLocalStorageKeys.WEBTORRENT_ENABLED) - peertubeLocalStorage.removeItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO) - peertubeLocalStorage.removeItem(UserLocalStorageKeys.VIDEOS_HISTORY_ENABLED) -} - -function saveUserInfoIntoLocalStorage (info: { - id: number - username: string - email: string - role: UserRole - nsfwPolicy: NSFWPolicyType - webTorrentEnabled: boolean - autoPlayVideo: boolean -}) { - peertubeLocalStorage.setItem(UserLocalStorageKeys.ID, info.id.toString()) - peertubeLocalStorage.setItem(UserLocalStorageKeys.USERNAME, info.username) - peertubeLocalStorage.setItem(UserLocalStorageKeys.EMAIL, info.email) - peertubeLocalStorage.setItem(UserLocalStorageKeys.ROLE, info.role.toString()) - peertubeLocalStorage.setItem(UserLocalStorageKeys.NSFW_POLICY, info.nsfwPolicy.toString()) - peertubeLocalStorage.setItem(UserLocalStorageKeys.WEBTORRENT_ENABLED, JSON.stringify(info.webTorrentEnabled)) - peertubeLocalStorage.setItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO, JSON.stringify(info.autoPlayVideo)) -} - -export { - getUserInfoFromLocalStorage, - saveUserInfoIntoLocalStorage, - flushUserInfoFromLocalStorage -} diff --git a/client/src/root-helpers/users/user-tokens.ts b/client/src/root-helpers/users/user-tokens.ts index d42e1c8f3..a6d614cb7 100644 --- a/client/src/root-helpers/users/user-tokens.ts +++ b/client/src/root-helpers/users/user-tokens.ts @@ -1,46 +1,11 @@ -import { peertubeLocalStorage } from '../peertube-web-storage' - -export type TokenOptions = { - accessToken: string - refreshToken: string - tokenType: string -} - -// Private class only used by User -export class Tokens { - private static KEYS = { - ACCESS_TOKEN: 'access_token', - REFRESH_TOKEN: 'refresh_token', - TOKEN_TYPE: 'token_type' - } +import { UserTokenLocalStorageKeys } from './user-local-storage-keys' +export class UserTokens { accessToken: string refreshToken: string tokenType: string - static load () { - const accessTokenLocalStorage = peertubeLocalStorage.getItem(this.KEYS.ACCESS_TOKEN) - const refreshTokenLocalStorage = peertubeLocalStorage.getItem(this.KEYS.REFRESH_TOKEN) - const tokenTypeLocalStorage = peertubeLocalStorage.getItem(this.KEYS.TOKEN_TYPE) - - if (accessTokenLocalStorage && refreshTokenLocalStorage && tokenTypeLocalStorage) { - return new Tokens({ - accessToken: accessTokenLocalStorage, - refreshToken: refreshTokenLocalStorage, - tokenType: tokenTypeLocalStorage - }) - } - - return null - } - - static flush () { - peertubeLocalStorage.removeItem(this.KEYS.ACCESS_TOKEN) - peertubeLocalStorage.removeItem(this.KEYS.REFRESH_TOKEN) - peertubeLocalStorage.removeItem(this.KEYS.TOKEN_TYPE) - } - - constructor (hash?: TokenOptions) { + constructor (hash?: Partial) { if (hash) { this.accessToken = hash.accessToken this.refreshToken = hash.refreshToken @@ -53,9 +18,29 @@ export class Tokens { } } - save () { - peertubeLocalStorage.setItem(Tokens.KEYS.ACCESS_TOKEN, this.accessToken) - peertubeLocalStorage.setItem(Tokens.KEYS.REFRESH_TOKEN, this.refreshToken) - peertubeLocalStorage.setItem(Tokens.KEYS.TOKEN_TYPE, this.tokenType) + static getUserTokens (localStorage: Pick) { + const accessTokenLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.ACCESS_TOKEN) + const refreshTokenLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.REFRESH_TOKEN) + const tokenTypeLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.TOKEN_TYPE) + + if (!accessTokenLocalStorage || !refreshTokenLocalStorage || !tokenTypeLocalStorage) return null + + return new UserTokens({ + accessToken: accessTokenLocalStorage, + refreshToken: refreshTokenLocalStorage, + tokenType: tokenTypeLocalStorage + }) + } + + static saveToLocalStorage (localStorage: Pick, tokens: UserTokens) { + localStorage.setItem(UserTokenLocalStorageKeys.ACCESS_TOKEN, tokens.accessToken) + localStorage.setItem(UserTokenLocalStorageKeys.REFRESH_TOKEN, tokens.refreshToken) + localStorage.setItem(UserTokenLocalStorageKeys.TOKEN_TYPE, tokens.tokenType) + } + + static flushLocalStorage (localStorage: Pick) { + localStorage.removeItem(UserTokenLocalStorageKeys.ACCESS_TOKEN) + localStorage.removeItem(UserTokenLocalStorageKeys.REFRESH_TOKEN) + localStorage.removeItem(UserTokenLocalStorageKeys.TOKEN_TYPE) } } diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 874be580d..94f1096b7 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -7,6 +7,7 @@ import { OAuth2ErrorCode, ResultList, UserRefreshToken, + Video, VideoCaption, VideoDetails, VideoPlaylist, @@ -16,9 +17,11 @@ import { import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager' import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings' import { TranslationsManager } from '../../assets/player/translations-manager' +import { isP2PEnabled } from '../../assets/player/utils' +import { getBoolOrDefault } from '../../root-helpers/local-storage-utils' import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage' import { PluginsManager } from '../../root-helpers/plugins-manager' -import { Tokens } from '../../root-helpers/users' +import { UserLocalStorageKeys, UserTokens } from '../../root-helpers/users' import { objectToUrlEncoded } from '../../root-helpers/utils' import { RegisterClientHelpers } from '../../types/register-client-option.model' import { PeerTubeEmbedApi } from './embed-api' @@ -48,7 +51,7 @@ export class PeerTubeEmbed { mode: PlayerMode scope = 'peertube' - userTokens: Tokens + userTokens: UserTokens headers = new Headers() LOCAL_STORAGE_OAUTH_CLIENT_KEYS = { CLIENT_ID: 'client_id', @@ -118,7 +121,7 @@ export class PeerTubeEmbed { return res.json() }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => { if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) { - Tokens.flush() + UserTokens.flushLocalStorage(peertubeLocalStorage) this.removeTokensFromHeaders() return resolve() @@ -126,7 +129,7 @@ export class PeerTubeEmbed { this.userTokens.accessToken = obj.access_token this.userTokens.refreshToken = obj.refresh_token - this.userTokens.save() + UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens) this.setHeadersFromTokens() @@ -138,7 +141,7 @@ export class PeerTubeEmbed { return refreshingTokenPromise .catch(() => { - Tokens.flush() + UserTokens.flushLocalStorage(peertubeLocalStorage) this.removeTokensFromHeaders() }).then(() => fetch(url, { @@ -258,7 +261,7 @@ export class PeerTubeEmbed { } async init () { - this.userTokens = Tokens.load() + this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage) await this.initCore() } @@ -515,6 +518,8 @@ export class PeerTubeEmbed { muted: this.muted, loop: this.loop, + p2pEnabled: this.isP2PEnabled(videoInfo), + captions: videoCaptions.length !== 0, subtitle: this.subtitle, @@ -669,7 +674,7 @@ export class PeerTubeEmbed { const title = this.title ? videoInfo.name : undefined - const description = this.warningTitle && (!videoInfo.isLocal || this.config.tracker.enabled) + const description = this.warningTitle && this.isP2PEnabled(videoInfo) ? '' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '' : undefined @@ -784,6 +789,15 @@ export class PeerTubeEmbed { translate: (value: string) => Promise.resolve(peertubeTranslate(value, translations)) } } + + private isP2PEnabled (video: Video) { + const userP2PEnabled = getBoolOrDefault( + peertubeLocalStorage.getItem(UserLocalStorageKeys.P2P_ENABLED), + this.config.defaults.p2p.enabled + ) + + return isP2PEnabled(video, this.config, userP2PEnabled) + } } PeerTubeEmbed.main() diff --git a/config/default.yaml b/config/default.yaml index fbe0dbbfb..421c19569 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -92,6 +92,11 @@ defaults: # No licence by default licence: null + p2p: + # Enable P2P by default + # Can be enabled/disabled by anonymous users and logged in users + enabled: true + # From the project root directory storage: tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... @@ -216,7 +221,7 @@ security: enabled: true tracker: - # If you disable the tracker, you disable the P2P aspect of PeerTube + # If you disable the tracker, you disable the P2P on your PeerTube instance enabled: true # Only handle requests on your videos # If you set this to false it means you have a public tracker diff --git a/config/production.yaml.example b/config/production.yaml.example index 6363a5179..13219fd5d 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -90,6 +90,11 @@ defaults: # No licence by default licence: null + p2p: + # Enable P2P by default + # Can be enabled/disabled by anonymous users and logged in users + enabled: true + # From the project root directory storage: tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 11d3525e4..f3b4508d9 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts @@ -183,6 +183,7 @@ async function createUser (req: express.Request, res: express.Response) { password: body.password, email: body.email, nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, + p2pEnabled: CONFIG.DEFAULTS.P2P.ENABLED, autoPlayVideo: true, role: body.role, videoQuota: body.videoQuota, @@ -232,6 +233,7 @@ async function registerUser (req: express.Request, res: express.Response) { password: body.password, email: body.email, nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, + p2pEnabled: CONFIG.DEFAULTS.P2P.ENABLED, autoPlayVideo: true, role: UserRole.USER, videoQuota: CONFIG.USER.VIDEO_QUOTA, diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 6bacdbbb6..1125771d4 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts @@ -197,7 +197,7 @@ async function updateMe (req: express.Request, res: express.Response) { const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly)[] = [ 'password', 'nsfwPolicy', - 'webTorrentEnabled', + 'p2pEnabled', 'autoPlayVideo', 'autoPlayNextVideo', 'autoPlayNextVideoPlaylist', @@ -213,6 +213,12 @@ async function updateMe (req: express.Request, res: express.Response) { if (body[key] !== undefined) user.set(key, body[key]) } + if (body.p2pEnabled !== undefined) { + user.set('p2pEnabled', body.p2pEnabled) + } else if (body.webTorrentEnabled !== undefined) { // FIXME: deprecated in 4.1 + user.set('p2pEnabled', body.webTorrentEnabled) + } + if (body.email !== undefined) { if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { user.pendingEmail = body.email diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index f52c60b60..badf171d2 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts @@ -49,7 +49,7 @@ function isUserNSFWPolicyValid (value: any) { return exists(value) && nsfwPolicies.includes(value) } -function isUserWebTorrentEnabledValid (value: any) { +function isUserP2PEnabledValid (value: any) { return isBooleanValid(value) } @@ -109,7 +109,7 @@ export { isUserAdminFlagsValid, isUserEmailVerifiedValid, isUserNSFWPolicyValid, - isUserWebTorrentEnabledValid, + isUserP2PEnabledValid, isUserAutoPlayVideoValid, isUserAutoPlayNextVideoValid, isUserAutoPlayNextVideoPlaylistValid, diff --git a/server/initializers/config.ts b/server/initializers/config.ts index e3e8c426e..a6ea6d888 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -78,6 +78,9 @@ const CONFIG = { COMMENTS_ENABLED: config.get('defaults.publish.comments_enabled'), PRIVACY: config.get('defaults.publish.privacy'), LICENCE: config.get('defaults.publish.licence') + }, + P2P: { + ENABLED: config.get('defaults.p2p.enabled') } }, diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 026c715c2..258ccdb51 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -25,7 +25,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 670 +const LAST_MIGRATION_VERSION = 675 // --------------------------------------------------------------------------- diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 75daeb5d8..19adaf177 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -144,6 +144,7 @@ async function createOAuthAdminIfNotExist () { role, verified: true, nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, + p2pEnabled: CONFIG.DEFAULTS.P2P.ENABLED, videoQuota: -1, videoQuotaDaily: -1 } diff --git a/server/initializers/migrations/0675-p2p-enabled.ts b/server/initializers/migrations/0675-p2p-enabled.ts new file mode 100644 index 000000000..b4f53381e --- /dev/null +++ b/server/initializers/migrations/0675-p2p-enabled.ts @@ -0,0 +1,21 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize + db: any +}): Promise { + await utils.queryInterface.renameColumn('user', 'webTorrentEnabled', 'p2pEnabled') + + await utils.sequelize.query('ALTER TABLE "user" ALTER COLUMN "p2pEnabled" DROP DEFAULT') +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index f2ef0a78a..754bee36d 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts @@ -226,6 +226,7 @@ async function createUserFromExternal (pluginAuth: string, options: { password: null, email: options.email, nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, + p2pEnabled: CONFIG.DEFAULTS.P2P.ENABLED, autoPlayVideo: true, role: options.role, videoQuota: CONFIG.USER.VIDEO_QUOTA, diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts index 8aea4cd6d..d759f85e1 100644 --- a/server/lib/server-config-manager.ts +++ b/server/lib/server-config-manager.ts @@ -61,6 +61,9 @@ class ServerConfigManager { commentsEnabled: CONFIG.DEFAULTS.PUBLISH.COMMENTS_ENABLED, privacy: CONFIG.DEFAULTS.PUBLISH.PRIVACY, licence: CONFIG.DEFAULTS.PUBLISH.LICENCE + }, + p2p: { + enabled: CONFIG.DEFAULTS.P2P.ENABLED } }, diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 7a6b2ce57..bc6007c6d 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -15,6 +15,7 @@ import { isUserDisplayNameValid, isUserNoModal, isUserNSFWPolicyValid, + isUserP2PEnabledValid, isUserPasswordValid, isUserPasswordValidOrEmpty, isUserRoleValid, @@ -239,6 +240,9 @@ const usersUpdateMeValidator = [ body('autoPlayVideo') .optional() .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), + body('p2pEnabled') + .optional() + .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'), body('videoLanguages') .optional() .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'), diff --git a/server/models/user/user.ts b/server/models/user/user.ts index b56f37e55..88c3ff528 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts @@ -55,7 +55,7 @@ import { isUserVideoQuotaDailyValid, isUserVideoQuotaValid, isUserVideosHistoryEnabledValid, - isUserWebTorrentEnabledValid + isUserP2PEnabledValid } from '../../helpers/custom-validators/users' import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' @@ -267,10 +267,9 @@ export class UserModel extends Model>> { nsfwPolicy: NSFWPolicyType @AllowNull(false) - @Default(true) - @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled')) + @Is('p2pEnabled', value => throwIfNotValid(value, isUserP2PEnabledValid, 'P2P enabled')) @Column - webTorrentEnabled: boolean + p2pEnabled: boolean @AllowNull(false) @Default(true) @@ -892,7 +891,11 @@ export class UserModel extends Model>> { emailVerified: this.emailVerified, nsfwPolicy: this.nsfwPolicy, - webTorrentEnabled: this.webTorrentEnabled, + + // FIXME: deprecated in 4.1 + webTorrentEnabled: this.p2pEnabled, + p2pEnabled: this.p2pEnabled, + videosHistoryEnabled: this.videosHistoryEnabled, autoPlayVideo: this.autoPlayVideo, autoPlayNextVideo: this.autoPlayNextVideo, diff --git a/server/tests/api/server/config-defaults.ts b/server/tests/api/server/config-defaults.ts index 3dff7bfb7..340d4b44b 100644 --- a/server/tests/api/server/config-defaults.ts +++ b/server/tests/api/server/config-defaults.ts @@ -21,18 +21,7 @@ describe('Test config defaults', function () { before(async function () { this.timeout(30000) - const overrideConfig = { - defaults: { - publish: { - comments_enabled: false, - download_enabled: false, - privacy: VideoPrivacy.INTERNAL, - licence: 4 - } - } - } - - server = await createSingleServer(1, overrideConfig) + server = await createSingleServer(1) await setAccessTokensToServers([ server ]) await setDefaultVideoChannel([ server ]) @@ -40,6 +29,23 @@ describe('Test config defaults', function () { }) describe('Default publish values', function () { + + before(async function () { + const overrideConfig = { + defaults: { + publish: { + comments_enabled: false, + download_enabled: false, + privacy: VideoPrivacy.INTERNAL, + licence: 4 + } + } + } + + await server.kill() + await server.run(overrideConfig) + }) + const attributes = { name: 'video', downloadEnabled: undefined, @@ -117,6 +123,45 @@ describe('Test config defaults', function () { }) }) + describe('Default P2P values', function () { + + before(async function () { + const overrideConfig = { + defaults: { + p2p: { + enabled: false + } + } + } + + await server.kill() + await server.run(overrideConfig) + }) + + it('Should not have P2P enabled', async function () { + const config = await server.config.getConfig() + + expect(config.defaults.p2p.enabled).to.be.false + }) + + it('Should create a user with this default setting', async function () { + await server.users.create({ username: 'user_p2p_1' }) + const userToken = await server.login.getAccessToken('user_p2p_1') + + const { p2pEnabled } = await server.users.getMyInfo({ token: userToken }) + expect(p2pEnabled).to.be.false + }) + + it('Should register a user with this default setting', async function () { + await server.users.register({ username: 'user_p2p_2' }) + + const userToken = await server.login.getAccessToken('user_p2p_2') + + const { p2pEnabled } = await server.users.getMyInfo({ token: userToken }) + expect(p2pEnabled).to.be.false + }) + }) + after(async function () { await cleanupTests([ server ]) }) diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index 748f4cd35..c132d99ea 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts @@ -292,7 +292,7 @@ describe('Test follows', function () { }) it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { - this.timeout(60000) + this.timeout(120000) await servers[1].videos.upload({ attributes: { name: 'server2' } }) await servers[2].videos.upload({ attributes: { name: 'server3' } }) diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 6c41e7d56..f00cbab5a 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -559,6 +559,28 @@ describe('Test users', function () { expect(user.autoPlayNextVideo).to.be.true }) + it('Should be able to change the p2p attribute', async function () { + { + await server.users.updateMe({ + token: userToken, + webTorrentEnabled: false + }) + + const user = await server.users.getMyInfo({ token: userToken }) + expect(user.p2pEnabled).to.be.false + } + + { + await server.users.updateMe({ + token: userToken, + p2pEnabled: true + }) + + const user = await server.users.getMyInfo({ token: userToken }) + expect(user.p2pEnabled).to.be.true + } + }) + it('Should be able to change the email attribute', async function () { await server.users.updateMe({ token: userToken, diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 9c3dcd6d3..71540e603 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts @@ -55,6 +55,10 @@ export interface ServerConfig { privacy: VideoPrivacy licence: number } + + p2p: { + enabled: boolean + } } webadmin: { diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts index 6d7df38fb..e664e44b5 100644 --- a/shared/models/users/user-update-me.model.ts +++ b/shared/models/users/user-update-me.model.ts @@ -5,7 +5,10 @@ export interface UserUpdateMe { description?: string nsfwPolicy?: NSFWPolicyType + // FIXME: deprecated in favour of p2pEnabled in 4.1 webTorrentEnabled?: boolean + p2pEnabled?: boolean + autoPlayVideo?: boolean autoPlayNextVideo?: boolean autoPlayNextVideoPlaylist?: boolean diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 78870c556..63c5c8a92 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts @@ -20,7 +20,11 @@ export interface User { autoPlayVideo: boolean autoPlayNextVideo: boolean autoPlayNextVideoPlaylist: boolean + + // @deprecated in favour of p2pEnabled webTorrentEnabled: boolean + p2pEnabled: boolean + videosHistoryEnabled: boolean videoLanguages: string[] diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 1d5581072..7b6e8a1e4 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -6679,7 +6679,7 @@ components: type: integer description: The user daily video quota in bytes example: -1 - webtorrentEnabled: + p2pEnabled: type: boolean description: Enable P2P in the player UserWithStats: @@ -6780,7 +6780,7 @@ components: - 'true' - 'false' - both - webTorrentEnabled: + p2pEnabled: type: boolean description: whether to enable P2P in the player or not autoPlayVideo: -- 2.41.0