aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.github/actions/reusable-deploy/action.yml46
-rw-r--r--.github/actions/reusable-prepare-peertube-build/action.yml31
-rw-r--r--.github/actions/reusable-prepare-peertube-run/action.yml16
-rw-r--r--.github/workflows/benchmark.yml69
-rw-r--r--.github/workflows/codeql.yml68
-rw-r--r--.github/workflows/codeql/codeql-config.yml4
-rw-r--r--.github/workflows/docker.yml70
-rw-r--r--.github/workflows/nightly.yml33
-rw-r--r--.github/workflows/stats.yml50
-rw-r--r--.github/workflows/test.yml27
-rw-r--r--.gitlab-ci.yml84
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html128
-rw-r--r--client/src/app/+about/about.component.html6
-rw-r--r--client/src/app/+accounts/accounts.component.html6
-rw-r--r--client/src/app/+accounts/accounts.component.scss16
-rw-r--r--client/src/app/+accounts/accounts.component.ts9
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html32
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts12
-rw-r--r--client/src/app/+login/login.component.html3
-rw-r--r--client/src/app/+login/login.component.ts5
-rw-r--r--client/src/app/+plugin-pages/index.ts3
-rw-r--r--client/src/app/+plugin-pages/plugin-pages-routing.module.ts19
-rw-r--r--client/src/app/+plugin-pages/plugin-pages.component.html1
-rw-r--r--client/src/app/+plugin-pages/plugin-pages.component.ts31
-rw-r--r--client/src/app/+plugin-pages/plugin-pages.module.ts21
-rw-r--r--client/src/app/+search/search-filters.component.html25
-rw-r--r--client/src/app/+search/search-filters.component.ts12
-rw-r--r--client/src/app/+search/search.component.ts23
-rw-r--r--client/src/app/+video-channels/video-channels.component.html6
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts14
-rw-r--r--client/src/app/+video-channels/video-channels.module.ts6
-rw-r--r--client/src/app/app-routing.module.ts6
-rw-r--r--client/src/app/core/plugins/plugin.service.ts31
-rw-r--r--client/src/app/core/routing/custom-reuse-strategy.ts2
-rw-r--r--client/src/app/menu/menu.component.html9
-rw-r--r--client/src/app/menu/menu.component.ts10
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts9
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.html4
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.scss9
-rw-r--r--client/src/app/shared/shared-moderation/account-block-badges.component.ts11
-rw-r--r--client/src/app/shared/shared-moderation/blocklist.service.ts20
-rw-r--r--client/src/app/shared/shared-moderation/index.ts1
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts7
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts4
-rw-r--r--client/src/app/shared/shared-search/advanced-search.model.ts30
-rw-r--r--client/src/app/shared/shared-search/search.service.ts14
-rw-r--r--client/src/assets/player/p2p-media-loader/hls-plugin.ts6
-rw-r--r--client/src/assets/player/peertube-plugin.ts52
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts2
-rw-r--r--client/src/root-helpers/plugins-manager.ts35
-rw-r--r--client/src/sass/player/peertube-skin.scss27
-rw-r--r--client/src/standalone/videos/embed.ts2
-rw-r--r--client/src/types/register-client-option.model.ts9
-rw-r--r--config/default.yaml6
-rw-r--r--config/production.yaml.example6
-rw-r--r--config/test.yaml1
-rw-r--r--package.json3
-rwxr-xr-xscripts/clean/server/test.sh6
-rwxr-xr-xscripts/prune-storage.ts10
-rwxr-xr-xscripts/update-host.ts6
-rw-r--r--server/controllers/api/abuse.ts15
-rw-r--r--server/controllers/api/blocklist.ts108
-rw-r--r--server/controllers/api/config.ts12
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/plugins.ts7
-rw-r--r--server/controllers/api/server/stats.ts4
-rw-r--r--server/controllers/api/users/my-subscriptions.ts21
-rw-r--r--server/controllers/api/videos/import.ts17
-rw-r--r--server/controllers/api/videos/live.ts4
-rw-r--r--server/controllers/api/videos/update.ts32
-rw-r--r--server/controllers/api/videos/upload.ts12
-rw-r--r--server/helpers/actors.ts17
-rw-r--r--server/helpers/decache.ts78
-rw-r--r--server/helpers/webtorrent.ts18
-rw-r--r--server/initializers/checker-before-init.ts1
-rw-r--r--server/initializers/config.ts9
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/activitypub/process/process-flag.ts9
-rw-r--r--server/lib/blocklist.ts4
-rw-r--r--server/lib/client-html.ts4
-rw-r--r--server/lib/job-queue/handlers/move-to-object-storage.ts4
-rw-r--r--server/lib/moderation.ts29
-rw-r--r--server/lib/notifier/shared/comment/comment-mention.ts4
-rw-r--r--server/lib/plugins/plugin-manager.ts4
-rw-r--r--server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts35
-rw-r--r--server/lib/server-config-manager.ts5
-rw-r--r--server/lib/uploadx.ts10
-rw-r--r--server/middlewares/error.ts10
-rw-r--r--server/middlewares/validators/blocklist.ts29
-rw-r--r--server/middlewares/validators/plugins.ts6
-rw-r--r--server/models/account/account-blocklist.ts40
-rw-r--r--server/models/server/server-blocklist.ts19
-rw-r--r--server/models/video/video-playlist-element.ts2
-rw-r--r--server/models/video/video-streaming-playlist.ts9
-rw-r--r--server/tests/api/check-params/blocklist.ts72
-rw-r--r--server/tests/api/check-params/config.ts12
-rw-r--r--server/tests/api/check-params/plugins.ts2
-rw-r--r--server/tests/api/moderation/blocklist.ts107
-rw-r--r--server/tests/api/notifications/moderation-notifications.ts84
-rw-r--r--server/tests/api/notifications/user-notifications.ts2
-rw-r--r--server/tests/api/redundancy/redundancy.ts16
-rw-r--r--server/tests/api/server/config.ts18
-rw-r--r--server/tests/api/server/email.ts2
-rw-r--r--server/tests/api/videos/video-privacy.ts13
-rw-r--r--server/tests/cli/peertube.ts19
-rw-r--r--server/tests/cli/prune-storage.ts49
-rw-r--r--server/tests/fixtures/peertube-plugin-test/main.js22
-rw-r--r--server/tests/plugins/external-auth.ts2
-rw-r--r--server/tests/plugins/filter-hooks.ts69
-rw-r--r--server/tools/peertube-plugins.ts3
-rw-r--r--shared/extra-utils/server/config-command.ts12
-rw-r--r--shared/extra-utils/server/plugins-command.ts5
-rw-r--r--shared/extra-utils/server/server.ts12
-rw-r--r--shared/extra-utils/users/blocklist-command.ts25
-rw-r--r--shared/extra-utils/videos/videos.ts1
-rw-r--r--shared/models/moderation/block-status.model.ts15
-rw-r--r--shared/models/moderation/index.ts1
-rw-r--r--shared/models/plugins/client/index.ts1
-rw-r--r--shared/models/plugins/client/plugin-selector-id.type.ts11
-rw-r--r--shared/models/plugins/client/register-client-route.model.ts7
-rw-r--r--shared/models/plugins/client/register-client-settings-script.model.ts2
-rw-r--r--shared/models/plugins/server/api/install-plugin.model.ts1
-rw-r--r--shared/models/plugins/server/server-hook.model.ts9
-rw-r--r--shared/models/server/custom-config.model.ts14
-rw-r--r--shared/models/server/server-config.model.ts6
-rw-r--r--support/doc/api/openapi.yaml56
-rw-r--r--support/doc/development/ci.md40
-rw-r--r--support/doc/plugins/guide.md4
-rw-r--r--support/doc/production.md114
-rw-r--r--support/docker/production/Dockerfile.bullseye4
-rw-r--r--yarn.lock20
132 files changed, 1986 insertions, 666 deletions
diff --git a/.github/actions/reusable-deploy/action.yml b/.github/actions/reusable-deploy/action.yml
new file mode 100644
index 000000000..bc69a2e43
--- /dev/null
+++ b/.github/actions/reusable-deploy/action.yml
@@ -0,0 +1,46 @@
1name: "Reusable deploy on builds.joinpeertube.org"
2
3description: "Reusable deploy on builds.joinpeertube.org"
4
5inputs:
6 source:
7 required: true
8 description: "Source file/files/directory/directories to deploy"
9 destination:
10 required: true
11 description: "Destination directory on builds.joinpeertube.org"
12 knownHosts:
13 required: true
14 description: "Known hosts"
15 deployKey:
16 required: true
17 description: "Deploy key"
18 deployUser:
19 required: true
20 description: "Deploy user"
21 deployHost:
22 required: true
23 description: "Deploy host"
24
25
26runs:
27 using: "composite"
28
29 steps:
30 - name: "Deploy"
31 shell: bash
32 run: |
33 mkdir -p ~/.ssh
34 chmod 700 ~/.ssh
35
36 echo "Adding ssh key to known hosts"
37 echo -e "${{ inputs.knownHosts }}" > ~/.ssh/known_hosts;
38
39 eval `ssh-agent -s`
40
41 echo "Adding ssh deploy key"
42 ssh-add <(echo "${{ inputs.deployKey }}");
43
44 echo "Uploading files"
45
46 scp ${{ inputs.source }} ${{ inputs.deployUser }}@${{ inputs.deployHost }}:../../web/${{ inputs.destination }};
diff --git a/.github/actions/reusable-prepare-peertube-build/action.yml b/.github/actions/reusable-prepare-peertube-build/action.yml
new file mode 100644
index 000000000..41ebf71c5
--- /dev/null
+++ b/.github/actions/reusable-prepare-peertube-build/action.yml
@@ -0,0 +1,31 @@
1name: "Reusable prepare PeerTube build"
2
3description: "Reusable prepare PeerTube build"
4
5inputs:
6 node-version:
7 required: true
8 description: 'NodeJS version'
9
10runs:
11 using: "composite"
12
13 steps:
14 - name: Use Node.js
15 uses: actions/setup-node@v1
16 with:
17 node-version: ${{ inputs.node-version }}
18
19 - name: Cache Node.js modules
20 uses: actions/cache@v2
21 with:
22 path: |
23 **/node_modules
24 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
25 restore-keys: |
26 ${{ runner.OS }}-node-
27 ${{ runner.OS }}-
28
29 - name: Install dependencies
30 shell: bash
31 run: yarn install --frozen-lockfile
diff --git a/.github/actions/reusable-prepare-peertube-run/action.yml b/.github/actions/reusable-prepare-peertube-run/action.yml
new file mode 100644
index 000000000..1a6cd2cfd
--- /dev/null
+++ b/.github/actions/reusable-prepare-peertube-run/action.yml
@@ -0,0 +1,16 @@
1name: "Reusable prepare PeerTube run"
2description: "Reusable prepare PeerTube run"
3
4runs:
5 using: "composite"
6
7 steps:
8 - name: Setup system dependencies
9 shell: bash
10 run: |
11 sudo apt-get install postgresql-client-common redis-tools parallel
12 wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
13 tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
14 mkdir -p $HOME/bin
15 cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
16 echo "$HOME/bin" >> $GITHUB_PATH
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 86f675432..7e8259d27 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -29,48 +29,15 @@ jobs:
29 env: 29 env:
30 PGUSER: peertube 30 PGUSER: peertube
31 PGHOST: localhost 31 PGHOST: localhost
32 NODE_PENDING_JOB_WAIT: 500
33 32
34 steps: 33 steps:
35 - uses: actions/checkout@v2 34 - uses: actions/checkout@v2
36 35
37 - name: Use Node.js 36 - uses: './.github/actions/reusable-prepare-peertube-build'
38 uses: actions/setup-node@v1
39 with: 37 with:
40 node-version: '12.x' 38 node-version: '12.x'
41 39
42 - name: Setup system dependencies 40 - uses: './.github/actions/reusable-prepare-peertube-run'
43 run: |
44 sudo apt-get install postgresql-client-common redis-tools parallel
45 wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
46 tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
47 mkdir -p $HOME/bin
48 cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
49 echo "$HOME/bin" >> $GITHUB_PATH
50
51 - name: Cache Node.js modules
52 uses: actions/cache@v2
53 with:
54 path: |
55 **/node_modules
56 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
57 restore-keys: |
58 ${{ runner.OS }}-node-
59 ${{ runner.OS }}-
60
61 - name: Cache fixtures
62 uses: actions/cache@v2
63 with:
64 path: |
65 fixtures
66 key: ${{ runner.OS }}-fixtures-${{ matrix.test_suite }}-${{ hashFiles('fixtures/*') }}
67 restore-keys: |
68 ${{ runner.OS }}-fixtures-${{ matrix.test_suite }}-
69 ${{ runner.OS }}-fixtures-
70 ${{ runner.OS }}-
71
72 - name: Install dependencies
73 run: yarn install --frozen-lockfile
74 41
75 - name: Build 42 - name: Build
76 run: | 43 run: |
@@ -111,27 +78,11 @@ jobs:
111 cat benchmark.json build-time.json startup-time.json 78 cat benchmark.json build-time.json startup-time.json
112 79
113 - name: Upload benchmark result 80 - name: Upload benchmark result
114 env: 81 uses: './.github/actions/reusable-deploy'
115 STATS_DEPLOYEMENT_KNOWN_HOSTS: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }} 82 with:
116 STATS_DEPLOYEMENT_KEY: ${{ secrets.STATS_DEPLOYEMENT_KEY }} 83 source: benchmark.json build-time.json startup-time.json
117 STATS_DEPLOYEMENT_USER: ${{ secrets.STATS_DEPLOYEMENT_USER }} 84 destination: peertube-stats
118 STATS_DEPLOYEMENT_HOST: ${{ secrets.STATS_DEPLOYEMENT_HOST }} 85 knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
119 run: | 86 deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
120 mkdir -p ~/.ssh 87 deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
121 chmod 700 ~/.ssh 88 deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
122 if [ ! -z ${STATS_DEPLOYEMENT_KNOWN_HOSTS+x} ]; then
123 echo "Adding ssh key to known hosts"
124 echo -e "${STATS_DEPLOYEMENT_KNOWN_HOSTS}" > ~/.ssh/known_hosts;
125 fi
126
127 eval `ssh-agent -s`
128
129 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
130 echo "Adding ssh reployement key"
131 ssh-add <(echo "${STATS_DEPLOYEMENT_KEY}");
132 fi
133
134 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
135 echo "Uploading files"
136 scp benchmark.json build-time.json startup-time.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
137 fi
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..8764cdd0e
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,68 @@
1# For most projects, this workflow file will not need changing; you simply need
2# to commit it to your repository.
3#
4# You may wish to alter this file to override the set of languages analyzed,
5# or to provide custom queries or build logic.
6#
7# ******** NOTE ********
8# We have attempted to detect the languages in your repository. Please check
9# the `language` matrix defined below to confirm you have the correct set of
10# supported CodeQL languages.
11#
12name: "CodeQL"
13
14on:
15 push:
16 branches: [ develop, next ]
17 schedule:
18 - cron: '36 9 * * 5'
19
20jobs:
21 analyze:
22 name: Analyze
23 runs-on: ubuntu-latest
24 permissions:
25 actions: read
26 contents: read
27 security-events: write
28
29 strategy:
30 fail-fast: false
31 matrix:
32 language: [ 'javascript' ]
33 # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
34 # Learn more about CodeQL language support at https://git.io/codeql-language-support
35
36 steps:
37 - name: Checkout repository
38 uses: actions/checkout@v2
39
40 # Initializes the CodeQL tools for scanning.
41 - name: Initialize CodeQL
42 uses: github/codeql-action/init@v1
43 with:
44 languages: ${{ matrix.language }}
45 config-file: ./.github/workflows/codeql/codeql-config.yml
46 # If you wish to specify custom queries, you can do so here or in a config file.
47 # By default, queries listed here will override any specified in a config file.
48 # Prefix the list here with "+" to use these queries and those in the config file.
49 # queries: ./path/to/local/query, your-org/your-repo/queries@main
50
51 # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
52 # If this step fails, then you should remove it and run the build manually (see below)
53 - name: Autobuild
54 uses: github/codeql-action/autobuild@v1
55
56 # ℹ️ Command-line programs to run using the OS shell.
57 # 📚 https://git.io/JvXDl
58
59 # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
60 # and modify them (or add more) to build your code if your project
61 # uses a compiled language
62
63 #- run: |
64 # make bootstrap
65 # make release
66
67 - name: Perform CodeQL Analysis
68 uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/codeql/codeql-config.yml b/.github/workflows/codeql/codeql-config.yml
new file mode 100644
index 000000000..8b771ae99
--- /dev/null
+++ b/.github/workflows/codeql/codeql-config.yml
@@ -0,0 +1,4 @@
1name: "PeerTube CodeQL config"
2
3paths-ignore:
4 - server/tests
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 000000000..7afe641b3
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,70 @@
1name: Docker
2
3on:
4 push:
5 branches:
6 - 'master'
7 schedule:
8 - cron: '0 3 * * *'
9
10jobs:
11 generate-matrix:
12 name: Generate matrix for Docker build
13 runs-on: ubuntu-latest
14 outputs:
15 matrix: ${{ steps.set-matrix.outputs.matrix }}
16 steps:
17 - name: Checkout
18 uses: actions/checkout@v2
19 with:
20 ref: master
21 - name: Set matrix for build
22 id: set-matrix
23 run: |
24 # FIXME: https://github.com/actions/checkout/issues/290
25 git fetch --force --tags
26
27 one="{ \"file\": \"./support/docker/production/Dockerfile.bullseye\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bullseye\" }"
28 two="{ \"file\": \"./support/docker/production/Dockerfile.buster\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-buster,chocobozzz/peertube:$(git describe --abbrev=0)-buster\" }"
29 three="{ \"file\": \"./support/docker/production/Dockerfile.nginx\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube-webserver:latest\" }"
30
31 matrix="[$one,$two,$three]"
32 echo ::set-output name=matrix::{\"include\":$(echo $matrix)}
33
34 docker:
35 runs-on: ubuntu-latest
36
37 needs: generate-matrix
38
39 strategy:
40 matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
41 fail-fast: false
42
43 steps:
44 -
45 name: Set up QEMU
46 uses: docker/setup-qemu-action@v1
47 -
48 name: Set up Docker Buildx
49 uses: docker/setup-buildx-action@v1
50 -
51 name: Login to DockerHub
52 uses: docker/login-action@v1
53 with:
54 username: ${{ secrets.DOCKERHUB_USERNAME }}
55 password: ${{ secrets.DOCKERHUB_TOKEN }}
56
57 -
58 name: Checkout develop
59 uses: actions/checkout@v2
60 with:
61 ref: ${{ matrix.ref }}
62 -
63 name: Docker build
64 uses: docker/build-push-action@v2
65 with:
66 context: '.'
67 platforms: linux/amd64,linux/arm64
68 push: true
69 file: ${{ matrix.file }}
70 tags: ${{ matrix.tags }}
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
new file mode 100644
index 000000000..23898b7ef
--- /dev/null
+++ b/.github/workflows/nightly.yml
@@ -0,0 +1,33 @@
1name: Nightly
2
3on:
4 schedule:
5 - cron: '0 3 * * *'
6
7jobs:
8
9 nightly:
10 runs-on: ubuntu-latest
11
12 steps:
13 -
14 name: Checkout develop
15 uses: actions/checkout@v2
16 with:
17 ref: develop
18
19 - uses: './.github/actions/reusable-prepare-peertube-build'
20 with:
21 node-version: '14.x'
22
23 - name: Build
24 run: npm run nightly
25
26 - uses: './.github/actions/reusable-deploy'
27 with:
28 source: ./peertube-nightly-*
29 destination: nightly
30 knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
31 deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
32 deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
33 deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml
index e211f6a3b..c87e6fb77 100644
--- a/.github/workflows/stats.yml
+++ b/.github/workflows/stats.yml
@@ -1,4 +1,4 @@
1name: "Stats" 1name: Stats
2 2
3on: 3on:
4 push: 4 push:
@@ -20,24 +20,10 @@ jobs:
20 steps: 20 steps:
21 - uses: actions/checkout@v2 21 - uses: actions/checkout@v2
22 22
23 - name: Use Node.js 23 - uses: './.github/actions/reusable-prepare-peertube-build'
24 uses: actions/setup-node@v1
25 with: 24 with:
26 node-version: '14.x' 25 node-version: '14.x'
27 26
28 - name: Cache Node.js modules
29 uses: actions/cache@v2
30 with:
31 path: |
32 **/node_modules
33 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
34 restore-keys: |
35 ${{ runner.OS }}-node-
36 ${{ runner.OS }}-
37
38 - name: Install dependencies
39 run: yarn install --frozen-lockfile
40
41 - name: Angular bundlewatch 27 - name: Angular bundlewatch
42 uses: jackyef/bundlewatch-gh-action@master 28 uses: jackyef/bundlewatch-gh-action@master
43 with: 29 with:
@@ -73,27 +59,11 @@ jobs:
73 59
74 - name: Upload stats 60 - name: Upload stats
75 if: github.event_name != 'pull_request' 61 if: github.event_name != 'pull_request'
76 env: 62 uses: './.github/actions/reusable-deploy'
77 STATS_DEPLOYEMENT_KNOWN_HOSTS: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }} 63 with:
78 STATS_DEPLOYEMENT_KEY: ${{ secrets.STATS_DEPLOYEMENT_KEY }} 64 source: lighthouse.json client-build-stats.json scc.json
79 STATS_DEPLOYEMENT_USER: ${{ secrets.STATS_DEPLOYEMENT_USER }} 65 destination: peertube-stats
80 STATS_DEPLOYEMENT_HOST: ${{ secrets.STATS_DEPLOYEMENT_HOST }} 66 knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
81 run: | 67 deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
82 mkdir -p ~/.ssh 68 deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
83 chmod 700 ~/.ssh 69 deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
84 if [ ! -z ${STATS_DEPLOYEMENT_KNOWN_HOSTS+x} ]; then
85 echo "Adding ssh key to known hosts"
86 echo -e "${STATS_DEPLOYEMENT_KNOWN_HOSTS}" > ~/.ssh/known_hosts;
87 fi
88
89 eval `ssh-agent -s`
90
91 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
92 echo "Adding ssh reployement key"
93 ssh-add <(echo "${STATS_DEPLOYEMENT_KEY}");
94 fi
95
96 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
97 echo "Uploading files"
98 scp lighthouse.json client-build-stats.json scc.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
99 fi
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 78a9a28c0..030ec3790 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,4 @@
1name: Test Suite 1name: Test
2 2
3on: 3on:
4 push: 4 push:
@@ -50,29 +50,11 @@ jobs:
50 steps: 50 steps:
51 - uses: actions/checkout@v2 51 - uses: actions/checkout@v2
52 52
53 - name: Use Node.js 53 - uses: './.github/actions/reusable-prepare-peertube-build'
54 uses: actions/setup-node@v1
55 with: 54 with:
56 node-version: '12.x' 55 node-version: '12.x'
57 56
58 - name: Setup system dependencies 57 - uses: './.github/actions/reusable-prepare-peertube-run'
59 run: |
60 sudo apt-get install postgresql-client-common redis-tools parallel
61 wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
62 tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
63 mkdir -p $HOME/bin
64 cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
65 echo "$HOME/bin" >> $GITHUB_PATH
66
67 - name: Cache Node.js modules
68 uses: actions/cache@v2
69 with:
70 path: |
71 **/node_modules
72 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
73 restore-keys: |
74 ${{ runner.OS }}-node-
75 ${{ runner.OS }}-
76 58
77 - name: Cache fixtures 59 - name: Cache fixtures
78 uses: actions/cache@v2 60 uses: actions/cache@v2
@@ -85,9 +67,6 @@ jobs:
85 ${{ runner.OS }}-fixtures- 67 ${{ runner.OS }}-fixtures-
86 ${{ runner.OS }}- 68 ${{ runner.OS }}-
87 69
88 - name: Install dependencies
89 run: yarn install --frozen-lockfile
90
91 - name: Set env test variable (schedule) 70 - name: Set env test variable (schedule)
92 if: github.event_name != 'schedule' 71 if: github.event_name != 'schedule'
93 run: | 72 run: |
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index ad94c8cab..000000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,84 +0,0 @@
1image: chocobozzz/peertube-ci:14
2
3stages:
4 - clients
5 - docker-nightly
6
7cache:
8 key: yarn
9 paths:
10 - .yarn-cache
11 - cached-fixtures
12
13# build-openapi-clients:
14# stage: clients
15# only:
16# refs:
17# - master
18# - schedules
19# changes:
20# - support/doc/api/openapi.yaml
21# script:
22# - apt-get update -qq
23# - apt-get -yqqq install openjdk-8-jre
24# - yarn install --pure-lockfile
25# - scripts/openapi-peertube-version.sh
26# - scripts/openapi-clients.sh
27
28build-nightly:
29 stage: docker-nightly
30 only:
31 - schedules
32 script:
33 - yarn install --pure-lockfile --cache-folder .yarn-cache
34 - npm run nightly
35 - mkdir "${HOME}/.ssh"
36 - chmod 700 "${HOME}/.ssh"
37 - if [ ! -z ${DEPLOYEMENT_KNOWN_HOSTS+x} ]; then echo -e "${DEPLOYEMENT_KNOWN_HOSTS}" > ${HOME}/.ssh/known_hosts; fi
38 - eval `ssh-agent -s`
39 - if [ ! -z ${DEPLOYEMENT_KEY+x} ]; then ssh-add <(echo "${DEPLOYEMENT_KEY}"); fi
40 - if [ ! -z ${DEPLOYEMENT_KEY+x} ]; then scp ./peertube-nightly-* ${DEPLOYEMENT_USER}@${DEPLOYEMENT_HOST}:../../web/nightly; fi
41
42.docker: &docker
43 stage: docker-nightly
44 cache: {}
45 image:
46 name: gcr.io/kaniko-project/executor:debug
47 entrypoint: [""]
48 before_script:
49 - mkdir -p /kaniko/.docker
50 - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$CI_REGISTRY_AUTH\",\"email\":\"$CI_REGISTRY_EMAIL\"}}}" > /kaniko/.docker/config.json
51 script:
52 - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $DOCKERFILE --destination $DOCKER_IMAGE_NAME
53
54build-docker-develop:
55 <<: *docker
56 only:
57 - schedules
58 variables:
59 DOCKER_IMAGE_NAME: chocobozzz/peertube:develop-bullseye
60 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
61
62build-docker-webserver:
63 <<: *docker
64 only:
65 - schedules
66 variables:
67 DOCKER_IMAGE_NAME: chocobozzz/peertube-webserver
68 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.nginx
69
70build-docker-tag:
71 <<: *docker
72 only:
73 - tags
74 variables:
75 DOCKER_IMAGE_NAME: chocobozzz/peertube:$CI_COMMIT_TAG-bullseye
76 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
77
78build-docker-master:
79 <<: *docker
80 only:
81 - master
82 variables:
83 DOCKER_IMAGE_NAME: chocobozzz/peertube:production-bullseye
84 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index 1026c4e0d..7f2a6aa77 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -116,95 +116,99 @@
116 <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container> 116 <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container>
117 </div> 117 </div>
118 118
119 <div class="anchor" id="moderation"></div> 119 <div myPluginSelector pluginSelectorId="about-instance-moderation">
120 <a 120 <div class="anchor" id="moderation"></div>
121 *ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
122 class="anchor-link"
123 routerLink="/about/instance"
124 fragment="moderation"
125 #anchorLink
126 (click)="onClickCopyLink(anchorLink)">
127 <h2 i18n class="middle-title">
128 MODERATION
129 </h2>
130 </a>
131
132 <div class="block moderation-information" *ngIf="html.moderationInformation">
133 <div class="anchor" id="moderation-information"></div>
134 <a 121 <a
122 *ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
135 class="anchor-link" 123 class="anchor-link"
136 routerLink="/about/instance" 124 routerLink="/about/instance"
137 fragment="moderation-information" 125 fragment="moderation"
138 #anchorLink 126 #anchorLink
139 (click)="onClickCopyLink(anchorLink)"> 127 (click)="onClickCopyLink(anchorLink)">
140 <h3 i18n class="section-title">Moderation information</h3> 128 <h2 i18n class="middle-title">
129 MODERATION
130 </h2>
141 </a> 131 </a>
142 132
143 <div [innerHTML]="html.moderationInformation"></div> 133 <div class="block moderation-information" *ngIf="html.moderationInformation">
144 </div> 134 <div class="anchor" id="moderation-information"></div>
135 <a
136 class="anchor-link"
137 routerLink="/about/instance"
138 fragment="moderation-information"
139 #anchorLink
140 (click)="onClickCopyLink(anchorLink)">
141 <h3 i18n class="section-title">Moderation information</h3>
142 </a>
145 143
146 <div class="block code-of-conduct" *ngIf="html.codeOfConduct"> 144 <div [innerHTML]="html.moderationInformation"></div>
147 <div class="anchor" id="code-of-conduct"></div> 145 </div>
148 <a
149 class="anchor-link"
150 routerLink="/about/instance"
151 fragment="code-of-conduct"
152 #anchorLink
153 (click)="onClickCopyLink(anchorLink)">
154 <h3 i18n class="section-title">Code of conduct</h3>
155 </a>
156 146
157 <div [innerHTML]="html.codeOfConduct"></div> 147 <div class="block code-of-conduct" *ngIf="html.codeOfConduct">
158 </div> 148 <div class="anchor" id="code-of-conduct"></div>
149 <a
150 class="anchor-link"
151 routerLink="/about/instance"
152 fragment="code-of-conduct"
153 #anchorLink
154 (click)="onClickCopyLink(anchorLink)">
155 <h3 i18n class="section-title">Code of conduct</h3>
156 </a>
159 157
160 <div class="block terms"> 158 <div [innerHTML]="html.codeOfConduct"></div>
161 <div class="anchor" id="terms"></div> 159 </div>
162 <a
163 class="anchor-link"
164 routerLink="/about/instance"
165 fragment="terms"
166 #anchorLink
167 (click)="onClickCopyLink(anchorLink)">
168 <h3 i18n class="section-title">Terms</h3>
169 </a>
170 160
171 <div [innerHTML]="html.terms"></div> 161 <div class="block terms">
172 </div> 162 <div class="anchor" id="terms"></div>
163 <a
164 class="anchor-link"
165 routerLink="/about/instance"
166 fragment="terms"
167 #anchorLink
168 (click)="onClickCopyLink(anchorLink)">
169 <h3 i18n class="section-title">Terms</h3>
170 </a>
173 171
174 <div class="anchor" id="other-information"></div> 172 <div [innerHTML]="html.terms"></div>
175 <a 173 </div>
176 *ngIf="html.hardwareInformation" 174 </div>
177 class="anchor-link"
178 routerLink="/about/instance"
179 fragment="other-information"
180 #anchorLink
181 (click)="onClickCopyLink(anchorLink)">
182 <h2 i18n class="middle-title">
183 OTHER INFORMATION
184 </h2>
185 </a>
186 175
187 <div class="block hardware-information" *ngIf="html.hardwareInformation"> 176 <div myPluginSelector pluginSelectorId="about-instance-other-information">
188 <div class="anchor" id="hardware-information"></div> 177 <div class="anchor" id="other-information"></div>
189 <a 178 <a
179 *ngIf="html.hardwareInformation"
190 class="anchor-link" 180 class="anchor-link"
191 routerLink="/about/instance" 181 routerLink="/about/instance"
192 fragment="hardware-information" 182 fragment="other-information"
193 #anchorLink 183 #anchorLink
194 (click)="onClickCopyLink(anchorLink)"> 184 (click)="onClickCopyLink(anchorLink)">
195 <h3 i18n class="section-title">Hardware information</h3> 185 <h2 i18n class="middle-title">
186 OTHER INFORMATION
187 </h2>
196 </a> 188 </a>
197 189
198 <div [innerHTML]="html.hardwareInformation"></div> 190 <div class="block hardware-information" *ngIf="html.hardwareInformation">
191 <div class="anchor" id="hardware-information"></div>
192 <a
193 class="anchor-link"
194 routerLink="/about/instance"
195 fragment="hardware-information"
196 #anchorLink
197 (click)="onClickCopyLink(anchorLink)">
198 <h3 i18n class="section-title">Hardware information</h3>
199 </a>
200
201 <div [innerHTML]="html.hardwareInformation"></div>
202 </div>
199 </div> 203 </div>
200 </div> 204 </div>
201 205
202 <div class="col-md-12 col-xl-6"> 206 <div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
203 <h2 class="sr-only" i18n>FEATURES</h2> 207 <h2 class="sr-only" i18n>FEATURES</h2>
204 <my-instance-features-table></my-instance-features-table> 208 <my-instance-features-table></my-instance-features-table>
205 </div> 209 </div>
206 210
207 <div class="col"> 211 <div class="col" myPluginSelector pluginSelectorId="about-instance-statistics">
208 <div class="anchor" id="statistics"></div> 212 <div class="anchor" id="statistics"></div>
209 <a 213 <a
210 class="anchor-link" 214 class="anchor-link"
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html
index 1ab00c5df..63d429ebf 100644
--- a/client/src/app/+about/about.component.html
+++ b/client/src/app/+about/about.component.html
@@ -2,11 +2,11 @@
2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }"> 2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
3 3
4 <div class="links"> 4 <div class="links">
5 <a i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a> 5 <a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a>
6 6
7 <a i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a> 7 <a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a>
8 8
9 <a i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a> 9 <a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a>
10 </div> 10 </div>
11 </div> 11 </div>
12 12
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 0bb24de2e..8362e6b7e 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -19,10 +19,8 @@
19 ></my-user-moderation-dropdown> 19 ></my-user-moderation-dropdown>
20 20
21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span> 21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
22 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> 22
23 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span> 23 <my-account-block-badges [account]="account"></my-account-block-badges>
24 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
25 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
26 </div> 24 </div>
27 25
28 <div class="actor-handle"> 26 <div class="actor-handle">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index cdd00487b..5043b98c4 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -30,16 +30,10 @@
30 } 30 }
31} 31}
32 32
33my-user-moderation-dropdown, 33my-user-moderation-dropdown {
34.badge { 34 margin: 0 10px;
35 @include margin-left(10px);
36 35
37 position: relative; 36 height: fit-content;
38 top: 3px;
39}
40
41.badge {
42 font-size: 13px;
43} 37}
44 38
45.copy-button { 39.copy-button {
@@ -64,6 +58,10 @@ my-user-moderation-dropdown,
64 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize)); 58 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
65} 59}
66 60
61.actor-display-name {
62 align-items: center;
63}
64
67.description { 65.description {
68 grid-column: 1 / 3; 66 grid-column: 1 / 3;
69 max-width: 1000px; 67 max-width: 1000px;
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 0dcbc250a..898325492 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -12,7 +12,7 @@ import {
12 VideoChannelService, 12 VideoChannelService,
13 VideoService 13 VideoService
14} from '@app/shared/shared-main' 14} from '@app/shared/shared-main'
15import { AccountReportComponent } from '@app/shared/shared-moderation' 15import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
16import { HttpStatusCode, User, UserRight } from '@shared/models' 16import { HttpStatusCode, User, UserRight } from '@shared/models'
17 17
18@Component({ 18@Component({
@@ -52,6 +52,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
52 private authService: AuthService, 52 private authService: AuthService,
53 private videoService: VideoService, 53 private videoService: VideoService,
54 private markdown: MarkdownService, 54 private markdown: MarkdownService,
55 private blocklist: BlocklistService,
55 private screenService: ScreenService 56 private screenService: ScreenService
56 ) { 57 ) {
57 } 58 }
@@ -159,6 +160,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
159 this.updateModerationActions() 160 this.updateModerationActions()
160 this.loadUserIfNeeded(account) 161 this.loadUserIfNeeded(account)
161 this.loadAccountVideosCount() 162 this.loadAccountVideosCount()
163 this.loadAccountBlockStatus()
162 } 164 }
163 165
164 private showReportModal () { 166 private showReportModal () {
@@ -217,4 +219,9 @@ export class AccountsComponent implements OnInit, OnDestroy {
217 this.accountVideosCount = res.total 219 this.accountVideosCount = res.total
218 }) 220 })
219 } 221 }
222
223 private loadAccountBlockStatus () {
224 this.blocklist.getStatus({ accounts: [ this.account.nameWithHostForced ], hosts: [ this.account.host ] })
225 .subscribe(status => this.account.updateBlockStatus(status))
226 }
220} 227}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index 318c8e2c2..c9533208a 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -56,6 +56,36 @@
56 </ng-container> 56 </ng-container>
57 </div> 57 </div>
58 58
59 <ng-container formGroupName="client">
60
61 <ng-container formGroupName="videos">
62 <ng-container formGroupName="miniature">
63 <div class="form-group">
64 <my-peertube-checkbox
65 inputName="clientVideosMiniaturePreferAuthorDisplayName" formControlName="preferAuthorDisplayName"
66 i18n-labelText labelText="Prefer author display name in video miniature"
67 ></my-peertube-checkbox>
68 </div>
69 </ng-container>
70 </ng-container>
71
72 <ng-container formGroupName="menu">
73 <ng-container formGroupName="login">
74 <div class="form-group">
75 <my-peertube-checkbox
76 inputName="clientMenuLoginRedirectOnSingleExternalAuth" formControlName="redirectOnSingleExternalAuth"
77 i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
78 >
79 <ng-container ngProjectAs="description">
80 <span *ngIf="countExternalAuth() === 0" i18n>⚠️ You don't have any external auth plugin enabled.</span>
81 <span *ngIf="countExternalAuth() > 1" i18n>⚠️ You have multiple external auth plugins enabled.</span>
82 </ng-container>
83 </my-peertube-checkbox>
84 </div>
85 </ng-container>
86 </ng-container>
87 </ng-container>
88
59 </div> 89 </div>
60 </div> 90 </div>
61 91
@@ -276,7 +306,7 @@
276 <div class="form-group col-12 col-lg-4 col-xl-3"> 306 <div class="form-group col-12 col-lg-4 col-xl-3">
277 <div i18n class="inner-form-title">VIDEO CHANNELS</div> 307 <div i18n class="inner-form-title">VIDEO CHANNELS</div>
278 </div> 308 </div>
279 309
280 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> 310 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
281 <div class="form-group" formGroupName="videoChannels"> 311 <div class="form-group" formGroupName="videoChannels">
282 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label> 312 <label i18n for="videoChannelsMaxPerUser">Max video channels per user</label>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
index 7a8258820..81457bd36 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
@@ -36,6 +36,10 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
36 } 36 }
37 } 37 }
38 38
39 countExternalAuth () {
40 return this.serverConfig.plugin.registeredExternalAuths.length
41 }
42
39 getVideoQuotaOptions () { 43 getVideoQuotaOptions () {
40 return this.configService.videoQuotaOptions 44 return this.configService.videoQuotaOptions
41 } 45 }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index fdb0a7532..f2eaa3033 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -106,6 +106,18 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
106 whitelisted: null 106 whitelisted: null
107 } 107 }
108 }, 108 },
109 client: {
110 videos: {
111 miniature: {
112 preferAuthorDisplayName: null
113 }
114 },
115 menu: {
116 login: {
117 redirectOnSingleExternalAuth: null
118 }
119 }
120 },
109 cache: { 121 cache: {
110 previews: { 122 previews: {
111 size: CACHE_PREVIEWS_SIZE_VALIDATOR 123 size: CACHE_PREVIEWS_SIZE_VALIDATOR
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 90eea1505..531b06dc9 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -48,7 +48,8 @@
48 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid"> 48 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
49 49
50 <div class="additionnal-links"> 50 <div class="additionnal-links">
51 <a i18n class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a> 51 <a i18n role="button" class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
52
52 <div *ngIf="signupAllowed" class="signup-link"> 53 <div *ngIf="signupAllowed" class="signup-link">
53 <span>·</span> 54 <span>·</span>
54 <a i18n routerLink="/signup" class="create-an-account">Create an account</a> 55 <a i18n routerLink="/signup" class="create-an-account">Create an account</a>
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index 1fa4bd3b5..648b8db36 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -1,4 +1,4 @@
1import { environment } from 'src/environments/environment' 1
2import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' 2import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute } from '@angular/router' 3import { ActivatedRoute } from '@angular/router'
4import { AuthService, Notifier, RedirectService, UserService } from '@app/core' 4import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
@@ -7,6 +7,7 @@ import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' 8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
10import { PluginsManager } from '@root-helpers/plugins-manager'
10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 11import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
11 12
12@Component({ 13@Component({
@@ -98,7 +99,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
98 } 99 }
99 100
100 getAuthHref (auth: RegisteredExternalAuthConfig) { 101 getAuthHref (auth: RegisteredExternalAuthConfig) {
101 return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` 102 return PluginsManager.getExternalAuthHref(auth)
102 } 103 }
103 104
104 login () { 105 login () {
diff --git a/client/src/app/+plugin-pages/index.ts b/client/src/app/+plugin-pages/index.ts
new file mode 100644
index 000000000..b988f13f6
--- /dev/null
+++ b/client/src/app/+plugin-pages/index.ts
@@ -0,0 +1,3 @@
1export * from './plugin-pages-routing.module'
2export * from './plugin-pages.component'
3export * from './plugin-pages.module'
diff --git a/client/src/app/+plugin-pages/plugin-pages-routing.module.ts b/client/src/app/+plugin-pages/plugin-pages-routing.module.ts
new file mode 100644
index 000000000..b47a787e0
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages-routing.module.ts
@@ -0,0 +1,19 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { PluginPagesComponent } from './plugin-pages.component'
4
5const pluginPagesRoutes: Routes = [
6 {
7 path: '**',
8 component: PluginPagesComponent,
9 data: {
10 reloadOnSameNavigation: true
11 }
12 }
13]
14
15@NgModule({
16 imports: [ RouterModule.forChild(pluginPagesRoutes) ],
17 exports: [ RouterModule ]
18})
19export class PluginPagesRoutingModule {}
diff --git a/client/src/app/+plugin-pages/plugin-pages.component.html b/client/src/app/+plugin-pages/plugin-pages.component.html
new file mode 100644
index 000000000..cf62d1bd7
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages.component.html
@@ -0,0 +1 @@
<div #root></div>
diff --git a/client/src/app/+plugin-pages/plugin-pages.component.ts b/client/src/app/+plugin-pages/plugin-pages.component.ts
new file mode 100644
index 000000000..5f294ee13
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages.component.ts
@@ -0,0 +1,31 @@
1import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { PluginService } from '@app/core'
4
5@Component({
6 templateUrl: './plugin-pages.component.html'
7})
8export class PluginPagesComponent implements AfterViewInit {
9 @ViewChild('root') root: ElementRef
10
11 constructor (
12 private route: ActivatedRoute,
13 private router: Router,
14 private pluginService: PluginService
15 ) {
16
17 }
18
19 ngAfterViewInit () {
20 const path = '/' + this.route.snapshot.url.map(u => u.path).join('/')
21
22 const registered = this.pluginService.getRegisteredClientRoute(path)
23 if (!registered) {
24 console.log('Could not find registered route %s.', path, this.pluginService.getAllRegisteredClientRoutes())
25
26 return this.router.navigate([ '/404' ], { skipLocationChange: true })
27 }
28
29 registered.onMount({ rootEl: this.root.nativeElement })
30 }
31}
diff --git a/client/src/app/+plugin-pages/plugin-pages.module.ts b/client/src/app/+plugin-pages/plugin-pages.module.ts
new file mode 100644
index 000000000..86f86c752
--- /dev/null
+++ b/client/src/app/+plugin-pages/plugin-pages.module.ts
@@ -0,0 +1,21 @@
1import { NgModule } from '@angular/core'
2import { PluginPagesRoutingModule } from './plugin-pages-routing.module'
3import { PluginPagesComponent } from './plugin-pages.component'
4
5@NgModule({
6 imports: [
7 PluginPagesRoutingModule
8 ],
9
10 declarations: [
11 PluginPagesComponent
12 ],
13
14 exports: [
15 PluginPagesComponent
16 ],
17
18 providers: [
19 ]
20})
21export class PluginPagesModule { }
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index 4b87a2102..c4861e8c4 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -182,6 +182,31 @@
182 > 182 >
183 </div> 183 </div>
184 184
185 <div class="form-group">
186 <div class="radio-label label-container">
187 <label i18n>Result types</label>
188 <button i18n class="reset-button reset-button-small" (click)="resetField('resultType')" *ngIf="advancedSearch.resultType !== undefined">
189 Reset
190 </button>
191 </div>
192
193 <div class="peertube-radio-container">
194 <input type="radio" name="resultType" id="resultTypeVideos" value="videos" [(ngModel)]="advancedSearch.resultType">
195 <label i18n for="resultTypeVideos" class="radio">Videos</label>
196 </div>
197
198 <div class="peertube-radio-container">
199 <input type="radio" name="resultType" id="resultTypeChannels" value="channels" [(ngModel)]="advancedSearch.resultType">
200 <label i18n for="resultTypeChannels" class="radio">Channels</label>
201 </div>
202
203 <div class="peertube-radio-container">
204 <input type="radio" name="resultType" id="resultTypePlaylists" value="playlists" [(ngModel)]="advancedSearch.resultType">
205 <label i18n for="resultTypePlaylists" class="radio">Playlists</label>
206 </div>
207
208 </div>
209
185 <div class="form-group" *ngIf="isSearchTargetEnabled()"> 210 <div class="form-group" *ngIf="isSearchTargetEnabled()">
186 <div class="radio-label label-container"> 211 <div class="radio-label label-container">
187 <label i18n>Search target</label> 212 <label i18n>Search target</label>
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts
index 5972ba553..aaa4ecc5a 100644
--- a/client/src/app/+search/search-filters.component.ts
+++ b/client/src/app/+search/search-filters.component.ts
@@ -22,7 +22,6 @@ export class SearchFiltersComponent implements OnInit {
22 publishedDateRanges: FormOption[] = [] 22 publishedDateRanges: FormOption[] = []
23 sorts: FormOption[] = [] 23 sorts: FormOption[] = []
24 durationRanges: FormOption[] = [] 24 durationRanges: FormOption[] = []
25 videoType: FormOption[] = []
26 25
27 publishedDateRange: string 26 publishedDateRange: string
28 durationRange: string 27 durationRange: string
@@ -54,17 +53,6 @@ export class SearchFiltersComponent implements OnInit {
54 } 53 }
55 ] 54 ]
56 55
57 this.videoType = [
58 {
59 id: 'vod',
60 label: $localize`VOD videos`
61 },
62 {
63 id: 'live',
64 label: $localize`Live videos`
65 }
66 ]
67
68 this.durationRanges = [ 56 this.durationRanges = [
69 { 57 {
70 id: 'short', 58 id: 'short',
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index fcf6ebbec..b9ec6dbcc 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -47,10 +47,6 @@ export class SearchComponent implements OnInit, OnDestroy {
47 private subActivatedRoute: Subscription 47 private subActivatedRoute: Subscription
48 private isInitialLoad = false // set to false to show the search filters on first arrival 48 private isInitialLoad = false // set to false to show the search filters on first arrival
49 49
50 private channelsPerPage = 2
51 private playlistsPerPage = 2
52 private videosPerPage = 10
53
54 private hasMoreResults = true 50 private hasMoreResults = true
55 private isSearching = false 51 private isSearching = false
56 52
@@ -247,7 +243,6 @@ export class SearchComponent implements OnInit, OnDestroy {
247 private resetPagination () { 243 private resetPagination () {
248 this.pagination.currentPage = 1 244 this.pagination.currentPage = 1
249 this.pagination.totalItems = null 245 this.pagination.totalItems = null
250 this.channelsPerPage = 2
251 246
252 this.results = [] 247 this.results = []
253 } 248 }
@@ -272,7 +267,7 @@ export class SearchComponent implements OnInit, OnDestroy {
272 private getVideosObs () { 267 private getVideosObs () {
273 const params = { 268 const params = {
274 search: this.currentSearch, 269 search: this.currentSearch,
275 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.videosPerPage }), 270 componentPagination: immutableAssign(this.pagination, { itemsPerPage: 10 }),
276 advancedSearch: this.advancedSearch 271 advancedSearch: this.advancedSearch
277 } 272 }
278 273
@@ -288,7 +283,7 @@ export class SearchComponent implements OnInit, OnDestroy {
288 private getVideoChannelObs () { 283 private getVideoChannelObs () {
289 const params = { 284 const params = {
290 search: this.currentSearch, 285 search: this.currentSearch,
291 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.channelsPerPage }), 286 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.buildChannelsPerPage() }),
292 advancedSearch: this.advancedSearch 287 advancedSearch: this.advancedSearch
293 } 288 }
294 289
@@ -304,7 +299,7 @@ export class SearchComponent implements OnInit, OnDestroy {
304 private getVideoPlaylistObs () { 299 private getVideoPlaylistObs () {
305 const params = { 300 const params = {
306 search: this.currentSearch, 301 search: this.currentSearch,
307 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }), 302 componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.buildPlaylistsPerPage() }),
308 advancedSearch: this.advancedSearch 303 advancedSearch: this.advancedSearch
309 } 304 }
310 305
@@ -334,4 +329,16 @@ export class SearchComponent implements OnInit, OnDestroy {
334 329
335 return undefined 330 return undefined
336 } 331 }
332
333 private buildChannelsPerPage () {
334 if (this.advancedSearch.resultType === 'channels') return 10
335
336 return 2
337 }
338
339 private buildPlaylistsPerPage () {
340 if (this.advancedSearch.resultType === 'playlists') return 10
341
342 return 2
343 }
337} 344}
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 064fbb6f5..aec2e373c 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -23,14 +23,16 @@
23 <div class="section-label" i18n>OWNER ACCOUNT</div> 23 <div class="section-label" i18n>OWNER ACCOUNT</div>
24 24
25 <div class="avatar-row"> 25 <div class="avatar-row">
26 <my-actor-avatar class="account-avatar" [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar> 26 <my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
27 27
28 <div class="actor-info"> 28 <div class="actor-info">
29 <h4> 29 <h4>
30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a> 30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ ownerAccount.displayName }}</a>
31 </h4> 31 </h4>
32 32
33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div> 33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
34
35 <my-account-block-badges [account]="ownerAccount"></my-account-block-badges>
34 </div> 36 </div>
35 </div> 37 </div>
36 38
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 272fc41d9..ebb991f4e 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -4,7 +4,8 @@ import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators
4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
5import { ActivatedRoute } from '@angular/router' 5import { ActivatedRoute } from '@angular/router'
6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core' 6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
7import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 7import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { BlocklistService } from '@app/shared/shared-moderation'
8import { SupportModalComponent } from '@app/shared/shared-support-modal' 9import { SupportModalComponent } from '@app/shared/shared-support-modal'
9import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 10import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
10import { HttpStatusCode } from '@shared/models' 11import { HttpStatusCode } from '@shared/models'
@@ -18,6 +19,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
18 @ViewChild('supportModal') supportModal: SupportModalComponent 19 @ViewChild('supportModal') supportModal: SupportModalComponent
19 20
20 videoChannel: VideoChannel 21 videoChannel: VideoChannel
22 ownerAccount: Account
21 hotkeys: Hotkey[] 23 hotkeys: Hotkey[]
22 links: ListOverflowItem[] = [] 24 links: ListOverflowItem[] = []
23 isChannelManageable = false 25 isChannelManageable = false
@@ -38,7 +40,8 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
38 private restExtractor: RestExtractor, 40 private restExtractor: RestExtractor,
39 private hotkeysService: HotkeysService, 41 private hotkeysService: HotkeysService,
40 private screenService: ScreenService, 42 private screenService: ScreenService,
41 private markdown: MarkdownService 43 private markdown: MarkdownService,
44 private blocklist: BlocklistService
42 ) { } 45 ) { }
43 46
44 ngOnInit () { 47 ngOnInit () {
@@ -58,8 +61,10 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
58 61
59 // After the markdown renderer to avoid layout changes 62 // After the markdown renderer to avoid layout changes
60 this.videoChannel = videoChannel 63 this.videoChannel = videoChannel
64 this.ownerAccount = new Account(this.videoChannel.ownerAccount)
61 65
62 this.loadChannelVideosCount() 66 this.loadChannelVideosCount()
67 this.loadOwnerBlockStatus()
63 }) 68 })
64 69
65 this.hotkeys = [ 70 this.hotkeys = [
@@ -125,4 +130,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
125 sort: '-publishedAt' 130 sort: '-publishedAt'
126 }).subscribe(res => this.channelVideosCount = res.total) 131 }).subscribe(res => this.channelVideosCount = res.total)
127 } 132 }
133
134 private loadOwnerBlockStatus () {
135 this.blocklist.getStatus({ accounts: [ this.ownerAccount.nameWithHostForced ], hosts: [ this.ownerAccount.host ] })
136 .subscribe(status => this.ownerAccount.updateBlockStatus(status))
137 }
128} 138}
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts
index 35c39cc2e..76aaecf83 100644
--- a/client/src/app/+video-channels/video-channels.module.ts
+++ b/client/src/app/+video-channels/video-channels.module.ts
@@ -2,15 +2,16 @@ import { NgModule } from '@angular/core'
2import { SharedFormModule } from '@app/shared/shared-forms' 2import { SharedFormModule } from '@app/shared/shared-forms'
3import { SharedGlobalIconModule } from '@app/shared/shared-icons' 3import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation'
5import { SharedSupportModal } from '@app/shared/shared-support-modal' 6import { SharedSupportModal } from '@app/shared/shared-support-modal'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 7import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 8import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' 9import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
10import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
9import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 11import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
10import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 12import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
11import { VideoChannelsRoutingModule } from './video-channels-routing.module' 13import { VideoChannelsRoutingModule } from './video-channels-routing.module'
12import { VideoChannelsComponent } from './video-channels.component' 14import { VideoChannelsComponent } from './video-channels.component'
13import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
14 15
15@NgModule({ 16@NgModule({
16 imports: [ 17 imports: [
@@ -23,7 +24,8 @@ import { SharedActorImageModule } from '../shared/shared-actor-image/shared-acto
23 SharedUserSubscriptionModule, 24 SharedUserSubscriptionModule,
24 SharedGlobalIconModule, 25 SharedGlobalIconModule,
25 SharedSupportModal, 26 SharedSupportModal,
26 SharedActorImageModule 27 SharedActorImageModule,
28 SharedModerationModule
27 ], 29 ],
28 30
29 declarations: [ 31 declarations: [
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 438cb6512..42328d83d 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -58,6 +58,12 @@ const routes: Routes = [
58 }, 58 },
59 59
60 { 60 {
61 path: 'p',
62 loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule),
63 canActivateChild: [ MetaGuard ]
64 },
65
66 {
61 path: 'about', 67 path: 'about',
62 loadChildren: () => import('./+about/about.module').then(m => m.AboutModule), 68 loadChildren: () => import('./+about/about.module').then(m => m.AboutModule),
63 canActivateChild: [ MetaGuard ] 69 canActivateChild: [ MetaGuard ]
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index 89391c2c5..fdbbd2d56 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -20,7 +20,8 @@ import {
20 PluginType, 20 PluginType,
21 PublicServerSetting, 21 PublicServerSetting,
22 RegisterClientFormFieldOptions, 22 RegisterClientFormFieldOptions,
23 RegisterClientSettingsScript, 23 RegisterClientSettingsScriptOptions,
24 RegisterClientRouteOptions,
24 RegisterClientVideoFieldOptions, 25 RegisterClientVideoFieldOptions,
25 ServerConfigPlugin 26 ServerConfigPlugin
26} from '@shared/models' 27} from '@shared/models'
@@ -48,7 +49,8 @@ export class PluginService implements ClientHook {
48 private formFields: FormFields = { 49 private formFields: FormFields = {
49 video: [] 50 video: []
50 } 51 }
51 private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScript } = {} 52 private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScriptOptions } = {}
53 private clientRoutes: { [ route: string ]: RegisterClientRouteOptions } = {}
52 54
53 private pluginsManager: PluginsManager 55 private pluginsManager: PluginsManager
54 56
@@ -67,7 +69,8 @@ export class PluginService implements ClientHook {
67 this.pluginsManager = new PluginsManager({ 69 this.pluginsManager = new PluginsManager({
68 peertubeHelpersFactory: this.buildPeerTubeHelpers.bind(this), 70 peertubeHelpersFactory: this.buildPeerTubeHelpers.bind(this),
69 onFormFields: this.onFormFields.bind(this), 71 onFormFields: this.onFormFields.bind(this),
70 onSettingsScripts: this.onSettingsScripts.bind(this) 72 onSettingsScripts: this.onSettingsScripts.bind(this),
73 onClientRoute: this.onClientRoute.bind(this)
71 }) 74 })
72 } 75 }
73 76
@@ -123,6 +126,14 @@ export class PluginService implements ClientHook {
123 return this.settingsScripts[npmName] 126 return this.settingsScripts[npmName]
124 } 127 }
125 128
129 getRegisteredClientRoute (route: string) {
130 return this.clientRoutes[route]
131 }
132
133 getAllRegisteredClientRoutes () {
134 return Object.keys(this.clientRoutes)
135 }
136
126 translateBy (npmName: string, toTranslate: string) { 137 translateBy (npmName: string, toTranslate: string) {
127 const helpers = this.helpers[npmName] 138 const helpers = this.helpers[npmName]
128 if (!helpers) { 139 if (!helpers) {
@@ -140,12 +151,20 @@ export class PluginService implements ClientHook {
140 }) 151 })
141 } 152 }
142 153
143 private onSettingsScripts (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) { 154 private onSettingsScripts (pluginInfo: PluginInfo, options: RegisterClientSettingsScriptOptions) {
144 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 155 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
145 156
146 this.settingsScripts[npmName] = options 157 this.settingsScripts[npmName] = options
147 } 158 }
148 159
160 private onClientRoute (options: RegisterClientRouteOptions) {
161 const route = options.route.startsWith('/')
162 ? options.route
163 : `/${options.route}`
164
165 this.clientRoutes[route] = options
166 }
167
149 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers { 168 private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
150 const { plugin } = pluginInfo 169 const { plugin } = pluginInfo
151 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType) 170 const npmName = this.nameToNpmName(pluginInfo.plugin.name, pluginInfo.pluginType)
@@ -161,6 +180,10 @@ export class PluginService implements ClientHook {
161 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router` 180 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router`
162 }, 181 },
163 182
183 getBasePluginClientPath: () => {
184 return '/p'
185 },
186
164 getSettings: () => { 187 getSettings: () => {
165 const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings' 188 const path = PluginService.BASE_PLUGIN_API_URL + '/' + npmName + '/public-settings'
166 189
diff --git a/client/src/app/core/routing/custom-reuse-strategy.ts b/client/src/app/core/routing/custom-reuse-strategy.ts
index 1498e221f..5d3ad2e67 100644
--- a/client/src/app/core/routing/custom-reuse-strategy.ts
+++ b/client/src/app/core/routing/custom-reuse-strategy.ts
@@ -58,7 +58,7 @@ export class CustomReuseStrategy implements RouteReuseStrategy {
58 58
59 // Reuse the route if we're going to and from the same route 59 // Reuse the route if we're going to and from the same route
60 shouldReuseRoute (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { 60 shouldReuseRoute (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
61 return future.routeConfig === curr.routeConfig 61 return future.routeConfig === curr.routeConfig && future.routeConfig?.data?.reloadOnSameNavigation !== true
62 } 62 }
63 63
64 private gb () { 64 private gb () {
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 46dd807ec..9ea991042 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -30,7 +30,10 @@
30 30
31 <div class="dropdown-divider"></div> 31 <div class="dropdown-divider"></div>
32 32
33 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"> 33 <a
34 myPluginSelector pluginSelectorId="menu-user-dropdown-language-item"
35 ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"
36 >
34 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> 37 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
35 <span i18n>Interface:</span> 38 <span i18n>Interface:</span>
36 <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span> 39 <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span>
@@ -96,7 +99,9 @@
96 </div> 99 </div>
97 100
98 <div *ngIf="!isLoggedIn" class="login-buttons-block"> 101 <div *ngIf="!isLoggedIn" class="login-buttons-block">
99 <a i18n routerLink="/login" class="peertube-button-link orange-button">Login</a> 102 <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a>
103 <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a>
104
100 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> 105 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a>
101 </div> 106 </div>
102 107
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index 97f07c956..d5ddc29cb 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -21,6 +21,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
21import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' 21import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
22import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service' 22import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
23import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' 23import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
24import { PluginsManager } from '@root-helpers/plugins-manager'
24import { HTMLServerConfig, ServerConfig, UserRight, VideoConstant } from '@shared/models' 25import { HTMLServerConfig, ServerConfig, UserRight, VideoConstant } from '@shared/models'
25 26
26const logger = debug('peertube:menu:MenuComponent') 27const logger = debug('peertube:menu:MenuComponent')
@@ -129,6 +130,15 @@ export class MenuComponent implements OnInit {
129 .subscribe(() => this.openQuickSettings()) 130 .subscribe(() => this.openQuickSettings())
130 } 131 }
131 132
133 getExternalLoginHref () {
134 if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined
135
136 const externalAuths = this.serverConfig.plugin.registeredExternalAuths
137 if (externalAuths.length !== 1) return undefined
138
139 return PluginsManager.getExternalAuthHref(externalAuths[0])
140 }
141
132 isRegistrationAllowed () { 142 isRegistrationAllowed () {
133 if (!this.serverConfig) return false 143 if (!this.serverConfig) return false
134 144
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index 92606e7fa..8b78d01a6 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -1,4 +1,4 @@
1import { Account as ServerAccount, ActorImage } from '@shared/models' 1import { Account as ServerAccount, ActorImage, BlockStatus } from '@shared/models'
2import { Actor } from './actor.model' 2import { Actor } from './actor.model'
3 3
4export class Account extends Actor implements ServerAccount { 4export class Account extends Actor implements ServerAccount {
@@ -49,4 +49,11 @@ export class Account extends Actor implements ServerAccount {
49 resetAvatar () { 49 resetAvatar () {
50 this.avatar = null 50 this.avatar = null
51 } 51 }
52
53 updateBlockStatus (blockStatus: BlockStatus) {
54 this.mutedByInstance = blockStatus.accounts[this.nameWithHostForced].blockedByServer
55 this.mutedByUser = blockStatus.accounts[this.nameWithHostForced].blockedByUser
56 this.mutedServerByUser = blockStatus.hosts[this.host].blockedByUser
57 this.mutedServerByInstance = blockStatus.hosts[this.host].blockedByServer
58 }
52} 59}
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.html b/client/src/app/shared/shared-moderation/account-block-badges.component.html
new file mode 100644
index 000000000..feac707c2
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.html
@@ -0,0 +1,4 @@
1<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
2<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
3<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
4<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.scss b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
new file mode 100644
index 000000000..ccc3666aa
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.scss
@@ -0,0 +1,9 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.badge {
5 @include margin-right(10px);
6
7 height: fit-content;
8 font-size: 12px;
9}
diff --git a/client/src/app/shared/shared-moderation/account-block-badges.component.ts b/client/src/app/shared/shared-moderation/account-block-badges.component.ts
new file mode 100644
index 000000000..a72601118
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/account-block-badges.component.ts
@@ -0,0 +1,11 @@
1import { Component, Input } from '@angular/core'
2import { Account } from '../shared-main'
3
4@Component({
5 selector: 'my-account-block-badges',
6 styleUrls: [ './account-block-badges.component.scss' ],
7 templateUrl: './account-block-badges.component.html'
8})
9export class AccountBlockBadgesComponent {
10 @Input() account: Account
11}
diff --git a/client/src/app/shared/shared-moderation/blocklist.service.ts b/client/src/app/shared/shared-moderation/blocklist.service.ts
index db2a8c584..f4836c6c4 100644
--- a/client/src/app/shared/shared-moderation/blocklist.service.ts
+++ b/client/src/app/shared/shared-moderation/blocklist.service.ts
@@ -3,7 +3,7 @@ import { catchError, map } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { RestExtractor, RestPagination, RestService } from '@app/core' 5import { RestExtractor, RestPagination, RestService } from '@app/core'
6import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '@shared/models' 6import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { Account } from '../shared-main' 8import { Account } from '../shared-main'
9import { AccountBlock } from './account-block.model' 9import { AccountBlock } from './account-block.model'
@@ -12,6 +12,7 @@ export enum BlocklistComponentType { Account, Instance }
12 12
13@Injectable() 13@Injectable()
14export class BlocklistService { 14export class BlocklistService {
15 static BASE_BLOCKLIST_URL = environment.apiUrl + '/api/v1/blocklist'
15 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist' 16 static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
16 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist' 17 static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
17 18
@@ -21,6 +22,23 @@ export class BlocklistService {
21 private restService: RestService 22 private restService: RestService
22 ) { } 23 ) { }
23 24
25 /** ********************* Blocklist status ***********************/
26
27 getStatus (options: {
28 accounts?: string[]
29 hosts?: string[]
30 }) {
31 const { accounts, hosts } = options
32
33 let params = new HttpParams()
34
35 if (accounts) params = this.restService.addArrayParams(params, 'accounts', accounts)
36 if (hosts) params = this.restService.addArrayParams(params, 'hosts', hosts)
37
38 return this.authHttp.get<BlockStatus>(BlocklistService.BASE_BLOCKLIST_URL + '/status', { params })
39 .pipe(catchError(err => this.restExtractor.handleError(err)))
40 }
41
24 /** ********************* User -> Account blocklist ***********************/ 42 /** ********************* User -> Account blocklist ***********************/
25 43
26 getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) { 44 getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts
index 41c910ffe..da85b2299 100644
--- a/client/src/app/shared/shared-moderation/index.ts
+++ b/client/src/app/shared/shared-moderation/index.ts
@@ -1,6 +1,7 @@
1export * from './report-modals' 1export * from './report-modals'
2 2
3export * from './abuse.service' 3export * from './abuse.service'
4export * from './account-block-badges.component'
4export * from './account-block.model' 5export * from './account-block.model'
5export * from './account-blocklist.component' 6export * from './account-blocklist.component'
6export * from './batch-domains-modal.component' 7export * from './batch-domains-modal.component'
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
index 95213e2bd..7cadda67c 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -13,6 +13,7 @@ import { UserBanModalComponent } from './user-ban-modal.component'
13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 13import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
14import { VideoBlockComponent } from './video-block.component' 14import { VideoBlockComponent } from './video-block.component'
15import { VideoBlockService } from './video-block.service' 15import { VideoBlockService } from './video-block.service'
16import { AccountBlockBadgesComponent } from './account-block-badges.component'
16import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' 17import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
17 18
18@NgModule({ 19@NgModule({
@@ -31,7 +32,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
31 VideoReportComponent, 32 VideoReportComponent,
32 BatchDomainsModalComponent, 33 BatchDomainsModalComponent,
33 CommentReportComponent, 34 CommentReportComponent,
34 AccountReportComponent 35 AccountReportComponent,
36 AccountBlockBadgesComponent
35 ], 37 ],
36 38
37 exports: [ 39 exports: [
@@ -41,7 +43,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
41 VideoReportComponent, 43 VideoReportComponent,
42 BatchDomainsModalComponent, 44 BatchDomainsModalComponent,
43 CommentReportComponent, 45 CommentReportComponent,
44 AccountReportComponent 46 AccountReportComponent,
47 AccountBlockBadgesComponent
45 ], 48 ],
46 49
47 providers: [ 50 providers: [
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
index b18d861d6..e2cd2cdc1 100644
--- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
@@ -289,13 +289,13 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
289 { 289 {
290 label: $localize`Mute the instance`, 290 label: $localize`Mute the instance`,
291 description: $localize`Hide any content from that instance for you.`, 291 description: $localize`Hide any content from that instance for you.`,
292 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false, 292 isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === false,
293 handler: ({ account }) => this.blockServerByUser(account.host) 293 handler: ({ account }) => this.blockServerByUser(account.host)
294 }, 294 },
295 { 295 {
296 label: $localize`Unmute the instance`, 296 label: $localize`Unmute the instance`,
297 description: $localize`Show back content from that instance for you.`, 297 description: $localize`Show back content from that instance for you.`,
298 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, 298 isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === true,
299 handler: ({ account }) => this.unblockServerByUser(account.host) 299 handler: ({ account }) => this.unblockServerByUser(account.host)
300 }, 300 },
301 { 301 {
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts
index 2675c6135..724c4d834 100644
--- a/client/src/app/shared/shared-search/advanced-search.model.ts
+++ b/client/src/app/shared/shared-search/advanced-search.model.ts
@@ -8,6 +8,8 @@ import {
8 VideosSearchQuery 8 VideosSearchQuery
9} from '@shared/models' 9} from '@shared/models'
10 10
11export type AdvancedSearchResultType = 'videos' | 'playlists' | 'channels'
12
11export class AdvancedSearch { 13export class AdvancedSearch {
12 startDate: string // ISO 8601 14 startDate: string // ISO 8601
13 endDate: string // ISO 8601 15 endDate: string // ISO 8601
@@ -36,6 +38,7 @@ export class AdvancedSearch {
36 sort: string 38 sort: string
37 39
38 searchTarget: SearchTargetType 40 searchTarget: SearchTargetType
41 resultType: AdvancedSearchResultType
39 42
40 // Filters we don't want to count, because they are mandatory 43 // Filters we don't want to count, because they are mandatory
41 private silentFilters = new Set([ 'sort', 'searchTarget' ]) 44 private silentFilters = new Set([ 'sort', 'searchTarget' ])
@@ -61,6 +64,7 @@ export class AdvancedSearch {
61 durationMax?: string 64 durationMax?: string
62 sort?: string 65 sort?: string
63 searchTarget?: SearchTargetType 66 searchTarget?: SearchTargetType
67 resultType?: AdvancedSearchResultType
64 }) { 68 }) {
65 if (!options) return 69 if (!options) return
66 70
@@ -84,6 +88,12 @@ export class AdvancedSearch {
84 88
85 this.searchTarget = options.searchTarget || undefined 89 this.searchTarget = options.searchTarget || undefined
86 90
91 this.resultType = options.resultType || undefined
92
93 if (!this.resultType && this.hasVideoFilter()) {
94 this.resultType = 'videos'
95 }
96
87 if (isNaN(this.durationMin)) this.durationMin = undefined 97 if (isNaN(this.durationMin)) this.durationMin = undefined
88 if (isNaN(this.durationMax)) this.durationMax = undefined 98 if (isNaN(this.durationMax)) this.durationMax = undefined
89 99
@@ -137,7 +147,8 @@ export class AdvancedSearch {
137 isLive: this.isLive, 147 isLive: this.isLive,
138 host: this.host, 148 host: this.host,
139 sort: this.sort, 149 sort: this.sort,
140 searchTarget: this.searchTarget 150 searchTarget: this.searchTarget,
151 resultType: this.resultType
141 } 152 }
142 } 153 }
143 154
@@ -199,4 +210,21 @@ export class AdvancedSearch {
199 210
200 return true 211 return true
201 } 212 }
213
214 private hasVideoFilter () {
215 return this.startDate !== undefined ||
216 this.endDate !== undefined ||
217 this.originallyPublishedStartDate !== undefined ||
218 this.originallyPublishedEndDate !== undefined ||
219 this.nsfw !== undefined !== undefined ||
220 this.categoryOneOf !== undefined ||
221 this.licenceOneOf !== undefined ||
222 this.languageOneOf !== undefined ||
223 this.tagsOneOf !== undefined ||
224 this.tagsAllOf !== undefined ||
225 this.durationMin !== undefined ||
226 this.durationMax !== undefined ||
227 this.host !== undefined ||
228 this.isLive !== undefined
229 }
202} 230}
diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts
index 71350c733..61acfb466 100644
--- a/client/src/app/shared/shared-search/search.service.ts
+++ b/client/src/app/shared/shared-search/search.service.ts
@@ -1,4 +1,4 @@
1import { Observable } from 'rxjs' 1import { Observable, of } from 'rxjs'
2import { catchError, map, switchMap } from 'rxjs/operators' 2import { catchError, map, switchMap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
@@ -39,6 +39,10 @@ export class SearchService {
39 }): Observable<ResultList<Video>> { 39 }): Observable<ResultList<Video>> {
40 const { search, uuids, componentPagination, advancedSearch } = parameters 40 const { search, uuids, componentPagination, advancedSearch } = parameters
41 41
42 if (advancedSearch?.resultType !== undefined && advancedSearch.resultType !== 'videos') {
43 return of({ total: 0, data: [] })
44 }
45
42 const url = SearchService.BASE_SEARCH_URL + 'videos' 46 const url = SearchService.BASE_SEARCH_URL + 'videos'
43 let pagination: RestPagination 47 let pagination: RestPagination
44 48
@@ -73,6 +77,10 @@ export class SearchService {
73 }): Observable<ResultList<VideoChannel>> { 77 }): Observable<ResultList<VideoChannel>> {
74 const { search, advancedSearch, componentPagination, handles } = parameters 78 const { search, advancedSearch, componentPagination, handles } = parameters
75 79
80 if (advancedSearch?.resultType !== undefined && advancedSearch.resultType !== 'channels') {
81 return of({ total: 0, data: [] })
82 }
83
76 const url = SearchService.BASE_SEARCH_URL + 'video-channels' 84 const url = SearchService.BASE_SEARCH_URL + 'video-channels'
77 85
78 let pagination: RestPagination 86 let pagination: RestPagination
@@ -107,6 +115,10 @@ export class SearchService {
107 }): Observable<ResultList<VideoPlaylist>> { 115 }): Observable<ResultList<VideoPlaylist>> {
108 const { search, advancedSearch, componentPagination, uuids } = parameters 116 const { search, advancedSearch, componentPagination, uuids } = parameters
109 117
118 if (advancedSearch?.resultType !== undefined && advancedSearch.resultType !== 'playlists') {
119 return of({ total: 0, data: [] })
120 }
121
110 const url = SearchService.BASE_SEARCH_URL + 'video-playlists' 122 const url = SearchService.BASE_SEARCH_URL + 'video-playlists'
111 123
112 let pagination: RestPagination 124 let pagination: RestPagination
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
index 71c31696a..421ce4934 100644
--- a/client/src/assets/player/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
@@ -146,7 +146,10 @@ class Html5Hlsjs {
146 } 146 }
147 147
148 duration () { 148 duration () {
149 return this._duration || this.videoElement.duration || 0 149 if (this._duration === Infinity) return Infinity
150 if (!isNaN(this.videoElement.duration)) return this.videoElement.duration
151
152 return this._duration || 0
150 } 153 }
151 154
152 seekable () { 155 seekable () {
@@ -366,6 +369,7 @@ class Html5Hlsjs {
366 369
367 this.isLive = data.details.live 370 this.isLive = data.details.live
368 this.dvrDuration = data.details.totalduration 371 this.dvrDuration = data.details.totalduration
372
369 this._duration = this.isLive ? Infinity : data.details.totalduration 373 this._duration = this.isLive ? Infinity : data.details.totalduration
370 }) 374 })
371 375
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index 0121e87d7..451b4a161 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -11,6 +11,7 @@ import {
11} from './peertube-player-local-storage' 11} from './peertube-player-local-storage'
12import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings' 12import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings'
13import { isMobile } from './utils' 13import { isMobile } from './utils'
14import { SettingsButton } from './videojs-components/settings-menu-button'
14 15
15const Plugin = videojs.getPlugin('plugin') 16const Plugin = videojs.getPlugin('plugin')
16 17
@@ -31,7 +32,8 @@ class PeerTubePlugin extends Plugin {
31 32
32 private menuOpened = false 33 private menuOpened = false
33 private mouseInControlBar = false 34 private mouseInControlBar = false
34 private readonly savedInactivityTimeout: number 35 private mouseInSettings = false
36 private readonly initialInactivityTimeout: number
35 37
36 constructor (player: videojs.Player, options?: PeerTubePluginOptions) { 38 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
37 super(player) 39 super(player)
@@ -40,8 +42,7 @@ class PeerTubePlugin extends Plugin {
40 this.videoDuration = options.videoDuration 42 this.videoDuration = options.videoDuration
41 this.videoCaptions = options.videoCaptions 43 this.videoCaptions = options.videoCaptions
42 this.isLive = options.isLive 44 this.isLive = options.isLive
43 45 this.initialInactivityTimeout = this.player.options_.inactivityTimeout
44 this.savedInactivityTimeout = player.options_.inactivityTimeout
45 46
46 if (options.autoplay) this.player.addClass('vjs-has-autoplay') 47 if (options.autoplay) this.player.addClass('vjs-has-autoplay')
47 48
@@ -108,13 +109,13 @@ class PeerTubePlugin extends Plugin {
108 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) 109 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
109 } 110 }
110 111
111 onMenuOpen () { 112 onMenuOpened () {
112 this.menuOpened = false 113 this.menuOpened = true
113 this.alterInactivity() 114 this.alterInactivity()
114 } 115 }
115 116
116 onMenuClosed () { 117 onMenuClosed () {
117 this.menuOpened = true 118 this.menuOpened = false
118 this.alterInactivity() 119 this.alterInactivity()
119 } 120 }
120 121
@@ -126,6 +127,8 @@ class PeerTubePlugin extends Plugin {
126 this.initCaptions() 127 this.initCaptions()
127 128
128 this.listenControlBarMouse() 129 this.listenControlBarMouse()
130
131 this.listenFullScreenChange()
129 } 132 }
130 133
131 private runViewAdd () { 134 private runViewAdd () {
@@ -198,27 +201,50 @@ class PeerTubePlugin extends Plugin {
198 return fetch(url, { method: 'PUT', body, headers }) 201 return fetch(url, { method: 'PUT', body, headers })
199 } 202 }
200 203
204 private listenFullScreenChange () {
205 this.player.on('fullscreenchange', () => {
206 if (this.player.isFullscreen()) this.player.focus()
207 })
208 }
209
201 private listenControlBarMouse () { 210 private listenControlBarMouse () {
202 this.player.controlBar.on('mouseenter', () => { 211 const controlBar = this.player.controlBar
212 const settingsButton: SettingsButton = (controlBar as any).settingsButton
213
214 controlBar.on('mouseenter', () => {
203 this.mouseInControlBar = true 215 this.mouseInControlBar = true
204 this.alterInactivity() 216 this.alterInactivity()
205 }) 217 })
206 218
207 this.player.controlBar.on('mouseleave', () => { 219 controlBar.on('mouseleave', () => {
208 this.mouseInControlBar = false 220 this.mouseInControlBar = false
209 this.alterInactivity() 221 this.alterInactivity()
210 }) 222 })
223
224 settingsButton.dialog.on('mouseenter', () => {
225 this.mouseInSettings = true
226 this.alterInactivity()
227 })
228
229 settingsButton.dialog.on('mouseleave', () => {
230 this.mouseInSettings = false
231 this.alterInactivity()
232 })
211 } 233 }
212 234
213 private alterInactivity () { 235 private alterInactivity () {
214 if (this.menuOpened) { 236 if (this.menuOpened || this.mouseInSettings || this.mouseInControlBar || this.isTouchEnabled()) {
215 this.player.options_.inactivityTimeout = this.savedInactivityTimeout 237 this.setInactivityTimeout(0)
216 return 238 return
217 } 239 }
218 240
219 if (!this.mouseInControlBar && !this.isTouchEnabled()) { 241 this.setInactivityTimeout(this.initialInactivityTimeout)
220 this.player.options_.inactivityTimeout = 1 242 this.player.reportUserActivity(true)
221 } 243 }
244
245 private setInactivityTimeout (timeout: number) {
246 (this.player as any).cache_.inactivityTimeout = timeout
247 this.player.options_.inactivityTimeout = timeout
222 } 248 }
223 249
224 private isTouchEnabled () { 250 private isTouchEnabled () {
diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts
index 75a5c6904..6de390f4d 100644
--- a/client/src/assets/player/videojs-components/settings-menu-button.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-button.ts
@@ -144,7 +144,7 @@ class SettingsButton extends Button {
144 } 144 }
145 145
146 showDialog () { 146 showDialog () {
147 this.player().peertube().onMenuOpen(); 147 this.player().peertube().onMenuOpened();
148 148
149 (this.menu.el() as HTMLElement).style.opacity = '1' 149 (this.menu.el() as HTMLElement).style.opacity = '1'
150 150
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts
index a1b763ff2..e574e75a3 100644
--- a/client/src/root-helpers/plugins-manager.ts
+++ b/client/src/root-helpers/plugins-manager.ts
@@ -13,8 +13,10 @@ import {
13 PluginType, 13 PluginType,
14 RegisterClientFormFieldOptions, 14 RegisterClientFormFieldOptions,
15 RegisterClientHookOptions, 15 RegisterClientHookOptions,
16 RegisterClientSettingsScript, 16 RegisterClientRouteOptions,
17 RegisterClientSettingsScriptOptions,
17 RegisterClientVideoFieldOptions, 18 RegisterClientVideoFieldOptions,
19 RegisteredExternalAuthConfig,
18 ServerConfigPlugin 20 ServerConfigPlugin
19} from '../../../shared/models' 21} from '../../../shared/models'
20import { environment } from '../environments/environment' 22import { environment } from '../environments/environment'
@@ -36,7 +38,8 @@ type PluginInfo = {
36 38
37type PeertubeHelpersFactory = (pluginInfo: PluginInfo) => RegisterClientHelpers 39type PeertubeHelpersFactory = (pluginInfo: PluginInfo) => RegisterClientHelpers
38type OnFormFields = (options: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void 40type OnFormFields = (options: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void
39type OnSettingsScripts = (pluginInfo: PluginInfo, options: RegisterClientSettingsScript) => void 41type OnSettingsScripts = (pluginInfo: PluginInfo, options: RegisterClientSettingsScriptOptions) => void
42type OnClientRoute = (options: RegisterClientRouteOptions) => void
40 43
41const logger = debug('peertube:plugins') 44const logger = debug('peertube:plugins')
42 45
@@ -63,21 +66,29 @@ class PluginsManager {
63 private readonly peertubeHelpersFactory: PeertubeHelpersFactory 66 private readonly peertubeHelpersFactory: PeertubeHelpersFactory
64 private readonly onFormFields: OnFormFields 67 private readonly onFormFields: OnFormFields
65 private readonly onSettingsScripts: OnSettingsScripts 68 private readonly onSettingsScripts: OnSettingsScripts
69 private readonly onClientRoute: OnClientRoute
66 70
67 constructor (options: { 71 constructor (options: {
68 peertubeHelpersFactory: PeertubeHelpersFactory 72 peertubeHelpersFactory: PeertubeHelpersFactory
69 onFormFields?: OnFormFields 73 onFormFields?: OnFormFields
70 onSettingsScripts?: OnSettingsScripts 74 onSettingsScripts?: OnSettingsScripts
75 onClientRoute?: OnClientRoute
71 }) { 76 }) {
72 this.peertubeHelpersFactory = options.peertubeHelpersFactory 77 this.peertubeHelpersFactory = options.peertubeHelpersFactory
73 this.onFormFields = options.onFormFields 78 this.onFormFields = options.onFormFields
74 this.onSettingsScripts = options.onSettingsScripts 79 this.onSettingsScripts = options.onSettingsScripts
80 this.onClientRoute = options.onClientRoute
75 } 81 }
76 82
77 static getPluginPathPrefix (isTheme: boolean) { 83 static getPluginPathPrefix (isTheme: boolean) {
78 return isTheme ? '/themes' : '/plugins' 84 return isTheme ? '/themes' : '/plugins'
79 } 85 }
80 86
87 static getExternalAuthHref (auth: RegisteredExternalAuthConfig) {
88 return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
89
90 }
91
81 loadPluginsList (config: HTMLServerConfig) { 92 loadPluginsList (config: HTMLServerConfig) {
82 for (const plugin of config.plugin.registered) { 93 for (const plugin of config.plugin.registered) {
83 this.addPlugin(plugin) 94 this.addPlugin(plugin)
@@ -215,7 +226,7 @@ class PluginsManager {
215 return this.onFormFields(commonOptions, videoFormOptions) 226 return this.onFormFields(commonOptions, videoFormOptions)
216 } 227 }
217 228
218 const registerSettingsScript = (options: RegisterClientSettingsScript) => { 229 const registerSettingsScript = (options: RegisterClientSettingsScriptOptions) => {
219 if (!this.onSettingsScripts) { 230 if (!this.onSettingsScripts) {
220 throw new Error('Registering settings script is not supported') 231 throw new Error('Registering settings script is not supported')
221 } 232 }
@@ -223,13 +234,29 @@ class PluginsManager {
223 return this.onSettingsScripts(pluginInfo, options) 234 return this.onSettingsScripts(pluginInfo, options)
224 } 235 }
225 236
237 const registerClientRoute = (options: RegisterClientRouteOptions) => {
238 if (!this.onClientRoute) {
239 throw new Error('Registering client route is not supported')
240 }
241
242 return this.onClientRoute(options)
243 }
244
226 const peertubeHelpers = this.peertubeHelpersFactory(pluginInfo) 245 const peertubeHelpers = this.peertubeHelpersFactory(pluginInfo)
227 246
228 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name) 247 console.log('Loading script %s of plugin %s.', clientScript.script, plugin.name)
229 248
230 const absURL = (environment.apiUrl || window.location.origin) + clientScript.script 249 const absURL = (environment.apiUrl || window.location.origin) + clientScript.script
231 return dynamicImport(absURL) 250 return dynamicImport(absURL)
232 .then((script: ClientScriptModule) => script.register({ registerHook, registerVideoField, registerSettingsScript, peertubeHelpers })) 251 .then((script: ClientScriptModule) => {
252 return script.register({
253 registerHook,
254 registerVideoField,
255 registerSettingsScript,
256 registerClientRoute,
257 peertubeHelpers
258 })
259 })
233 .then(() => this.sortHooksByPriority()) 260 .then(() => this.sortHooksByPriority())
234 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err)) 261 .catch(err => console.error('Cannot import or register plugin %s.', pluginInfo.plugin.name, err))
235 } 262 }
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 96d752699..332a0e17d 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -71,7 +71,7 @@ body {
71 height: $big-play-height; 71 height: $big-play-height;
72 line-height: $big-play-height; 72 line-height: $big-play-height;
73 margin-top: -(math.div($big-play-height, 2)); 73 margin-top: -(math.div($big-play-height, 2));
74 transition: 0.4s opacity; 74 transition: 0.2s background-color;
75 75
76 &::-moz-focus-inner { 76 &::-moz-focus-inner {
77 border: 0; 77 border: 0;
@@ -89,30 +89,6 @@ body {
89 &:hover { 89 &:hover {
90 background-color: var(--mainColor, #696969); 90 background-color: var(--mainColor, #696969);
91 } 91 }
92
93 }
94
95 // Small effect when we click on the play button
96 &.vjs-has-big-play-button-clicked {
97
98 .vjs-big-play-button,
99 .vjs-poster {
100 display: block;
101 visibility: hidden;
102
103 &.vjs-big-play-button,
104 &.vjs-big-play-button::before {
105 opacity: 0;
106 transition: visibility 0.2s, opacity 0.2s;
107 }
108
109 &.vjs-poster,
110 &.vjs-poster::before {
111 opacity: 0;
112 transition: visibility 0.3s, opacity 0.3s;
113 transition-delay: 0.05s;
114 }
115 }
116 } 92 }
117 93
118 // Show poster and controls when playing audio-only content 94 // Show poster and controls when playing audio-only content
@@ -158,6 +134,7 @@ body {
158 background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6)); 134 background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6));
159 box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2); 135 box-shadow: 0 -15px 40px 10px rgba(0, 0, 0, 0.2);
160 text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); 136 text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
137 transition: visibility 0.3s, opacity 0.3s !important;
161 138
162 > button:first-child { 139 > button:first-child {
163 @include margin-left($first-control-bar-element-margin-left); 140 @include margin-left($first-control-bar-element-margin-left);
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 9d1c6c443..874be580d 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -758,8 +758,8 @@ export class PeerTubeEmbed {
758 758
759 return { 759 return {
760 getBaseStaticRoute: unimplemented, 760 getBaseStaticRoute: unimplemented,
761
762 getBaseRouterRoute: unimplemented, 761 getBaseRouterRoute: unimplemented,
762 getBasePluginClientPath: unimplemented,
763 763
764 getSettings: unimplemented, 764 getSettings: unimplemented,
765 765
diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts
index 3415ef08f..73f82e781 100644
--- a/client/src/types/register-client-option.model.ts
+++ b/client/src/types/register-client-option.model.ts
@@ -1,7 +1,8 @@
1import { 1import {
2 RegisterClientFormFieldOptions, 2 RegisterClientFormFieldOptions,
3 RegisterClientHookOptions, 3 RegisterClientHookOptions,
4 RegisterClientSettingsScript, 4 RegisterClientRouteOptions,
5 RegisterClientSettingsScriptOptions,
5 RegisterClientVideoFieldOptions, 6 RegisterClientVideoFieldOptions,
6 ServerConfig 7 ServerConfig
7} from '@shared/models' 8} from '@shared/models'
@@ -11,7 +12,9 @@ export type RegisterClientOptions = {
11 12
12 registerVideoField: (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void 13 registerVideoField: (commonOptions: RegisterClientFormFieldOptions, videoFormOptions: RegisterClientVideoFieldOptions) => void
13 14
14 registerSettingsScript: (options: RegisterClientSettingsScript) => void 15 registerSettingsScript: (options: RegisterClientSettingsScriptOptions) => void
16
17 registerClientRoute: (options: RegisterClientRouteOptions) => void
15 18
16 peertubeHelpers: RegisterClientHelpers 19 peertubeHelpers: RegisterClientHelpers
17} 20}
@@ -21,6 +24,8 @@ export type RegisterClientHelpers = {
21 24
22 getBaseRouterRoute: () => string 25 getBaseRouterRoute: () => string
23 26
27 getBasePluginClientPath: () => string
28
24 isLoggedIn: () => boolean 29 isLoggedIn: () => boolean
25 30
26 getAuthHeader: () => { 'Authorization': string } | undefined 31 getAuthHeader: () => { 'Authorization': string } | undefined
diff --git a/config/default.yaml b/config/default.yaml
index b9c725cea..074951117 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -82,6 +82,12 @@ client:
82 # By default PeerTube client displays author username 82 # By default PeerTube client displays author username
83 prefer_author_display_name: false 83 prefer_author_display_name: false
84 84
85 menu:
86 login:
87 # If you enable only one external auth plugin
88 # You can automatically redirect your users on this external platform when they click on the login button
89 redirect_on_single_external_auth: false
90
85# From the project root directory 91# From the project root directory
86storage: 92storage:
87 tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... 93 tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
diff --git a/config/production.yaml.example b/config/production.yaml.example
index d67349c1d..e38b79587 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -80,6 +80,12 @@ client:
80 # By default PeerTube client displays author username 80 # By default PeerTube client displays author username
81 prefer_author_display_name: false 81 prefer_author_display_name: false
82 82
83 menu:
84 login:
85 # If you enable only one external auth plugin
86 # You can automatically redirect your users on this external platform when they click on the login button
87 redirect_on_single_external_auth: false
88
83# From the project root directory 89# From the project root directory
84storage: 90storage:
85 tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... 91 tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
diff --git a/config/test.yaml b/config/test.yaml
index 2e7f982d3..461e1b4ba 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -155,6 +155,7 @@ search:
155federation: 155federation:
156 videos: 156 videos:
157 federate_unlisted: true 157 federate_unlisted: true
158 cleanup_remote_interactions: false
158 159
159views: 160views:
160 videos: 161 videos:
diff --git a/package.json b/package.json
index f2906128f..2d342418d 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,6 @@
90 "cookie-parser": "^1.4.3", 90 "cookie-parser": "^1.4.3",
91 "cors": "^2.8.1", 91 "cors": "^2.8.1",
92 "create-torrent": "^5.0.0", 92 "create-torrent": "^5.0.0",
93 "decache": "^4.6.0",
94 "deep-object-diff": "^1.1.0", 93 "deep-object-diff": "^1.1.0",
95 "email-templates": "^8.0.3", 94 "email-templates": "^8.0.3",
96 "execa": "^5.1.1", 95 "execa": "^5.1.1",
@@ -118,7 +117,7 @@
118 "markdown-it-emoji": "^2.0.0", 117 "markdown-it-emoji": "^2.0.0",
119 "memoizee": "^0.4.14", 118 "memoizee": "^0.4.14",
120 "morgan": "^1.5.3", 119 "morgan": "^1.5.3",
121 "multer": "^1.1.0", 120 "multer": "^1.4.4",
122 "node-media-server": "^2.1.4", 121 "node-media-server": "^2.1.4",
123 "nodemailer": "^6.0.0", 122 "nodemailer": "^6.0.0",
124 "oauth2-server": "3.1.1", 123 "oauth2-server": "3.1.1",
diff --git a/scripts/clean/server/test.sh b/scripts/clean/server/test.sh
index f85daf810..5c30c7639 100755
--- a/scripts/clean/server/test.sh
+++ b/scripts/clean/server/test.sh
@@ -20,9 +20,9 @@ dropRedis () {
20 port=$((9000+$1)) 20 port=$((9000+$1))
21 host="localhost" 21 host="localhost"
22 22
23 redis-cli -h "$host" KEYS "bull-localhost:$port*" | grep -v empty | xargs --no-run-if-empty redis-cli -h "$host" DEL 23 redis-cli -h "$host" KEYS "bull-localhost:$port*" | grep -v empty | xargs -r redis-cli -h "$host" DEL
24 redis-cli -h "$host" KEYS "redis-localhost:$port*" | grep -v empty | xargs --no-run-if-empty redis-cli -h "$host" DEL 24 redis-cli -h "$host" KEYS "redis-localhost:$port*" | grep -v empty | xargs -r redis-cli -h "$host" DEL
25 redis-cli -h "$host" KEYS "*redis-localhost:$port-" | grep -v empty | xargs --no-run-if-empty redis-cli -h "$host" DEL 25 redis-cli -h "$host" KEYS "*redis-localhost:$port-" | grep -v empty | xargs -r redis-cli -h "$host" DEL
26} 26}
27 27
28seq=$(seq 1 6) 28seq=$(seq 1 6)
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts
index 9df80d503..12d78fdc6 100755
--- a/scripts/prune-storage.ts
+++ b/scripts/prune-storage.ts
@@ -15,7 +15,8 @@ import { ActorImageModel } from '../server/models/actor/actor-image'
15import { uniq, values } from 'lodash' 15import { uniq, values } from 'lodash'
16import { ThumbnailType } from '@shared/models' 16import { ThumbnailType } from '@shared/models'
17import { VideoFileModel } from '@server/models/video/video-file' 17import { VideoFileModel } from '@server/models/video/video-file'
18import { HLS_REDUNDANCY_DIRECTORY } from '@server/initializers/constants' 18import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants'
19import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
19 20
20run() 21run()
21 .then(() => process.exit(0)) 22 .then(() => process.exit(0))
@@ -40,6 +41,9 @@ async function run () {
40 41
41 toDelete = toDelete.concat( 42 toDelete = toDelete.concat(
42 await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesWebTorrentFileExist()), 43 await pruneDirectory(CONFIG.STORAGE.VIDEOS_DIR, doesWebTorrentFileExist()),
44
45 await pruneDirectory(HLS_STREAMING_PLAYLIST_DIRECTORY, doesHLSPlaylistExist()),
46
43 await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesTorrentFileExist()), 47 await pruneDirectory(CONFIG.STORAGE.TORRENTS_DIR, doesTorrentFileExist()),
44 48
45 await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist), 49 await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist),
@@ -94,6 +98,10 @@ function doesWebTorrentFileExist () {
94 return (filePath: string) => VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath)) 98 return (filePath: string) => VideoFileModel.doesOwnedWebTorrentVideoFileExist(basename(filePath))
95} 99}
96 100
101function doesHLSPlaylistExist () {
102 return (hlsPath: string) => VideoStreamingPlaylistModel.doesOwnedHLSPlaylistExist(basename(hlsPath))
103}
104
97function doesTorrentFileExist () { 105function doesTorrentFileExist () {
98 return (filePath: string) => VideoFileModel.doesOwnedTorrentFileExist(basename(filePath)) 106 return (filePath: string) => VideoFileModel.doesOwnedTorrentFileExist(basename(filePath))
99} 107}
diff --git a/scripts/update-host.ts b/scripts/update-host.ts
index c6eb9d533..66c0137d9 100755
--- a/scripts/update-host.ts
+++ b/scripts/update-host.ts
@@ -17,7 +17,7 @@ import { VideoCommentModel } from '../server/models/video/video-comment'
17import { AccountModel } from '../server/models/account/account' 17import { AccountModel } from '../server/models/account/account'
18import { VideoChannelModel } from '../server/models/video/video-channel' 18import { VideoChannelModel } from '../server/models/video/video-channel'
19import { initDatabaseModels } from '../server/initializers/database' 19import { initDatabaseModels } from '../server/initializers/database'
20import { updateTorrentUrls } from '@server/helpers/webtorrent' 20import { updateTorrentMetadata } from '@server/helpers/webtorrent'
21import { getServerActor } from '@server/models/application/application' 21import { getServerActor } from '@server/models/application/application'
22 22
23run() 23run()
@@ -126,7 +126,7 @@ async function run () {
126 126
127 for (const file of video.VideoFiles) { 127 for (const file of video.VideoFiles) {
128 console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid) 128 console.log('Updating torrent file %s of video %s.', file.resolution, video.uuid)
129 await updateTorrentUrls(video, file) 129 await updateTorrentMetadata(video, file)
130 130
131 await file.save() 131 await file.save()
132 } 132 }
@@ -135,7 +135,7 @@ async function run () {
135 for (const file of (playlist?.VideoFiles || [])) { 135 for (const file of (playlist?.VideoFiles || [])) {
136 console.log('Updating fragmented torrent file %s of video %s.', file.resolution, video.uuid) 136 console.log('Updating fragmented torrent file %s of video %s.', file.resolution, video.uuid)
137 137
138 await updateTorrentUrls(video, file) 138 await updateTorrentMetadata(playlist, file)
139 139
140 await file.save() 140 await file.save()
141 } 141 }
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
index 72c418e74..a6d0b0512 100644
--- a/server/controllers/api/abuse.ts
+++ b/server/controllers/api/abuse.ts
@@ -167,7 +167,11 @@ async function reportAbuse (req: express.Request, res: express.Response) {
167 const body: AbuseCreate = req.body 167 const body: AbuseCreate = req.body
168 168
169 const { id } = await sequelizeTypescript.transaction(async t => { 169 const { id } = await sequelizeTypescript.transaction(async t => {
170 const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 170 const user = res.locals.oauth.token.User
171 // Don't send abuse notification if reporter is an admin/moderator
172 const skipNotification = user.hasRight(UserRight.MANAGE_ABUSES)
173
174 const reporterAccount = await AccountModel.load(user.Account.id, t)
171 const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r]) 175 const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r])
172 176
173 const baseAbuse = { 177 const baseAbuse = {
@@ -184,7 +188,8 @@ async function reportAbuse (req: express.Request, res: express.Response) {
184 reporterAccount, 188 reporterAccount,
185 transaction: t, 189 transaction: t,
186 startAt: body.video.startAt, 190 startAt: body.video.startAt,
187 endAt: body.video.endAt 191 endAt: body.video.endAt,
192 skipNotification
188 }) 193 })
189 } 194 }
190 195
@@ -193,7 +198,8 @@ async function reportAbuse (req: express.Request, res: express.Response) {
193 baseAbuse, 198 baseAbuse,
194 commentInstance, 199 commentInstance,
195 reporterAccount, 200 reporterAccount,
196 transaction: t 201 transaction: t,
202 skipNotification
197 }) 203 })
198 } 204 }
199 205
@@ -202,7 +208,8 @@ async function reportAbuse (req: express.Request, res: express.Response) {
202 baseAbuse, 208 baseAbuse,
203 accountInstance, 209 accountInstance,
204 reporterAccount, 210 reporterAccount,
205 transaction: t 211 transaction: t,
212 skipNotification
206 }) 213 })
207 }) 214 })
208 215
diff --git a/server/controllers/api/blocklist.ts b/server/controllers/api/blocklist.ts
new file mode 100644
index 000000000..1e936ad10
--- /dev/null
+++ b/server/controllers/api/blocklist.ts
@@ -0,0 +1,108 @@
1import express from 'express'
2import { handleToNameAndHost } from '@server/helpers/actors'
3import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
4import { getServerActor } from '@server/models/application/application'
5import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
6import { MActorAccountId, MUserAccountId } from '@server/types/models'
7import { BlockStatus } from '@shared/models'
8import { asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares'
9import { logger } from '@server/helpers/logger'
10
11const blocklistRouter = express.Router()
12
13blocklistRouter.get('/status',
14 optionalAuthenticate,
15 blocklistStatusValidator,
16 asyncMiddleware(getBlocklistStatus)
17)
18
19// ---------------------------------------------------------------------------
20
21export {
22 blocklistRouter
23}
24
25// ---------------------------------------------------------------------------
26
27async function getBlocklistStatus (req: express.Request, res: express.Response) {
28 const hosts = req.query.hosts as string[]
29 const accounts = req.query.accounts as string[]
30 const user = res.locals.oauth?.token.User
31
32 const serverActor = await getServerActor()
33
34 const byAccountIds = [ serverActor.Account.id ]
35 if (user) byAccountIds.push(user.Account.id)
36
37 const status: BlockStatus = {
38 accounts: {},
39 hosts: {}
40 }
41
42 const baseOptions = {
43 byAccountIds,
44 user,
45 serverActor,
46 status
47 }
48
49 await Promise.all([
50 populateServerBlocklistStatus({ ...baseOptions, hosts }),
51 populateAccountBlocklistStatus({ ...baseOptions, accounts })
52 ])
53
54 return res.json(status)
55}
56
57async function populateServerBlocklistStatus (options: {
58 byAccountIds: number[]
59 user?: MUserAccountId
60 serverActor: MActorAccountId
61 hosts: string[]
62 status: BlockStatus
63}) {
64 const { byAccountIds, user, serverActor, hosts, status } = options
65
66 if (!hosts || hosts.length === 0) return
67
68 const serverBlocklistStatus = await ServerBlocklistModel.getBlockStatus(byAccountIds, hosts)
69
70 logger.debug('Got server blocklist status.', { serverBlocklistStatus, byAccountIds, hosts })
71
72 for (const host of hosts) {
73 const block = serverBlocklistStatus.find(b => b.host === host)
74
75 status.hosts[host] = getStatus(block, serverActor, user)
76 }
77}
78
79async function populateAccountBlocklistStatus (options: {
80 byAccountIds: number[]
81 user?: MUserAccountId
82 serverActor: MActorAccountId
83 accounts: string[]
84 status: BlockStatus
85}) {
86 const { byAccountIds, user, serverActor, accounts, status } = options
87
88 if (!accounts || accounts.length === 0) return
89
90 const accountBlocklistStatus = await AccountBlocklistModel.getBlockStatus(byAccountIds, accounts)
91
92 logger.debug('Got account blocklist status.', { accountBlocklistStatus, byAccountIds, accounts })
93
94 for (const account of accounts) {
95 const sanitizedHandle = handleToNameAndHost(account)
96
97 const block = accountBlocklistStatus.find(b => b.name === sanitizedHandle.name && b.host === sanitizedHandle.host)
98
99 status.accounts[sanitizedHandle.handle] = getStatus(block, serverActor, user)
100 }
101}
102
103function getStatus (block: { accountId: number }, serverActor: MActorAccountId, user?: MUserAccountId) {
104 return {
105 blockedByServer: !!(block && block.accountId === serverActor.Account.id),
106 blockedByUser: !!(block && user && block.accountId === user.Account.id)
107 }
108}
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 805ad99c7..b253db397 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -169,6 +169,18 @@ function customConfig (): CustomConfig {
169 whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED 169 whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
170 } 170 }
171 }, 171 },
172 client: {
173 videos: {
174 miniature: {
175 preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
176 }
177 },
178 menu: {
179 login: {
180 redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
181 }
182 }
183 },
172 cache: { 184 cache: {
173 previews: { 185 previews: {
174 size: CONFIG.CACHE.PREVIEWS.SIZE 186 size: CONFIG.CACHE.PREVIEWS.SIZE
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 9949b378a..5f49336b1 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -6,6 +6,7 @@ import { badRequest } from '../../helpers/express-utils'
6import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
7import { abuseRouter } from './abuse' 7import { abuseRouter } from './abuse'
8import { accountsRouter } from './accounts' 8import { accountsRouter } from './accounts'
9import { blocklistRouter } from './blocklist'
9import { bulkRouter } from './bulk' 10import { bulkRouter } from './bulk'
10import { configRouter } from './config' 11import { configRouter } from './config'
11import { customPageRouter } from './custom-page' 12import { customPageRouter } from './custom-page'
@@ -49,6 +50,7 @@ apiRouter.use('/search', searchRouter)
49apiRouter.use('/overviews', overviewsRouter) 50apiRouter.use('/overviews', overviewsRouter)
50apiRouter.use('/plugins', pluginRouter) 51apiRouter.use('/plugins', pluginRouter)
51apiRouter.use('/custom-pages', customPageRouter) 52apiRouter.use('/custom-pages', customPageRouter)
53apiRouter.use('/blocklist', blocklistRouter)
52apiRouter.use('/ping', pong) 54apiRouter.use('/ping', pong)
53apiRouter.use('/*', badRequest) 55apiRouter.use('/*', badRequest)
54 56
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 2de7fe41f..de9e055dc 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -144,8 +144,13 @@ async function installPlugin (req: express.Request, res: express.Response) {
144 144
145 const fromDisk = !!body.path 145 const fromDisk = !!body.path
146 const toInstall = body.npmName || body.path 146 const toInstall = body.npmName || body.path
147
148 const pluginVersion = body.pluginVersion && body.npmName
149 ? body.pluginVersion
150 : undefined
151
147 try { 152 try {
148 const plugin = await PluginManager.Instance.install(toInstall, undefined, fromDisk) 153 const plugin = await PluginManager.Instance.install(toInstall, pluginVersion, fromDisk)
149 154
150 return res.json(plugin.toFormattedJSON()) 155 return res.json(plugin.toFormattedJSON())
151 } catch (err) { 156 } catch (err) {
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts
index d661144ca..2ab398f4d 100644
--- a/server/controllers/api/server/stats.ts
+++ b/server/controllers/api/server/stats.ts
@@ -3,6 +3,7 @@ import { StatsManager } from '@server/lib/stat-manager'
3import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants' 3import { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants'
4import { asyncMiddleware } from '../../../middlewares' 4import { asyncMiddleware } from '../../../middlewares'
5import { cacheRoute } from '../../../middlewares/cache/cache' 5import { cacheRoute } from '../../../middlewares/cache/cache'
6import { Hooks } from '@server/lib/plugins/hooks'
6 7
7const statsRouter = express.Router() 8const statsRouter = express.Router()
8 9
@@ -12,7 +13,8 @@ statsRouter.get('/stats',
12) 13)
13 14
14async function getStats (_req: express.Request, res: express.Response) { 15async function getStats (_req: express.Request, res: express.Response) {
15 const data = await StatsManager.Instance.getStats() 16 let data = await StatsManager.Instance.getStats()
17 data = await Hooks.wrapObject(data, 'filter:api.server.stats.get.result')
16 18
17 return res.json(data) 19 return res.json(data)
18} 20}
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index 6799ca8c5..fb1f68635 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -1,5 +1,6 @@
1import 'multer' 1import 'multer'
2import express from 'express' 2import express from 'express'
3import { handlesToNameAndHost } from '@server/helpers/actors'
3import { pickCommonVideoQuery } from '@server/helpers/query' 4import { pickCommonVideoQuery } from '@server/helpers/query'
4import { sendUndoFollow } from '@server/lib/activitypub/send' 5import { sendUndoFollow } from '@server/lib/activitypub/send'
5import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' 6import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
@@ -7,7 +8,6 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
7import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 8import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
8import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 9import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
9import { getFormattedObjects } from '../../../helpers/utils' 10import { getFormattedObjects } from '../../../helpers/utils'
10import { WEBSERVER } from '../../../initializers/constants'
11import { sequelizeTypescript } from '../../../initializers/database' 11import { sequelizeTypescript } from '../../../initializers/database'
12import { JobQueue } from '../../../lib/job-queue' 12import { JobQueue } from '../../../lib/job-queue'
13import { 13import {
@@ -89,28 +89,23 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
89 const uris = req.query.uris as string[] 89 const uris = req.query.uris as string[]
90 const user = res.locals.oauth.token.User 90 const user = res.locals.oauth.token.User
91 91
92 const handles = uris.map(u => { 92 const sanitizedHandles = handlesToNameAndHost(uris)
93 let [ name, host ] = u.split('@')
94 if (host === WEBSERVER.HOST) host = null
95 93
96 return { name, host, uri: u } 94 const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles)
97 })
98
99 const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, handles)
100 95
101 const existObject: { [id: string ]: boolean } = {} 96 const existObject: { [id: string ]: boolean } = {}
102 for (const handle of handles) { 97 for (const sanitizedHandle of sanitizedHandles) {
103 const obj = results.find(r => { 98 const obj = results.find(r => {
104 const server = r.ActorFollowing.Server 99 const server = r.ActorFollowing.Server
105 100
106 return r.ActorFollowing.preferredUsername === handle.name && 101 return r.ActorFollowing.preferredUsername === sanitizedHandle.name &&
107 ( 102 (
108 (!server && !handle.host) || 103 (!server && !sanitizedHandle.host) ||
109 (server.host === handle.host) 104 (server.host === sanitizedHandle.host)
110 ) 105 )
111 }) 106 })
112 107
113 existObject[handle.uri] = obj !== undefined 108 existObject[sanitizedHandle.handle] = obj !== undefined
114 } 109 }
115 110
116 return res.json(existObject) 111 return res.json(existObject)
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index eddb9b32d..52864bdfd 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -38,6 +38,7 @@ import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoIm
38import { VideoModel } from '../../../models/video/video' 38import { VideoModel } from '../../../models/video/video'
39import { VideoCaptionModel } from '../../../models/video/video-caption' 39import { VideoCaptionModel } from '../../../models/video/video-caption'
40import { VideoImportModel } from '../../../models/video/video-import' 40import { VideoImportModel } from '../../../models/video/video-import'
41import { Hooks } from '@server/lib/plugins/hooks'
41 42
42const auditLogger = auditLoggerFactory('video-imports') 43const auditLogger = auditLoggerFactory('video-imports')
43const videoImportsRouter = express.Router() 44const videoImportsRouter = express.Router()
@@ -94,7 +95,7 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
94 videoName = result.name 95 videoName = result.name
95 } 96 }
96 97
97 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) 98 const video = await buildVideo(res.locals.videoChannel.id, body, { name: videoName })
98 99
99 const thumbnailModel = await processThumbnail(req, video) 100 const thumbnailModel = await processThumbnail(req, video)
100 const previewModel = await processPreview(req, video) 101 const previewModel = await processPreview(req, video)
@@ -151,7 +152,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
151 }) 152 })
152 } 153 }
153 154
154 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) 155 const video = await buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
155 156
156 // Process video thumbnail from request.files 157 // Process video thumbnail from request.files
157 let thumbnailModel = await processThumbnail(req, video) 158 let thumbnailModel = await processThumbnail(req, video)
@@ -210,8 +211,8 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
210 return res.json(videoImport.toFormattedJSON()).end() 211 return res.json(videoImport.toFormattedJSON()).end()
211} 212}
212 213
213function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo): MVideoThumbnail { 214async function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo): Promise<MVideoThumbnail> {
214 const videoData = { 215 let videoData = {
215 name: body.name || importData.name || 'Unknown name', 216 name: body.name || importData.name || 'Unknown name',
216 remote: false, 217 remote: false,
217 category: body.category || importData.category, 218 category: body.category || importData.category,
@@ -231,6 +232,14 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
231 ? new Date(body.originallyPublishedAt) 232 ? new Date(body.originallyPublishedAt)
232 : importData.originallyPublishedAt 233 : importData.originallyPublishedAt
233 } 234 }
235
236 videoData = await Hooks.wrapObject(
237 videoData,
238 body.targetUrl
239 ? 'filter:api.video.import-url.video-attribute.result'
240 : 'filter:api.video.import-torrent.video-attribute.result'
241 )
242
234 const video = new VideoModel(videoData) 243 const video = new VideoModel(videoData)
235 video.url = getLocalVideoActivityPubUrl(video) 244 video.url = getLocalVideoActivityPubUrl(video)
236 245
diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts
index e29615ff5..3e1480cf2 100644
--- a/server/controllers/api/videos/live.ts
+++ b/server/controllers/api/videos/live.ts
@@ -83,7 +83,9 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
83 const videoInfo: LiveVideoCreate = req.body 83 const videoInfo: LiveVideoCreate = req.body
84 84
85 // Prepare data so we don't block the transaction 85 // Prepare data so we don't block the transaction
86 const videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id) 86 let videoData = buildLocalVideoFromReq(videoInfo, res.locals.videoChannel.id)
87 videoData = await Hooks.wrapObject(videoData, 'filter:api.video.live.video-attribute.result')
88
87 videoData.isLive = true 89 videoData.isLive = true
88 videoData.state = VideoState.WAITING_FOR_LIVE 90 videoData.state = VideoState.WAITING_FOR_LIVE
89 videoData.duration = 0 91 videoData.duration = 0
diff --git a/server/controllers/api/videos/update.ts b/server/controllers/api/videos/update.ts
index 3fcff3e86..589556f47 100644
--- a/server/controllers/api/videos/update.ts
+++ b/server/controllers/api/videos/update.ts
@@ -1,5 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { Transaction } from 'sequelize/types' 2import { Transaction } from 'sequelize/types'
3import { updateTorrentMetadata } from '@server/helpers/webtorrent'
3import { changeVideoChannelShare } from '@server/lib/activitypub/share' 4import { changeVideoChannelShare } from '@server/lib/activitypub/share'
4import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' 5import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
5import { openapiOperationDoc } from '@server/middlewares/doc' 6import { openapiOperationDoc } from '@server/middlewares/doc'
@@ -68,7 +69,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
68 }) 69 })
69 70
70 try { 71 try {
71 const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { 72 const { videoInstanceUpdated, isNewVideo } = await sequelizeTypescript.transaction(async t => {
72 // Refresh video since thumbnails to prevent concurrent updates 73 // Refresh video since thumbnails to prevent concurrent updates
73 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoFromReq.id, t) 74 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoFromReq.id, t)
74 75
@@ -137,8 +138,6 @@ async function updateVideo (req: express.Request, res: express.Response) {
137 transaction: t 138 transaction: t
138 }) 139 })
139 140
140 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
141
142 auditLogger.update( 141 auditLogger.update(
143 getAuditIdFromRes(res), 142 getAuditIdFromRes(res),
144 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), 143 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
@@ -146,12 +145,14 @@ async function updateVideo (req: express.Request, res: express.Response) {
146 ) 145 )
147 logger.info('Video with name %s and uuid %s updated.', video.name, video.uuid, lTags(video.uuid)) 146 logger.info('Video with name %s and uuid %s updated.', video.name, video.uuid, lTags(video.uuid))
148 147
149 return videoInstanceUpdated 148 return { videoInstanceUpdated, isNewVideo }
150 }) 149 })
151 150
152 if (wasConfidentialVideo) { 151 if (videoInfoToUpdate.name) await updateTorrentsMetadata(videoInstanceUpdated)
153 Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated) 152
154 } 153 await sequelizeTypescript.transaction(t => federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t))
154
155 if (wasConfidentialVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
155 156
156 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res }) 157 Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res })
157 } catch (err) { 158 } catch (err) {
@@ -199,3 +200,20 @@ function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: Vide
199 return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction) 200 return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction)
200 } 201 }
201} 202}
203
204async function updateTorrentsMetadata (video: MVideoFullLight) {
205 for (const file of (video.VideoFiles || [])) {
206 await updateTorrentMetadata(video, file)
207
208 await file.save()
209 }
210
211 const hls = video.getHLSPlaylist()
212 if (!hls) return
213
214 for (const file of (hls.VideoFiles || [])) {
215 await updateTorrentMetadata(hls, file)
216
217 await file.save()
218 }
219}
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 6773b500f..1be87f746 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -8,6 +8,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
9import { generateWebTorrentVideoFilename } from '@server/lib/paths' 9import { generateWebTorrentVideoFilename } from '@server/lib/paths'
10import { Redis } from '@server/lib/redis' 10import { Redis } from '@server/lib/redis'
11import { uploadx } from '@server/lib/uploadx'
11import { 12import {
12 addMoveToObjectStorageJob, 13 addMoveToObjectStorageJob,
13 addOptimizeOrMergeAudioJob, 14 addOptimizeOrMergeAudioJob,
@@ -19,7 +20,6 @@ import { VideoPathManager } from '@server/lib/video-path-manager'
19import { buildNextVideoState } from '@server/lib/video-state' 20import { buildNextVideoState } from '@server/lib/video-state'
20import { openapiOperationDoc } from '@server/middlewares/doc' 21import { openapiOperationDoc } from '@server/middlewares/doc'
21import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 22import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
22import { Uploadx } from '@uploadx/core'
23import { VideoCreate, VideoState } from '../../../../shared' 23import { VideoCreate, VideoState } from '../../../../shared'
24import { HttpStatusCode } from '../../../../shared/models' 24import { HttpStatusCode } from '../../../../shared/models'
25import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 25import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
@@ -41,8 +41,8 @@ import {
41 authenticate, 41 authenticate,
42 videosAddLegacyValidator, 42 videosAddLegacyValidator,
43 videosAddResumableInitValidator, 43 videosAddResumableInitValidator,
44 videosResumableUploadIdValidator, 44 videosAddResumableValidator,
45 videosAddResumableValidator 45 videosResumableUploadIdValidator
46} from '../../../middlewares' 46} from '../../../middlewares'
47import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 47import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
48import { VideoModel } from '../../../models/video/video' 48import { VideoModel } from '../../../models/video/video'
@@ -52,9 +52,6 @@ const lTags = loggerTagsFactory('api', 'video')
52const auditLogger = auditLoggerFactory('videos') 52const auditLogger = auditLoggerFactory('videos')
53const uploadRouter = express.Router() 53const uploadRouter = express.Router()
54 54
55const uploadx = new Uploadx({ directory: getResumableUploadPath() })
56uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
57
58const reqVideoFileAdd = createReqFiles( 55const reqVideoFileAdd = createReqFiles(
59 [ 'videofile', 'thumbnailfile', 'previewfile' ], 56 [ 'videofile', 'thumbnailfile', 'previewfile' ],
60 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), 57 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
@@ -156,7 +153,8 @@ async function addVideo (options: {
156 const videoChannel = res.locals.videoChannel 153 const videoChannel = res.locals.videoChannel
157 const user = res.locals.oauth.token.User 154 const user = res.locals.oauth.token.User
158 155
159 const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id) 156 let videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
157 videoData = await Hooks.wrapObject(videoData, 'filter:api.video.upload.video-attribute.result')
160 158
161 videoData.state = buildNextVideoState() 159 videoData.state = buildNextVideoState()
162 videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware 160 videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
diff --git a/server/helpers/actors.ts b/server/helpers/actors.ts
new file mode 100644
index 000000000..c31fe6f8e
--- /dev/null
+++ b/server/helpers/actors.ts
@@ -0,0 +1,17 @@
1import { WEBSERVER } from '@server/initializers/constants'
2
3function handleToNameAndHost (handle: string) {
4 let [ name, host ] = handle.split('@')
5 if (host === WEBSERVER.HOST) host = null
6
7 return { name, host, handle }
8}
9
10function handlesToNameAndHost (handles: string[]) {
11 return handles.map(h => handleToNameAndHost(h))
12}
13
14export {
15 handleToNameAndHost,
16 handlesToNameAndHost
17}
diff --git a/server/helpers/decache.ts b/server/helpers/decache.ts
new file mode 100644
index 000000000..e31973b7a
--- /dev/null
+++ b/server/helpers/decache.ts
@@ -0,0 +1,78 @@
1// Thanks: https://github.com/dwyl/decache
2// We reuse this file to also uncache plugin base path
3
4import { extname } from 'path'
5
6function decachePlugin (pluginPath: string, libraryPath: string) {
7 const moduleName = find(libraryPath)
8
9 if (!moduleName) return
10
11 searchCache(moduleName, function (mod) {
12 delete require.cache[mod.id]
13 })
14
15 removeCachedPath(pluginPath)
16}
17
18function decacheModule (name: string) {
19 const moduleName = find(name)
20
21 if (!moduleName) return
22
23 searchCache(moduleName, function (mod) {
24 delete require.cache[mod.id]
25 })
26
27 removeCachedPath(moduleName)
28}
29
30// ---------------------------------------------------------------------------
31
32export {
33 decacheModule,
34 decachePlugin
35}
36
37// ---------------------------------------------------------------------------
38
39function find (moduleName: string) {
40 try {
41 return require.resolve(moduleName)
42 } catch {
43 return ''
44 }
45}
46
47function searchCache (moduleName: string, callback: (current: NodeModule) => void) {
48 const resolvedModule = require.resolve(moduleName)
49 let mod: NodeModule
50 const visited = {}
51
52 if (resolvedModule && ((mod = require.cache[resolvedModule]) !== undefined)) {
53 // Recursively go over the results
54 (function run (current) {
55 visited[current.id] = true
56
57 current.children.forEach(function (child) {
58 if (extname(child.filename) !== '.node' && !visited[child.id]) {
59 run(child)
60 }
61 })
62
63 // Call the specified callback providing the
64 // found module
65 callback(current)
66 })(mod)
67 }
68};
69
70function removeCachedPath (pluginPath: string) {
71 const pathCache = (module.constructor as any)._pathCache
72
73 Object.keys(pathCache).forEach(function (cacheKey) {
74 if (cacheKey.includes(pluginPath)) {
75 delete pathCache[cacheKey]
76 }
77 })
78}
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index c75c058e4..ecc703646 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -14,7 +14,7 @@ import { MVideo } from '@server/types/models/video/video'
14import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/types/models/video/video-file' 14import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/types/models/video/video-file'
15import { MStreamingPlaylistVideo } from '@server/types/models/video/video-streaming-playlist' 15import { MStreamingPlaylistVideo } from '@server/types/models/video/video-streaming-playlist'
16import { CONFIG } from '../initializers/config' 16import { CONFIG } from '../initializers/config'
17import { promisify2 } from './core-utils' 17import { promisify2, sha1 } from './core-utils'
18import { logger } from './logger' 18import { logger } from './logger'
19import { generateVideoImportTmpPath } from './utils' 19import { generateVideoImportTmpPath } from './utils'
20import { extractVideo } from './video' 20import { extractVideo } from './video'
@@ -94,7 +94,7 @@ function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlayli
94 94
95 const options = { 95 const options = {
96 // Keep the extname, it's used by the client to stream the file inside a web browser 96 // Keep the extname, it's used by the client to stream the file inside a web browser
97 name: `${video.name} ${videoFile.resolution}p${videoFile.extname}`, 97 name: buildInfoName(video, videoFile),
98 createdBy: 'PeerTube', 98 createdBy: 'PeerTube',
99 announceList: buildAnnounceList(), 99 announceList: buildAnnounceList(),
100 urlList: buildUrlList(video, videoFile) 100 urlList: buildUrlList(video, videoFile)
@@ -120,7 +120,7 @@ function createTorrentAndSetInfoHash (videoOrPlaylist: MVideo | MStreamingPlayli
120 }) 120 })
121} 121}
122 122
123async function updateTorrentUrls (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { 123async function updateTorrentMetadata (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) {
124 const video = extractVideo(videoOrPlaylist) 124 const video = extractVideo(videoOrPlaylist)
125 125
126 const oldTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename) 126 const oldTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename)
@@ -133,15 +133,19 @@ async function updateTorrentUrls (videoOrPlaylist: MVideo | MStreamingPlaylistVi
133 133
134 decoded['url-list'] = buildUrlList(video, videoFile) 134 decoded['url-list'] = buildUrlList(video, videoFile)
135 135
136 decoded.info.name = buildInfoName(video, videoFile)
137 decoded['creation date'] = Math.ceil(Date.now() / 1000)
138
136 const newTorrentFilename = generateTorrentFileName(videoOrPlaylist, videoFile.resolution) 139 const newTorrentFilename = generateTorrentFileName(videoOrPlaylist, videoFile.resolution)
137 const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, newTorrentFilename) 140 const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, newTorrentFilename)
138 141
139 logger.info('Updating torrent URLs %s -> %s.', oldTorrentPath, newTorrentPath) 142 logger.info('Updating torrent metadata %s -> %s.', oldTorrentPath, newTorrentPath)
140 143
141 await writeFile(newTorrentPath, encode(decoded)) 144 await writeFile(newTorrentPath, encode(decoded))
142 await remove(join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename)) 145 await remove(join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename))
143 146
144 videoFile.torrentFilename = newTorrentFilename 147 videoFile.torrentFilename = newTorrentFilename
148 videoFile.infoHash = sha1(encode(decoded.info))
145} 149}
146 150
147function generateMagnetUri ( 151function generateMagnetUri (
@@ -171,7 +175,7 @@ function generateMagnetUri (
171 175
172export { 176export {
173 createTorrentPromise, 177 createTorrentPromise,
174 updateTorrentUrls, 178 updateTorrentMetadata,
175 createTorrentAndSetInfoHash, 179 createTorrentAndSetInfoHash,
176 generateMagnetUri, 180 generateMagnetUri,
177 downloadWebTorrentVideo 181 downloadWebTorrentVideo
@@ -226,3 +230,7 @@ function buildAnnounceList () {
226function buildUrlList (video: MVideo, videoFile: MVideoFile) { 230function buildUrlList (video: MVideo, videoFile: MVideoFile) {
227 return [ videoFile.getFileUrl(video) ] 231 return [ videoFile.getFileUrl(video) ]
228} 232}
233
234function buildInfoName (video: MVideo, videoFile: MVideoFile) {
235 return `${video.name} ${videoFile.resolution}p${videoFile.extname}`
236}
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 51c396548..c85c389cd 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -33,6 +33,7 @@ function checkMissedConfig () {
33 'transcoding.resolutions.2160p', 33 'transcoding.resolutions.2160p',
34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled', 34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled',
35 'trending.videos.interval_days', 35 'trending.videos.interval_days',
36 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth',
36 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 37 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
37 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt', 38 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
38 'services.twitter.username', 'services.twitter.whitelisted', 39 'services.twitter.username', 'services.twitter.whitelisted',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index dadda2a77..eb848be6b 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -1,7 +1,7 @@
1import bytes from 'bytes' 1import bytes from 'bytes'
2import { IConfig } from 'config' 2import { IConfig } from 'config'
3import decache from 'decache'
4import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { decacheModule } from '@server/helpers/decache'
5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' 5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
6import { BroadcastMessageLevel } from '@shared/models/server' 6import { BroadcastMessageLevel } from '@shared/models/server'
7import { VideosRedundancyStrategy } from '../../shared/models' 7import { VideosRedundancyStrategy } from '../../shared/models'
@@ -63,6 +63,11 @@ const CONFIG = {
63 MINIATURE: { 63 MINIATURE: {
64 get PREFER_AUTHOR_DISPLAY_NAME () { return config.get<boolean>('client.videos.miniature.prefer_author_display_name') } 64 get PREFER_AUTHOR_DISPLAY_NAME () { return config.get<boolean>('client.videos.miniature.prefer_author_display_name') }
65 } 65 }
66 },
67 MENU: {
68 LOGIN: {
69 get REDIRECT_ON_SINGLE_EXTERNAL_AUTH () { return config.get<boolean>('client.menu.login.redirect_on_single_external_auth') }
70 }
66 } 71 }
67 }, 72 },
68 73
@@ -497,7 +502,7 @@ export function reloadConfig () {
497 delete require.cache[fileName] 502 delete require.cache[fileName]
498 } 503 }
499 504
500 decache('config') 505 decacheModule('config')
501 } 506 }
502 507
503 purge() 508 purge()
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index b8633e83e..c61c01d62 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -223,7 +223,7 @@ const SCHEDULER_INTERVALS_MS = {
223 REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day 223 REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day
224 REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day 224 REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day
225 UPDATE_INBOX_STATS: 1000 * 60, // 1 minute 225 UPDATE_INBOX_STATS: 1000 * 60, // 1 minute
226 REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 * 16 // 16 hours 226 REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 // 1 hour
227} 227}
228 228
229// --------------------------------------------------------------------------- 229// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index 7ed409d0e..fd3e46e2b 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -75,7 +75,8 @@ async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byAc
75 endAt, 75 endAt,
76 reporterAccount, 76 reporterAccount,
77 transaction: t, 77 transaction: t,
78 videoInstance: video 78 videoInstance: video,
79 skipNotification: false
79 }) 80 })
80 } 81 }
81 82
@@ -84,7 +85,8 @@ async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byAc
84 baseAbuse, 85 baseAbuse,
85 reporterAccount, 86 reporterAccount,
86 transaction: t, 87 transaction: t,
87 commentInstance: videoComment 88 commentInstance: videoComment,
89 skipNotification: false
88 }) 90 })
89 } 91 }
90 92
@@ -92,7 +94,8 @@ async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byAc
92 baseAbuse, 94 baseAbuse,
93 reporterAccount, 95 reporterAccount,
94 transaction: t, 96 transaction: t,
95 accountInstance: flaggedAccount 97 accountInstance: flaggedAccount,
98 skipNotification: false
96 }) 99 })
97 }) 100 })
98 } catch (err) { 101 } catch (err) {
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
index d6b684015..98273a6ea 100644
--- a/server/lib/blocklist.ts
+++ b/server/lib/blocklist.ts
@@ -40,12 +40,12 @@ async function isBlockedByServerOrAccount (targetAccount: MAccountServer, userAc
40 40
41 if (userAccount) sourceAccounts.push(userAccount.id) 41 if (userAccount) sourceAccounts.push(userAccount.id)
42 42
43 const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id) 43 const accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, targetAccount.id)
44 if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) { 44 if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) {
45 return true 45 return true
46 } 46 }
47 47
48 const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId) 48 const instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, targetAccount.Actor.serverId)
49 if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) { 49 if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) {
50 return true 50 return true
51 } 51 }
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 360b4667f..adc3d712e 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -350,10 +350,6 @@ class ClientHtml {
350 return join(__dirname, '../../../client/dist/standalone/videos/embed.html') 350 return join(__dirname, '../../../client/dist/standalone/videos/embed.html')
351 } 351 }
352 352
353 private static addHtmlLang (htmlStringPage: string, paramLang: string) {
354 return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`)
355 }
356
357 private static addManifestContentHash (htmlStringPage: string) { 353 private static addManifestContentHash (htmlStringPage: string) {
358 return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) 354 return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST)
359 } 355 }
diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts
index 54a7c566b..b5eea0184 100644
--- a/server/lib/job-queue/handlers/move-to-object-storage.ts
+++ b/server/lib/job-queue/handlers/move-to-object-storage.ts
@@ -2,7 +2,7 @@ import { Job } from 'bull'
2import { remove } from 'fs-extra' 2import { remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { logger } from '@server/helpers/logger' 4import { logger } from '@server/helpers/logger'
5import { updateTorrentUrls } from '@server/helpers/webtorrent' 5import { updateTorrentMetadata } from '@server/helpers/webtorrent'
6import { CONFIG } from '@server/initializers/config' 6import { CONFIG } from '@server/initializers/config'
7import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' 7import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants'
8import { storeHLSFile, storeWebTorrentFile } from '@server/lib/object-storage' 8import { storeHLSFile, storeWebTorrentFile } from '@server/lib/object-storage'
@@ -113,7 +113,7 @@ async function onFileMoved (options: {
113 file.fileUrl = fileUrl 113 file.fileUrl = fileUrl
114 file.storage = VideoStorage.OBJECT_STORAGE 114 file.storage = VideoStorage.OBJECT_STORAGE
115 115
116 await updateTorrentUrls(videoOrPlaylist, file) 116 await updateTorrentMetadata(videoOrPlaylist, file)
117 await file.save() 117 await file.save()
118 118
119 logger.debug('Removing %s because it\'s now on object storage', oldPath) 119 logger.debug('Removing %s because it\'s now on object storage', oldPath)
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts
index 456b615b2..c2565f867 100644
--- a/server/lib/moderation.ts
+++ b/server/lib/moderation.ts
@@ -107,8 +107,9 @@ async function createVideoAbuse (options: {
107 endAt: number 107 endAt: number
108 transaction: Transaction 108 transaction: Transaction
109 reporterAccount: MAccountDefault 109 reporterAccount: MAccountDefault
110 skipNotification: boolean
110}) { 111}) {
111 const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount } = options 112 const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount, skipNotification } = options
112 113
113 const associateFun = async (abuseInstance: MAbuseFull) => { 114 const associateFun = async (abuseInstance: MAbuseFull) => {
114 const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({ 115 const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({
@@ -129,6 +130,7 @@ async function createVideoAbuse (options: {
129 reporterAccount, 130 reporterAccount,
130 flaggedAccount: videoInstance.VideoChannel.Account, 131 flaggedAccount: videoInstance.VideoChannel.Account,
131 transaction, 132 transaction,
133 skipNotification,
132 associateFun 134 associateFun
133 }) 135 })
134} 136}
@@ -138,8 +140,9 @@ function createVideoCommentAbuse (options: {
138 commentInstance: MCommentOwnerVideo 140 commentInstance: MCommentOwnerVideo
139 transaction: Transaction 141 transaction: Transaction
140 reporterAccount: MAccountDefault 142 reporterAccount: MAccountDefault
143 skipNotification: boolean
141}) { 144}) {
142 const { baseAbuse, commentInstance, transaction, reporterAccount } = options 145 const { baseAbuse, commentInstance, transaction, reporterAccount, skipNotification } = options
143 146
144 const associateFun = async (abuseInstance: MAbuseFull) => { 147 const associateFun = async (abuseInstance: MAbuseFull) => {
145 const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({ 148 const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({
@@ -158,6 +161,7 @@ function createVideoCommentAbuse (options: {
158 reporterAccount, 161 reporterAccount,
159 flaggedAccount: commentInstance.Account, 162 flaggedAccount: commentInstance.Account,
160 transaction, 163 transaction,
164 skipNotification,
161 associateFun 165 associateFun
162 }) 166 })
163} 167}
@@ -167,8 +171,9 @@ function createAccountAbuse (options: {
167 accountInstance: MAccountDefault 171 accountInstance: MAccountDefault
168 transaction: Transaction 172 transaction: Transaction
169 reporterAccount: MAccountDefault 173 reporterAccount: MAccountDefault
174 skipNotification: boolean
170}) { 175}) {
171 const { baseAbuse, accountInstance, transaction, reporterAccount } = options 176 const { baseAbuse, accountInstance, transaction, reporterAccount, skipNotification } = options
172 177
173 const associateFun = () => { 178 const associateFun = () => {
174 return Promise.resolve({ isOwned: accountInstance.isOwned() }) 179 return Promise.resolve({ isOwned: accountInstance.isOwned() })
@@ -179,6 +184,7 @@ function createAccountAbuse (options: {
179 reporterAccount, 184 reporterAccount,
180 flaggedAccount: accountInstance, 185 flaggedAccount: accountInstance,
181 transaction, 186 transaction,
187 skipNotification,
182 associateFun 188 associateFun
183 }) 189 })
184} 190}
@@ -207,9 +213,10 @@ async function createAbuse (options: {
207 reporterAccount: MAccountDefault 213 reporterAccount: MAccountDefault
208 flaggedAccount: MAccountLight 214 flaggedAccount: MAccountLight
209 associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} > 215 associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} >
216 skipNotification: boolean
210 transaction: Transaction 217 transaction: Transaction
211}) { 218}) {
212 const { base, reporterAccount, flaggedAccount, associateFun, transaction } = options 219 const { base, reporterAccount, flaggedAccount, associateFun, transaction, skipNotification } = options
213 const auditLogger = auditLoggerFactory('abuse') 220 const auditLogger = auditLoggerFactory('abuse')
214 221
215 const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id }) 222 const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id })
@@ -227,13 +234,15 @@ async function createAbuse (options: {
227 const abuseJSON = abuseInstance.toFormattedAdminJSON() 234 const abuseJSON = abuseInstance.toFormattedAdminJSON()
228 auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON)) 235 auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON))
229 236
230 afterCommitIfTransaction(transaction, () => { 237 if (!skipNotification) {
231 Notifier.Instance.notifyOnNewAbuse({ 238 afterCommitIfTransaction(transaction, () => {
232 abuse: abuseJSON, 239 Notifier.Instance.notifyOnNewAbuse({
233 abuseInstance, 240 abuse: abuseJSON,
234 reporter: reporterAccount.Actor.getIdentifier() 241 abuseInstance,
242 reporter: reporterAccount.Actor.getIdentifier()
243 })
235 }) 244 })
236 }) 245 }
237 246
238 logger.info('Abuse report %d created.', abuseInstance.id) 247 logger.info('Abuse report %d created.', abuseInstance.id)
239 248
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts
index 4f84d8dea..765cbaad9 100644
--- a/server/lib/notifier/shared/comment/comment-mention.ts
+++ b/server/lib/notifier/shared/comment/comment-mention.ts
@@ -47,8 +47,8 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU
47 47
48 const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ]) 48 const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ])
49 49
50 this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, this.payload.accountId) 50 this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, this.payload.accountId)
51 this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, this.payload.Account.Actor.serverId) 51 this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, this.payload.Account.Actor.serverId)
52 } 52 }
53 53
54 log () { 54 log () {
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index d4d2a7edc..6c2f4764e 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -1,8 +1,8 @@
1import decache from 'decache'
2import express from 'express' 1import express from 'express'
3import { createReadStream, createWriteStream } from 'fs' 2import { createReadStream, createWriteStream } from 'fs'
4import { ensureDir, outputFile, readJSON } from 'fs-extra' 3import { ensureDir, outputFile, readJSON } from 'fs-extra'
5import { basename, join } from 'path' 4import { basename, join } from 'path'
5import { decachePlugin } from '@server/helpers/decache'
6import { MOAuthTokenUser, MUser } from '@server/types/models' 6import { MOAuthTokenUser, MUser } from '@server/types/models'
7import { getCompleteLocale } from '@shared/core-utils' 7import { getCompleteLocale } from '@shared/core-utils'
8import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models' 8import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models'
@@ -420,7 +420,7 @@ export class PluginManager implements ServerHook {
420 420
421 // Delete cache if needed 421 // Delete cache if needed
422 const modulePath = join(pluginPath, packageJSON.library) 422 const modulePath = join(pluginPath, packageJSON.library)
423 decache(modulePath) 423 decachePlugin(pluginPath, modulePath)
424 const library: PluginLibrary = require(modulePath) 424 const library: PluginLibrary = require(modulePath)
425 425
426 if (!isLibraryCodeValid(library)) { 426 if (!isLibraryCodeValid(library)) {
diff --git a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
index d6e561cad..61e93eafa 100644
--- a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
+++ b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
@@ -1,9 +1,7 @@
1import { map } from 'bluebird' 1
2import { readdir, remove, stat } from 'fs-extra'
3import { logger, loggerTagsFactory } from '@server/helpers/logger' 2import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { getResumableUploadPath } from '@server/helpers/upload'
5import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants' 3import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants'
6import { METAFILE_EXTNAME } from '@uploadx/core' 4import { uploadx } from '../uploadx'
7import { AbstractScheduler } from './abstract-scheduler' 5import { AbstractScheduler } from './abstract-scheduler'
8 6
9const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner') 7const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner')
@@ -22,36 +20,17 @@ export class RemoveDanglingResumableUploadsScheduler extends AbstractScheduler {
22 } 20 }
23 21
24 protected async internalExecute () { 22 protected async internalExecute () {
25 const path = getResumableUploadPath() 23 logger.debug('Removing dangling resumable uploads', lTags())
26 const files = await readdir(path)
27
28 const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME))
29 24
30 if (metafiles.length === 0) return 25 const now = new Date().getTime()
31
32 logger.debug('Reading resumable video upload folder %s with %d files', path, metafiles.length, lTags())
33 26
34 try { 27 try {
35 await map(metafiles, metafile => { 28 // Remove files that were not updated since the last execution
36 return this.deleteIfOlderThan(metafile, this.lastExecutionTimeMs) 29 await uploadx.storage.purge(now - this.lastExecutionTimeMs)
37 }, { concurrency: 5 })
38 } catch (error) { 30 } catch (error) {
39 logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() }) 31 logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() })
40 } finally { 32 } finally {
41 this.lastExecutionTimeMs = new Date().getTime() 33 this.lastExecutionTimeMs = now
42 }
43 }
44
45 private async deleteIfOlderThan (metafile: string, olderThan: number) {
46 const metafilePath = getResumableUploadPath(metafile)
47 const statResult = await stat(metafilePath)
48
49 // Delete uploads that started since a long time
50 if (statResult.ctimeMs < olderThan) {
51 await remove(metafilePath)
52
53 const datafile = metafilePath.replace(new RegExp(`${METAFILE_EXTNAME}$`), '')
54 await remove(datafile)
55 } 34 }
56 } 35 }
57 36
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts
index bdf6492f9..6aa459f82 100644
--- a/server/lib/server-config-manager.ts
+++ b/server/lib/server-config-manager.ts
@@ -47,6 +47,11 @@ class ServerConfigManager {
47 miniature: { 47 miniature: {
48 preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME 48 preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
49 } 49 }
50 },
51 menu: {
52 login: {
53 redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
54 }
50 } 55 }
51 }, 56 },
52 57
diff --git a/server/lib/uploadx.ts b/server/lib/uploadx.ts
new file mode 100644
index 000000000..11b1044db
--- /dev/null
+++ b/server/lib/uploadx.ts
@@ -0,0 +1,10 @@
1import express from 'express'
2import { getResumableUploadPath } from '@server/helpers/upload'
3import { Uploadx } from '@uploadx/core'
4
5const uploadx = new Uploadx({ directory: getResumableUploadPath() })
6uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
7
8export {
9 uploadx
10}
diff --git a/server/middlewares/error.ts b/server/middlewares/error.ts
index 6c52ce7bd..34c87a26d 100644
--- a/server/middlewares/error.ts
+++ b/server/middlewares/error.ts
@@ -1,5 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details' 2import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
3import { logger } from '@server/helpers/logger'
3import { HttpStatusCode } from '@shared/models' 4import { HttpStatusCode } from '@shared/models'
4 5
5function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) { 6function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -18,7 +19,8 @@ function apiFailMiddleware (req: express.Request, res: express.Response, next: e
18 19
19 res.status(status) 20 res.status(status)
20 res.setHeader('Content-Type', 'application/problem+json') 21 res.setHeader('Content-Type', 'application/problem+json')
21 res.json(new ProblemDocument({ 22
23 const json = new ProblemDocument({
22 status, 24 status,
23 title, 25 title,
24 instance, 26 instance,
@@ -28,7 +30,11 @@ function apiFailMiddleware (req: express.Request, res: express.Response, next: e
28 type: type 30 type: type
29 ? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}` 31 ? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}`
30 : undefined 32 : undefined
31 }, extension)) 33 }, extension)
34
35 logger.debug('Bad HTTP request.', { json })
36
37 res.json(json)
32 } 38 }
33 39
34 if (next) next() 40 if (next) next()
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index b7749e204..12980ced4 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -1,8 +1,10 @@
1import express from 'express' 1import express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor'
4import { toArray } from '@server/helpers/custom-validators/misc'
3import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
4import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' 6import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
5import { isHostValid } from '../../helpers/custom-validators/servers' 7import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
6import { logger } from '../../helpers/logger' 8import { logger } from '../../helpers/logger'
7import { WEBSERVER } from '../../initializers/constants' 9import { WEBSERVER } from '../../initializers/constants'
8import { AccountBlocklistModel } from '../../models/account/account-blocklist' 10import { AccountBlocklistModel } from '../../models/account/account-blocklist'
@@ -123,6 +125,26 @@ const unblockServerByServerValidator = [
123 } 125 }
124] 126]
125 127
128const blocklistStatusValidator = [
129 query('hosts')
130 .optional()
131 .customSanitizer(toArray)
132 .custom(isEachUniqueHostValid).withMessage('Should have a valid hosts array'),
133
134 query('accounts')
135 .optional()
136 .customSanitizer(toArray)
137 .custom(areValidActorHandles).withMessage('Should have a valid accounts array'),
138
139 (req: express.Request, res: express.Response, next: express.NextFunction) => {
140 logger.debug('Checking blocklistStatusValidator parameters', { query: req.query })
141
142 if (areValidationErrors(req, res)) return
143
144 return next()
145 }
146]
147
126// --------------------------------------------------------------------------- 148// ---------------------------------------------------------------------------
127 149
128export { 150export {
@@ -131,7 +153,8 @@ export {
131 unblockAccountByAccountValidator, 153 unblockAccountByAccountValidator,
132 unblockServerByAccountValidator, 154 unblockServerByAccountValidator,
133 unblockAccountByServerValidator, 155 unblockAccountByServerValidator,
134 unblockServerByServerValidator 156 unblockServerByServerValidator,
157 blocklistStatusValidator
135} 158}
136 159
137// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index 21171af23..c1e9ebefb 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -116,6 +116,9 @@ const installOrUpdatePluginValidator = [
116 body('npmName') 116 body('npmName')
117 .optional() 117 .optional()
118 .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), 118 .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
119 body('pluginVersion')
120 .optional()
121 .custom(isPluginVersionValid).withMessage('Should have a valid plugin version'),
119 body('path') 122 body('path')
120 .optional() 123 .optional()
121 .custom(isSafePath).withMessage('Should have a valid safe path'), 124 .custom(isSafePath).withMessage('Should have a valid safe path'),
@@ -129,6 +132,9 @@ const installOrUpdatePluginValidator = [
129 if (!body.path && !body.npmName) { 132 if (!body.path && !body.npmName) {
130 return res.fail({ message: 'Should have either a npmName or a path' }) 133 return res.fail({ message: 'Should have either a npmName or a path' })
131 } 134 }
135 if (body.pluginVersion && !body.npmName) {
136 return res.fail({ message: 'Should have a npmName when specifying a pluginVersion' })
137 }
132 138
133 return next() 139 return next()
134 } 140 }
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index b2375b006..21983428a 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -1,11 +1,12 @@
1import { Op } from 'sequelize' 1import { Op, QueryTypes } from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { handlesToNameAndHost } from '@server/helpers/actors'
3import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' 4import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
4import { AttributesOnly } from '@shared/core-utils' 5import { AttributesOnly } from '@shared/core-utils'
5import { AccountBlock } from '../../../shared/models' 6import { AccountBlock } from '../../../shared/models'
6import { ActorModel } from '../actor/actor' 7import { ActorModel } from '../actor/actor'
7import { ServerModel } from '../server/server' 8import { ServerModel } from '../server/server'
8import { getSort, searchAttribute } from '../utils' 9import { createSafeIn, getSort, searchAttribute } from '../utils'
9import { AccountModel } from './account' 10import { AccountModel } from './account'
10 11
11enum ScopeNames { 12enum ScopeNames {
@@ -77,7 +78,7 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
77 }) 78 })
78 BlockedAccount: AccountModel 79 BlockedAccount: AccountModel
79 80
80 static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) { 81 static isAccountMutedByAccounts (accountIds: number[], targetAccountId: number) {
81 const query = { 82 const query = {
82 attributes: [ 'accountId', 'id' ], 83 attributes: [ 'accountId', 'id' ],
83 where: { 84 where: {
@@ -187,6 +188,39 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
187 .then(entries => entries.map(e => `${e.BlockedAccount.Actor.preferredUsername}@${e.BlockedAccount.Actor.Server.host}`)) 188 .then(entries => entries.map(e => `${e.BlockedAccount.Actor.preferredUsername}@${e.BlockedAccount.Actor.Server.host}`))
188 } 189 }
189 190
191 static getBlockStatus (byAccountIds: number[], handles: string[]): Promise<{ name: string, host: string, accountId: number }[]> {
192 const sanitizedHandles = handlesToNameAndHost(handles)
193
194 const localHandles = sanitizedHandles.filter(h => !h.host)
195 .map(h => h.name)
196
197 const remoteHandles = sanitizedHandles.filter(h => !!h.host)
198 .map(h => ([ h.name, h.host ]))
199
200 const handlesWhere: string[] = []
201
202 if (localHandles.length !== 0) {
203 handlesWhere.push(`("actor"."preferredUsername" IN (:localHandles) AND "server"."id" IS NULL)`)
204 }
205
206 if (remoteHandles.length !== 0) {
207 handlesWhere.push(`(("actor"."preferredUsername", "server"."host") IN (:remoteHandles))`)
208 }
209
210 const rawQuery = `SELECT "accountBlocklist"."accountId", "actor"."preferredUsername" AS "name", "server"."host" ` +
211 `FROM "accountBlocklist" ` +
212 `INNER JOIN "account" ON "account"."id" = "accountBlocklist"."targetAccountId" ` +
213 `INNER JOIN "actor" ON "actor"."id" = "account"."actorId" ` +
214 `LEFT JOIN "server" ON "server"."id" = "actor"."serverId" ` +
215 `WHERE "accountBlocklist"."accountId" IN (${createSafeIn(AccountBlocklistModel.sequelize, byAccountIds)}) ` +
216 `AND (${handlesWhere.join(' OR ')})`
217
218 return AccountBlocklistModel.sequelize.query(rawQuery, {
219 type: QueryTypes.SELECT as QueryTypes.SELECT,
220 replacements: { byAccountIds, localHandles, remoteHandles }
221 })
222 }
223
190 toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock { 224 toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock {
191 return { 225 return {
192 byAccount: this.ByAccount.toFormattedJSON(), 226 byAccount: this.ByAccount.toFormattedJSON(),
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index b3579d589..092998db3 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -1,10 +1,10 @@
1import { Op } from 'sequelize' 1import { Op, QueryTypes } from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' 3import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
4import { AttributesOnly } from '@shared/core-utils' 4import { AttributesOnly } from '@shared/core-utils'
5import { ServerBlock } from '@shared/models' 5import { ServerBlock } from '@shared/models'
6import { AccountModel } from '../account/account' 6import { AccountModel } from '../account/account'
7import { getSort, searchAttribute } from '../utils' 7import { createSafeIn, getSort, searchAttribute } from '../utils'
8import { ServerModel } from './server' 8import { ServerModel } from './server'
9 9
10enum ScopeNames { 10enum ScopeNames {
@@ -76,7 +76,7 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
76 }) 76 })
77 BlockedServer: ServerModel 77 BlockedServer: ServerModel
78 78
79 static isServerMutedByMulti (accountIds: number[], targetServerId: number) { 79 static isServerMutedByAccounts (accountIds: number[], targetServerId: number) {
80 const query = { 80 const query = {
81 attributes: [ 'accountId', 'id' ], 81 attributes: [ 'accountId', 'id' ],
82 where: { 82 where: {
@@ -141,6 +141,19 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
141 .then(entries => entries.map(e => e.BlockedServer.host)) 141 .then(entries => entries.map(e => e.BlockedServer.host))
142 } 142 }
143 143
144 static getBlockStatus (byAccountIds: number[], hosts: string[]): Promise<{ host: string, accountId: number }[]> {
145 const rawQuery = `SELECT "server"."host", "serverBlocklist"."accountId" ` +
146 `FROM "serverBlocklist" ` +
147 `INNER JOIN "server" ON "server"."id" = "serverBlocklist"."targetServerId" ` +
148 `WHERE "server"."host" IN (:hosts) ` +
149 `AND "serverBlocklist"."accountId" IN (${createSafeIn(ServerBlocklistModel.sequelize, byAccountIds)})`
150
151 return ServerBlocklistModel.sequelize.query(rawQuery, {
152 type: QueryTypes.SELECT as QueryTypes.SELECT,
153 replacements: { hosts }
154 })
155 }
156
144 static listForApi (parameters: { 157 static listForApi (parameters: {
145 start: number 158 start: number
146 count: number 159 count: number
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index 82c832188..a87b2bcae 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -276,7 +276,7 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
276 } 276 }
277 277
278 const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) 278 const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`)
279 return VideoPlaylistElementModel.update({ position: positionQuery as any }, query) 279 return VideoPlaylistElementModel.update({ position: positionQuery }, query)
280 } 280 }
281 281
282 static increasePositionOf ( 282 static increasePositionOf (
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index 4643c5452..e36852cad 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -198,6 +198,15 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
198 return Object.assign(playlist, { videoId: video.id, Video: video }) 198 return Object.assign(playlist, { videoId: video.id, Video: video })
199 } 199 }
200 200
201 static doesOwnedHLSPlaylistExist (videoUUID: string) {
202 const query = `SELECT 1 FROM "videoStreamingPlaylist" ` +
203 `INNER JOIN "video" ON "video"."id" = "videoStreamingPlaylist"."videoId" ` +
204 `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` +
205 `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1`
206
207 return doesExist(query, { videoUUID })
208 }
209
201 assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { 210 assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) {
202 const masterPlaylistUrl = this.getMasterPlaylistUrl(video) 211 const masterPlaylistUrl = this.getMasterPlaylistUrl(video)
203 212
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
index 7d5fae5cf..f72a892e2 100644
--- a/server/tests/api/check-params/blocklist.ts
+++ b/server/tests/api/check-params/blocklist.ts
@@ -481,6 +481,78 @@ describe('Test blocklist API validators', function () {
481 }) 481 })
482 }) 482 })
483 483
484 describe('When getting blocklist status', function () {
485 const path = '/api/v1/blocklist/status'
486
487 it('Should fail with a bad token', async function () {
488 await makeGetRequest({
489 url: server.url,
490 path,
491 token: 'false',
492 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
493 })
494 })
495
496 it('Should fail with a bad accounts field', async function () {
497 await makeGetRequest({
498 url: server.url,
499 path,
500 query: {
501 accounts: 1
502 },
503 expectedStatus: HttpStatusCode.BAD_REQUEST_400
504 })
505
506 await makeGetRequest({
507 url: server.url,
508 path,
509 query: {
510 accounts: [ 1 ]
511 },
512 expectedStatus: HttpStatusCode.BAD_REQUEST_400
513 })
514 })
515
516 it('Should fail with a bad hosts field', async function () {
517 await makeGetRequest({
518 url: server.url,
519 path,
520 query: {
521 hosts: 1
522 },
523 expectedStatus: HttpStatusCode.BAD_REQUEST_400
524 })
525
526 await makeGetRequest({
527 url: server.url,
528 path,
529 query: {
530 hosts: [ 1 ]
531 },
532 expectedStatus: HttpStatusCode.BAD_REQUEST_400
533 })
534 })
535
536 it('Should succeed with the correct parameters', async function () {
537 await makeGetRequest({
538 url: server.url,
539 path,
540 query: {},
541 expectedStatus: HttpStatusCode.OK_200
542 })
543
544 await makeGetRequest({
545 url: server.url,
546 path,
547 query: {
548 hosts: [ 'example.com' ],
549 accounts: [ 'john@example.com' ]
550 },
551 expectedStatus: HttpStatusCode.OK_200
552 })
553 })
554 })
555
484 after(async function () { 556 after(async function () {
485 await cleanupTests(servers) 557 await cleanupTests(servers)
486 }) 558 })
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index d0cd7722b..a6e87730a 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -54,6 +54,18 @@ describe('Test config API validators', function () {
54 whitelisted: true 54 whitelisted: true
55 } 55 }
56 }, 56 },
57 client: {
58 videos: {
59 miniature: {
60 preferAuthorDisplayName: false
61 }
62 },
63 menu: {
64 login: {
65 redirectOnSingleExternalAuth: false
66 }
67 }
68 },
57 cache: { 69 cache: {
58 previews: { 70 previews: {
59 size: 2 71 size: 2
diff --git a/server/tests/api/check-params/plugins.ts b/server/tests/api/check-params/plugins.ts
index 33f84ecbc..2c436376c 100644
--- a/server/tests/api/check-params/plugins.ts
+++ b/server/tests/api/check-params/plugins.ts
@@ -30,7 +30,7 @@ describe('Test server plugins API validators', function () {
30 // --------------------------------------------------------------- 30 // ---------------------------------------------------------------
31 31
32 before(async function () { 32 before(async function () {
33 this.timeout(30000) 33 this.timeout(60000)
34 34
35 server = await createSingleServer(1) 35 server = await createSingleServer(1)
36 36
diff --git a/server/tests/api/moderation/blocklist.ts b/server/tests/api/moderation/blocklist.ts
index 089af8b15..b3fd8ecac 100644
--- a/server/tests/api/moderation/blocklist.ts
+++ b/server/tests/api/moderation/blocklist.ts
@@ -254,6 +254,45 @@ describe('Test blocklist', function () {
254 } 254 }
255 }) 255 })
256 256
257 it('Should get blocked status', async function () {
258 const remoteHandle = 'user2@' + servers[1].host
259 const localHandle = 'user1@' + servers[0].host
260 const unknownHandle = 'user5@' + servers[0].host
261
262 {
263 const status = await command.getStatus({ accounts: [ remoteHandle ] })
264 expect(Object.keys(status.accounts)).to.have.lengthOf(1)
265 expect(status.accounts[remoteHandle].blockedByUser).to.be.false
266 expect(status.accounts[remoteHandle].blockedByServer).to.be.false
267
268 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
269 }
270
271 {
272 const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] })
273 expect(Object.keys(status.accounts)).to.have.lengthOf(1)
274 expect(status.accounts[remoteHandle].blockedByUser).to.be.true
275 expect(status.accounts[remoteHandle].blockedByServer).to.be.false
276
277 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
278 }
279
280 {
281 const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] })
282 expect(Object.keys(status.accounts)).to.have.lengthOf(3)
283
284 for (const handle of [ localHandle, remoteHandle ]) {
285 expect(status.accounts[handle].blockedByUser).to.be.true
286 expect(status.accounts[handle].blockedByServer).to.be.false
287 }
288
289 expect(status.accounts[unknownHandle].blockedByUser).to.be.false
290 expect(status.accounts[unknownHandle].blockedByServer).to.be.false
291
292 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
293 }
294 })
295
257 it('Should not allow a remote blocked user to comment my videos', async function () { 296 it('Should not allow a remote blocked user to comment my videos', async function () {
258 this.timeout(60000) 297 this.timeout(60000)
259 298
@@ -434,6 +473,35 @@ describe('Test blocklist', function () {
434 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) 473 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
435 }) 474 })
436 475
476 it('Should get blocklist status', async function () {
477 const blockedServer = servers[1].host
478 const notBlockedServer = 'example.com'
479
480 {
481 const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] })
482 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
483
484 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
485 expect(status.hosts[blockedServer].blockedByUser).to.be.false
486 expect(status.hosts[blockedServer].blockedByServer).to.be.false
487
488 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
489 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
490 }
491
492 {
493 const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] })
494 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
495
496 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
497 expect(status.hosts[blockedServer].blockedByUser).to.be.true
498 expect(status.hosts[blockedServer].blockedByServer).to.be.false
499
500 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
501 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
502 }
503 })
504
437 it('Should unblock the remote server', async function () { 505 it('Should unblock the remote server', async function () {
438 await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port }) 506 await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port })
439 }) 507 })
@@ -575,6 +643,27 @@ describe('Test blocklist', function () {
575 } 643 }
576 }) 644 })
577 645
646 it('Should get blocked status', async function () {
647 const remoteHandle = 'user2@' + servers[1].host
648 const localHandle = 'user1@' + servers[0].host
649 const unknownHandle = 'user5@' + servers[0].host
650
651 for (const token of [ undefined, servers[0].accessToken ]) {
652 const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] })
653 expect(Object.keys(status.accounts)).to.have.lengthOf(3)
654
655 for (const handle of [ localHandle, remoteHandle ]) {
656 expect(status.accounts[handle].blockedByUser).to.be.false
657 expect(status.accounts[handle].blockedByServer).to.be.true
658 }
659
660 expect(status.accounts[unknownHandle].blockedByUser).to.be.false
661 expect(status.accounts[unknownHandle].blockedByServer).to.be.false
662
663 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
664 }
665 })
666
578 it('Should unblock the remote account', async function () { 667 it('Should unblock the remote account', async function () {
579 await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port }) 668 await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port })
580 }) 669 })
@@ -620,6 +709,7 @@ describe('Test blocklist', function () {
620 }) 709 })
621 710
622 describe('When managing server blocklist', function () { 711 describe('When managing server blocklist', function () {
712
623 it('Should list all videos', async function () { 713 it('Should list all videos', async function () {
624 for (const token of [ userModeratorToken, servers[0].accessToken ]) { 714 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
625 await checkAllVideos(servers[0], token) 715 await checkAllVideos(servers[0], token)
@@ -713,6 +803,23 @@ describe('Test blocklist', function () {
713 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) 803 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
714 }) 804 })
715 805
806 it('Should get blocklist status', async function () {
807 const blockedServer = servers[1].host
808 const notBlockedServer = 'example.com'
809
810 for (const token of [ undefined, servers[0].accessToken ]) {
811 const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] })
812 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
813
814 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
815 expect(status.hosts[blockedServer].blockedByUser).to.be.false
816 expect(status.hosts[blockedServer].blockedByServer).to.be.true
817
818 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
819 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
820 }
821 })
822
716 it('Should unblock the remote server', async function () { 823 it('Should unblock the remote server', async function () {
717 await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port }) 824 await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port })
718 }) 825 })
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts
index f806fed31..81ce8061b 100644
--- a/server/tests/api/notifications/moderation-notifications.ts
+++ b/server/tests/api/notifications/moderation-notifications.ts
@@ -24,11 +24,13 @@ import {
24 wait, 24 wait,
25 waitJobs 25 waitJobs
26} from '@shared/extra-utils' 26} from '@shared/extra-utils'
27import { AbuseState, CustomConfig, UserNotification, VideoPrivacy } from '@shared/models' 27import { AbuseState, CustomConfig, UserNotification, UserRole, VideoPrivacy } from '@shared/models'
28 28
29describe('Test moderation notifications', function () { 29describe('Test moderation notifications', function () {
30 let servers: PeerTubeServer[] = [] 30 let servers: PeerTubeServer[] = []
31 let userAccessToken: string 31 let userToken1: string
32 let userToken2: string
33
32 let userNotifications: UserNotification[] = [] 34 let userNotifications: UserNotification[] = []
33 let adminNotifications: UserNotification[] = [] 35 let adminNotifications: UserNotification[] = []
34 let adminNotificationsServer2: UserNotification[] = [] 36 let adminNotificationsServer2: UserNotification[] = []
@@ -39,11 +41,13 @@ describe('Test moderation notifications', function () {
39 41
40 const res = await prepareNotificationsTest(3) 42 const res = await prepareNotificationsTest(3)
41 emails = res.emails 43 emails = res.emails
42 userAccessToken = res.userAccessToken 44 userToken1 = res.userAccessToken
43 servers = res.servers 45 servers = res.servers
44 userNotifications = res.userNotifications 46 userNotifications = res.userNotifications
45 adminNotifications = res.adminNotifications 47 adminNotifications = res.adminNotifications
46 adminNotificationsServer2 = res.adminNotificationsServer2 48 adminNotificationsServer2 = res.adminNotificationsServer2
49
50 userToken2 = await servers[1].users.generateUserAndToken('user2', UserRole.USER)
47 }) 51 })
48 52
49 describe('Abuse for moderators notification', function () { 53 describe('Abuse for moderators notification', function () {
@@ -58,15 +62,27 @@ describe('Test moderation notifications', function () {
58 } 62 }
59 }) 63 })
60 64
61 it('Should send a notification to moderators on local video abuse', async function () { 65 it('Should not send a notification to moderators on local abuse reported by an admin', async function () {
62 this.timeout(20000) 66 this.timeout(20000)
63 67
64 const name = 'video for abuse ' + buildUUID() 68 const name = 'video for abuse ' + buildUUID()
65 const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 69 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
66 70
67 await servers[0].abuses.report({ videoId: video.id, reason: 'super reason' }) 71 await servers[0].abuses.report({ videoId: video.id, reason: 'super reason' })
68 72
69 await waitJobs(servers) 73 await waitJobs(servers)
74 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'absence' })
75 })
76
77 it('Should send a notification to moderators on local video abuse', async function () {
78 this.timeout(20000)
79
80 const name = 'video for abuse ' + buildUUID()
81 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
82
83 await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
84
85 await waitJobs(servers)
70 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) 86 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
71 }) 87 })
72 88
@@ -74,12 +90,12 @@ describe('Test moderation notifications', function () {
74 this.timeout(20000) 90 this.timeout(20000)
75 91
76 const name = 'video for abuse ' + buildUUID() 92 const name = 'video for abuse ' + buildUUID()
77 const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 93 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
78 94
79 await waitJobs(servers) 95 await waitJobs(servers)
80 96
81 const videoId = await servers[1].videos.getId({ uuid: video.uuid }) 97 const videoId = await servers[1].videos.getId({ uuid: video.uuid })
82 await servers[1].abuses.report({ videoId, reason: 'super reason' }) 98 await servers[1].abuses.report({ token: userToken2, videoId, reason: 'super reason' })
83 99
84 await waitJobs(servers) 100 await waitJobs(servers)
85 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) 101 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
@@ -89,16 +105,16 @@ describe('Test moderation notifications', function () {
89 this.timeout(20000) 105 this.timeout(20000)
90 106
91 const name = 'video for abuse ' + buildUUID() 107 const name = 'video for abuse ' + buildUUID()
92 const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 108 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
93 const comment = await servers[0].comments.createThread({ 109 const comment = await servers[0].comments.createThread({
94 token: userAccessToken, 110 token: userToken1,
95 videoId: video.id, 111 videoId: video.id,
96 text: 'comment abuse ' + buildUUID() 112 text: 'comment abuse ' + buildUUID()
97 }) 113 })
98 114
99 await waitJobs(servers) 115 await waitJobs(servers)
100 116
101 await servers[0].abuses.report({ commentId: comment.id, reason: 'super reason' }) 117 await servers[0].abuses.report({ token: userToken1, commentId: comment.id, reason: 'super reason' })
102 118
103 await waitJobs(servers) 119 await waitJobs(servers)
104 await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) 120 await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
@@ -108,10 +124,10 @@ describe('Test moderation notifications', function () {
108 this.timeout(20000) 124 this.timeout(20000)
109 125
110 const name = 'video for abuse ' + buildUUID() 126 const name = 'video for abuse ' + buildUUID()
111 const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 127 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
112 128
113 await servers[0].comments.createThread({ 129 await servers[0].comments.createThread({
114 token: userAccessToken, 130 token: userToken1,
115 videoId: video.id, 131 videoId: video.id,
116 text: 'comment abuse ' + buildUUID() 132 text: 'comment abuse ' + buildUUID()
117 }) 133 })
@@ -120,7 +136,7 @@ describe('Test moderation notifications', function () {
120 136
121 const { data } = await servers[1].comments.listThreads({ videoId: video.uuid }) 137 const { data } = await servers[1].comments.listThreads({ videoId: video.uuid })
122 const commentId = data[0].id 138 const commentId = data[0].id
123 await servers[1].abuses.report({ commentId, reason: 'super reason' }) 139 await servers[1].abuses.report({ token: userToken2, commentId, reason: 'super reason' })
124 140
125 await waitJobs(servers) 141 await waitJobs(servers)
126 await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' }) 142 await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
@@ -133,7 +149,7 @@ describe('Test moderation notifications', function () {
133 const { account } = await servers[0].users.create({ username, password: 'donald' }) 149 const { account } = await servers[0].users.create({ username, password: 'donald' })
134 const accountId = account.id 150 const accountId = account.id
135 151
136 await servers[0].abuses.report({ accountId, reason: 'super reason' }) 152 await servers[0].abuses.report({ token: userToken1, accountId, reason: 'super reason' })
137 153
138 await waitJobs(servers) 154 await waitJobs(servers)
139 await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' }) 155 await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
@@ -149,7 +165,7 @@ describe('Test moderation notifications', function () {
149 await waitJobs(servers) 165 await waitJobs(servers)
150 166
151 const account = await servers[1].accounts.get({ accountName: username + '@' + servers[0].host }) 167 const account = await servers[1].accounts.get({ accountName: username + '@' + servers[0].host })
152 await servers[1].abuses.report({ accountId: account.id, reason: 'super reason' }) 168 await servers[1].abuses.report({ token: userToken2, accountId: account.id, reason: 'super reason' })
153 169
154 await waitJobs(servers) 170 await waitJobs(servers)
155 await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' }) 171 await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
@@ -165,13 +181,13 @@ describe('Test moderation notifications', function () {
165 server: servers[0], 181 server: servers[0],
166 emails, 182 emails,
167 socketNotifications: userNotifications, 183 socketNotifications: userNotifications,
168 token: userAccessToken 184 token: userToken1
169 } 185 }
170 186
171 const name = 'abuse ' + buildUUID() 187 const name = 'abuse ' + buildUUID()
172 const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 188 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
173 189
174 const body = await servers[0].abuses.report({ token: userAccessToken, videoId: video.id, reason: 'super reason' }) 190 const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
175 abuseId = body.abuse.id 191 abuseId = body.abuse.id
176 }) 192 })
177 193
@@ -205,7 +221,7 @@ describe('Test moderation notifications', function () {
205 server: servers[0], 221 server: servers[0],
206 emails, 222 emails,
207 socketNotifications: userNotifications, 223 socketNotifications: userNotifications,
208 token: userAccessToken 224 token: userToken1
209 } 225 }
210 226
211 baseParamsAdmin = { 227 baseParamsAdmin = {
@@ -216,15 +232,15 @@ describe('Test moderation notifications', function () {
216 } 232 }
217 233
218 const name = 'abuse ' + buildUUID() 234 const name = 'abuse ' + buildUUID()
219 const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 235 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
220 236
221 { 237 {
222 const body = await servers[0].abuses.report({ token: userAccessToken, videoId: video.id, reason: 'super reason' }) 238 const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
223 abuseId = body.abuse.id 239 abuseId = body.abuse.id
224 } 240 }
225 241
226 { 242 {
227 const body = await servers[0].abuses.report({ token: userAccessToken, videoId: video.id, reason: 'super reason 2' }) 243 const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason 2' })
228 abuseId2 = body.abuse.id 244 abuseId2 = body.abuse.id
229 } 245 }
230 }) 246 })
@@ -254,7 +270,7 @@ describe('Test moderation notifications', function () {
254 this.timeout(10000) 270 this.timeout(10000)
255 271
256 const message = 'my super message to moderators' 272 const message = 'my super message to moderators'
257 await servers[0].abuses.addMessage({ token: userAccessToken, abuseId: abuseId2, message }) 273 await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message })
258 await waitJobs(servers) 274 await waitJobs(servers)
259 275
260 const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com' 276 const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com'
@@ -265,7 +281,7 @@ describe('Test moderation notifications', function () {
265 this.timeout(10000) 281 this.timeout(10000)
266 282
267 const message = 'my super message that should not be sent to reporter' 283 const message = 'my super message that should not be sent to reporter'
268 await servers[0].abuses.addMessage({ token: userAccessToken, abuseId: abuseId2, message }) 284 await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message })
269 await waitJobs(servers) 285 await waitJobs(servers)
270 286
271 const toEmail = 'user_1@example.com' 287 const toEmail = 'user_1@example.com'
@@ -281,7 +297,7 @@ describe('Test moderation notifications', function () {
281 server: servers[0], 297 server: servers[0],
282 emails, 298 emails,
283 socketNotifications: userNotifications, 299 socketNotifications: userNotifications,
284 token: userAccessToken 300 token: userToken1
285 } 301 }
286 }) 302 })
287 303
@@ -289,7 +305,7 @@ describe('Test moderation notifications', function () {
289 this.timeout(10000) 305 this.timeout(10000)
290 306
291 const name = 'video for abuse ' + buildUUID() 307 const name = 'video for abuse ' + buildUUID()
292 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 308 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
293 309
294 await servers[0].blacklist.add({ videoId: uuid }) 310 await servers[0].blacklist.add({ videoId: uuid })
295 311
@@ -301,7 +317,7 @@ describe('Test moderation notifications', function () {
301 this.timeout(10000) 317 this.timeout(10000)
302 318
303 const name = 'video for abuse ' + buildUUID() 319 const name = 'video for abuse ' + buildUUID()
304 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userAccessToken, attributes: { name } }) 320 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
305 321
306 await servers[0].blacklist.add({ videoId: uuid }) 322 await servers[0].blacklist.add({ videoId: uuid })
307 323
@@ -335,7 +351,7 @@ describe('Test moderation notifications', function () {
335 351
336 await checkUserRegistered({ ...baseParams, username: 'user_45', checkType: 'presence' }) 352 await checkUserRegistered({ ...baseParams, username: 'user_45', checkType: 'presence' })
337 353
338 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } 354 const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
339 await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_45', checkType: 'absence' }) 355 await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_45', checkType: 'absence' })
340 }) 356 })
341 }) 357 })
@@ -377,7 +393,7 @@ describe('Test moderation notifications', function () {
377 393
378 await checkNewInstanceFollower({ ...baseParams, followerHost: 'localhost:' + servers[2].port, checkType: 'presence' }) 394 await checkNewInstanceFollower({ ...baseParams, followerHost: 'localhost:' + servers[2].port, checkType: 'presence' })
379 395
380 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } 396 const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
381 await checkNewInstanceFollower({ ...baseParams, ...userOverride, followerHost: 'localhost:' + servers[2].port, checkType: 'absence' }) 397 await checkNewInstanceFollower({ ...baseParams, ...userOverride, followerHost: 'localhost:' + servers[2].port, checkType: 'absence' })
382 }) 398 })
383 399
@@ -404,7 +420,7 @@ describe('Test moderation notifications', function () {
404 const followingHost = servers[2].host 420 const followingHost = servers[2].host
405 await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' }) 421 await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' })
406 422
407 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } 423 const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
408 await checkAutoInstanceFollowing({ ...baseParams, ...userOverride, followerHost, followingHost, checkType: 'absence' }) 424 await checkAutoInstanceFollowing({ ...baseParams, ...userOverride, followerHost, followingHost, checkType: 'absence' })
409 425
410 config.followings.instance.autoFollowBack.enabled = false 426 config.followings.instance.autoFollowBack.enabled = false
@@ -461,7 +477,7 @@ describe('Test moderation notifications', function () {
461 server: servers[0], 477 server: servers[0],
462 emails, 478 emails,
463 socketNotifications: userNotifications, 479 socketNotifications: userNotifications,
464 token: userAccessToken 480 token: userToken1
465 } 481 }
466 482
467 currentCustomConfig = await servers[0].config.getCustomConfig() 483 currentCustomConfig = await servers[0].config.getCustomConfig()
@@ -490,7 +506,7 @@ describe('Test moderation notifications', function () {
490 this.timeout(120000) 506 this.timeout(120000)
491 507
492 videoName = 'video with auto-blacklist ' + buildUUID() 508 videoName = 'video with auto-blacklist ' + buildUUID()
493 const video = await servers[0].videos.upload({ token: userAccessToken, attributes: { name: videoName } }) 509 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name: videoName } })
494 shortUUID = video.shortUUID 510 shortUUID = video.shortUUID
495 uuid = video.uuid 511 uuid = video.uuid
496 512
@@ -547,7 +563,7 @@ describe('Test moderation notifications', function () {
547 } 563 }
548 } 564 }
549 565
550 const { shortUUID, uuid } = await servers[0].videos.upload({ token: userAccessToken, attributes }) 566 const { shortUUID, uuid } = await servers[0].videos.upload({ token: userToken1, attributes })
551 567
552 await servers[0].blacklist.remove({ videoId: uuid }) 568 await servers[0].blacklist.remove({ videoId: uuid })
553 569
@@ -579,7 +595,7 @@ describe('Test moderation notifications', function () {
579 } 595 }
580 } 596 }
581 597
582 const { shortUUID } = await servers[0].videos.upload({ token: userAccessToken, attributes }) 598 const { shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes })
583 599
584 await wait(6000) 600 await wait(6000)
585 await checkVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' }) 601 await checkVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' })
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index 468efdf35..9af20843e 100644
--- a/server/tests/api/notifications/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -267,7 +267,7 @@ describe('Test user notifications', function () {
267 }) 267 })
268 268
269 it('Should send a notification when an imported video is transcoded', async function () { 269 it('Should send a notification when an imported video is transcoded', async function () {
270 this.timeout(50000) 270 this.timeout(120000)
271 271
272 const name = 'video import ' + buildUUID() 272 const name = 'video import ' + buildUUID()
273 273
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 86b40cfe6..b5929129a 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -307,7 +307,7 @@ describe('Test videos redundancy', function () {
307 const strategy = 'most-views' 307 const strategy = 'most-views'
308 308
309 before(function () { 309 before(function () {
310 this.timeout(120000) 310 this.timeout(240000)
311 311
312 return createServers(strategy) 312 return createServers(strategy)
313 }) 313 })
@@ -357,7 +357,7 @@ describe('Test videos redundancy', function () {
357 const strategy = 'trending' 357 const strategy = 'trending'
358 358
359 before(function () { 359 before(function () {
360 this.timeout(120000) 360 this.timeout(240000)
361 361
362 return createServers(strategy) 362 return createServers(strategy)
363 }) 363 })
@@ -420,7 +420,7 @@ describe('Test videos redundancy', function () {
420 const strategy = 'recently-added' 420 const strategy = 'recently-added'
421 421
422 before(function () { 422 before(function () {
423 this.timeout(120000) 423 this.timeout(240000)
424 424
425 return createServers(strategy, { min_views: 3 }) 425 return createServers(strategy, { min_views: 3 })
426 }) 426 })
@@ -491,7 +491,7 @@ describe('Test videos redundancy', function () {
491 const strategy = 'recently-added' 491 const strategy = 'recently-added'
492 492
493 before(async function () { 493 before(async function () {
494 this.timeout(120000) 494 this.timeout(240000)
495 495
496 await createServers(strategy, { min_views: 3 }, false) 496 await createServers(strategy, { min_views: 3 }, false)
497 }) 497 })
@@ -553,7 +553,7 @@ describe('Test videos redundancy', function () {
553 553
554 describe('With manual strategy', function () { 554 describe('With manual strategy', function () {
555 before(function () { 555 before(function () {
556 this.timeout(120000) 556 this.timeout(240000)
557 557
558 return createServers(null) 558 return createServers(null)
559 }) 559 })
@@ -632,7 +632,7 @@ describe('Test videos redundancy', function () {
632 } 632 }
633 633
634 before(async function () { 634 before(async function () {
635 this.timeout(120000) 635 this.timeout(240000)
636 636
637 await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) 637 await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
638 638
@@ -674,7 +674,7 @@ describe('Test videos redundancy', function () {
674 const strategy = 'recently-added' 674 const strategy = 'recently-added'
675 675
676 before(async function () { 676 before(async function () {
677 this.timeout(120000) 677 this.timeout(240000)
678 678
679 await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) 679 await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
680 680
@@ -698,7 +698,7 @@ describe('Test videos redundancy', function () {
698 }) 698 })
699 699
700 it('Should cache video 2 webseeds on the first video', async function () { 700 it('Should cache video 2 webseeds on the first video', async function () {
701 this.timeout(120000) 701 this.timeout(240000)
702 702
703 await waitJobs(servers) 703 await waitJobs(servers)
704 704
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index ea524723c..96ec17b0f 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -43,6 +43,9 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
43 expect(data.services.twitter.username).to.equal('@Chocobozzz') 43 expect(data.services.twitter.username).to.equal('@Chocobozzz')
44 expect(data.services.twitter.whitelisted).to.be.false 44 expect(data.services.twitter.whitelisted).to.be.false
45 45
46 expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false
47 expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false
48
46 expect(data.cache.previews.size).to.equal(1) 49 expect(data.cache.previews.size).to.equal(1)
47 expect(data.cache.captions.size).to.equal(1) 50 expect(data.cache.captions.size).to.equal(1)
48 expect(data.cache.torrents.size).to.equal(1) 51 expect(data.cache.torrents.size).to.equal(1)
@@ -138,6 +141,9 @@ function checkUpdatedConfig (data: CustomConfig) {
138 expect(data.services.twitter.username).to.equal('@Kuja') 141 expect(data.services.twitter.username).to.equal('@Kuja')
139 expect(data.services.twitter.whitelisted).to.be.true 142 expect(data.services.twitter.whitelisted).to.be.true
140 143
144 expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.true
145 expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.true
146
141 expect(data.cache.previews.size).to.equal(2) 147 expect(data.cache.previews.size).to.equal(2)
142 expect(data.cache.captions.size).to.equal(3) 148 expect(data.cache.captions.size).to.equal(3)
143 expect(data.cache.torrents.size).to.equal(4) 149 expect(data.cache.torrents.size).to.equal(4)
@@ -246,6 +252,18 @@ const newCustomConfig: CustomConfig = {
246 whitelisted: true 252 whitelisted: true
247 } 253 }
248 }, 254 },
255 client: {
256 videos: {
257 miniature: {
258 preferAuthorDisplayName: true
259 }
260 },
261 menu: {
262 login: {
263 redirectOnSingleExternalAuth: true
264 }
265 }
266 },
249 cache: { 267 cache: {
250 previews: { 268 previews: {
251 size: 2 269 size: 2
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index 5f97edbc2..cd8d70341 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -185,7 +185,7 @@ describe('Test emails', function () {
185 this.timeout(10000) 185 this.timeout(10000)
186 186
187 const reason = 'my super bad reason' 187 const reason = 'my super bad reason'
188 await server.abuses.report({ videoId, reason }) 188 await server.abuses.report({ token: userAccessToken, videoId, reason })
189 189
190 await waitJobs(server) 190 await waitJobs(server)
191 expect(emails).to.have.lengthOf(3) 191 expect(emails).to.have.lengthOf(3)
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts
index b51b3bcdd..08b624ff3 100644
--- a/server/tests/api/videos/video-privacy.ts
+++ b/server/tests/api/videos/video-privacy.ts
@@ -2,7 +2,15 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { cleanupTests, createSingleServer, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils' 5import {
6 cleanupTests,
7 createSingleServer,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 wait,
12 waitJobs
13} from '@shared/extra-utils'
6import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models' 14import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
7 15
8const expect = chai.expect 16const expect = chai.expect
@@ -209,7 +217,7 @@ describe('Test video privacy', function () {
209 describe('Privacy update', function () { 217 describe('Privacy update', function () {
210 218
211 it('Should update the private and internal videos to public on server 1', async function () { 219 it('Should update the private and internal videos to public on server 1', async function () {
212 this.timeout(10000) 220 this.timeout(100000)
213 221
214 now = Date.now() 222 now = Date.now()
215 223
@@ -230,6 +238,7 @@ describe('Test video privacy', function () {
230 await servers[0].videos.update({ id: internalVideoId, attributes }) 238 await servers[0].videos.update({ id: internalVideoId, attributes })
231 } 239 }
232 240
241 await wait(10000)
233 await waitJobs(servers) 242 await waitJobs(servers)
234 }) 243 })
235 244
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
index f2a984962..3ac440f84 100644
--- a/server/tests/cli/peertube.ts
+++ b/server/tests/cli/peertube.ts
@@ -207,6 +207,25 @@ describe('Test CLI wrapper', function () {
207 207
208 expect(res).to.not.contain('peertube-plugin-hello-world') 208 expect(res).to.not.contain('peertube-plugin-hello-world')
209 }) 209 })
210
211 it('Should install a plugin in requested version', async function () {
212 this.timeout(60000)
213
214 await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world --plugin-version 0.0.17`)
215 })
216
217 it('Should list installed plugins, in correct version', async function () {
218 const res = await cliCommand.execWithEnv(`${cmd} plugins list`)
219
220 expect(res).to.contain('peertube-plugin-hello-world')
221 expect(res).to.contain('0.0.17')
222 })
223
224 it('Should uninstall the plugin again', async function () {
225 const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`)
226
227 expect(res).to.not.contain('peertube-plugin-hello-world')
228 })
210 }) 229 })
211 230
212 describe('Manage video redundancies', function () { 231 describe('Manage video redundancies', function () {
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts
index 2d4c02da7..1c0282da9 100644
--- a/server/tests/cli/prune-storage.ts
+++ b/server/tests/cli/prune-storage.ts
@@ -36,7 +36,7 @@ async function assertNotExists (server: PeerTubeServer, directory: string, subst
36 } 36 }
37} 37}
38 38
39async function assertCountAreOkay (servers: PeerTubeServer[], videoServer2UUID: string) { 39async function assertCountAreOkay (servers: PeerTubeServer[]) {
40 for (const server of servers) { 40 for (const server of servers) {
41 const videosCount = await countFiles(server, 'videos') 41 const videosCount = await countFiles(server, 'videos')
42 expect(videosCount).to.equal(8) 42 expect(videosCount).to.equal(8)
@@ -52,22 +52,16 @@ async function assertCountAreOkay (servers: PeerTubeServer[], videoServer2UUID:
52 52
53 const avatarsCount = await countFiles(server, 'avatars') 53 const avatarsCount = await countFiles(server, 'avatars')
54 expect(avatarsCount).to.equal(2) 54 expect(avatarsCount).to.equal(2)
55 }
56
57 // When we'll prune HLS directories too
58 // const hlsRootCount = await countFiles(servers[1], 'streaming-playlists/hls/')
59 // expect(hlsRootCount).to.equal(2)
60 55
61 // const hlsCount = await countFiles(servers[1], 'streaming-playlists/hls/' + videoServer2UUID) 56 const hlsRootCount = await countFiles(server, 'streaming-playlists/hls')
62 // expect(hlsCount).to.equal(10) 57 expect(hlsRootCount).to.equal(2)
58 }
63} 59}
64 60
65describe('Test prune storage scripts', function () { 61describe('Test prune storage scripts', function () {
66 let servers: PeerTubeServer[] 62 let servers: PeerTubeServer[]
67 const badNames: { [directory: string]: string[] } = {} 63 const badNames: { [directory: string]: string[] } = {}
68 64
69 let videoServer2UUID: string
70
71 before(async function () { 65 before(async function () {
72 this.timeout(120000) 66 this.timeout(120000)
73 67
@@ -77,9 +71,7 @@ describe('Test prune storage scripts', function () {
77 71
78 for (const server of servers) { 72 for (const server of servers) {
79 await server.videos.upload({ attributes: { name: 'video 1' } }) 73 await server.videos.upload({ attributes: { name: 'video 1' } })
80 74 await server.videos.upload({ attributes: { name: 'video 2' } })
81 const { uuid } = await server.videos.upload({ attributes: { name: 'video 2' } })
82 if (server.serverNumber === 2) videoServer2UUID = uuid
83 75
84 await server.users.updateMyAvatar({ fixture: 'avatar.png' }) 76 await server.users.updateMyAvatar({ fixture: 'avatar.png' })
85 77
@@ -123,7 +115,7 @@ describe('Test prune storage scripts', function () {
123 }) 115 })
124 116
125 it('Should have the files on the disk', async function () { 117 it('Should have the files on the disk', async function () {
126 await assertCountAreOkay(servers, videoServer2UUID) 118 await assertCountAreOkay(servers)
127 }) 119 })
128 120
129 it('Should create some dirty files', async function () { 121 it('Should create some dirty files', async function () {
@@ -188,27 +180,14 @@ describe('Test prune storage scripts', function () {
188 badNames['avatars'] = [ n1, n2 ] 180 badNames['avatars'] = [ n1, n2 ]
189 } 181 }
190 182
191 // When we'll prune HLS directories too 183 {
192 // { 184 const directory = join('streaming-playlists', 'hls')
193 // const directory = join('streaming-playlists', 'hls') 185 const base = servers[0].servers.buildDirectory(directory)
194 // const base = servers[1].servers.buildDirectory(directory)
195
196 // const n1 = buildUUID()
197 // await createFile(join(base, n1))
198 // badNames[directory] = [ n1 ]
199 // }
200
201 // {
202 // const directory = join('streaming-playlists', 'hls', videoServer2UUID)
203 // const base = servers[1].servers.buildDirectory(directory)
204 // const n1 = buildUUID() + '-240-fragmented-.mp4'
205 // const n2 = buildUUID() + '-master.m3u8'
206
207 // await createFile(join(base, n1))
208 // await createFile(join(base, n2))
209 186
210 // badNames[directory] = [ n1, n2 ] 187 const n1 = buildUUID()
211 // } 188 await createFile(join(base, n1))
189 badNames[directory] = [ n1 ]
190 }
212 } 191 }
213 }) 192 })
214 193
@@ -220,7 +199,7 @@ describe('Test prune storage scripts', function () {
220 }) 199 })
221 200
222 it('Should have removed files', async function () { 201 it('Should have removed files', async function () {
223 await assertCountAreOkay(servers, videoServer2UUID) 202 await assertCountAreOkay(servers)
224 203
225 for (const directory of Object.keys(badNames)) { 204 for (const directory of Object.keys(badNames)) {
226 for (const name of badNames[directory]) { 205 for (const name of badNames[directory]) {
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js
index db405ff31..04e059848 100644
--- a/server/tests/fixtures/peertube-plugin-test/main.js
+++ b/server/tests/fixtures/peertube-plugin-test/main.js
@@ -233,6 +233,28 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
233 } 233 }
234 }) 234 })
235 235
236 registerHook({
237 target: 'filter:api.server.stats.get.result',
238 handler: (result) => {
239 return { ...result, customStats: 14 }
240 }
241 })
242
243 // Upload/import/live attributes
244 for (const target of [
245 'filter:api.video.upload.video-attribute.result',
246 'filter:api.video.import-url.video-attribute.result',
247 'filter:api.video.import-torrent.video-attribute.result',
248 'filter:api.video.live.video-attribute.result'
249 ]) {
250 registerHook({
251 target,
252 handler: (result) => {
253 return { ...result, description: result.description + ' - ' + target }
254 }
255 })
256 }
257
236 { 258 {
237 const filterHooks = [ 259 const filterHooks = [
238 'filter:api.search.videos.local.list.params', 260 'filter:api.search.videos.local.list.params',
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
index f3e018d43..25b25bfee 100644
--- a/server/tests/plugins/external-auth.ts
+++ b/server/tests/plugins/external-auth.ts
@@ -125,7 +125,7 @@ describe('Test external auth plugins', function () {
125 expectedStatus: HttpStatusCode.BAD_REQUEST_400 125 expectedStatus: HttpStatusCode.BAD_REQUEST_400
126 }) 126 })
127 127
128 await server.servers.waitUntilLog('expired external auth token', 2) 128 await server.servers.waitUntilLog('expired external auth token', 4)
129 }) 129 })
130 130
131 it('Should auto login Cyan, create the user and use the token', async function () { 131 it('Should auto login Cyan, create the user and use the token', async function () {
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts
index 02915f08c..ff2afc56b 100644
--- a/server/tests/plugins/filter-hooks.ts
+++ b/server/tests/plugins/filter-hooks.ts
@@ -537,6 +537,75 @@ describe('Test plugin filter hooks', function () {
537 }) 537 })
538 }) 538 })
539 539
540 describe('Upload/import/live attributes filters', function () {
541
542 before(async function () {
543 await servers[0].config.enableLive({ transcoding: false, allowReplay: false })
544 await servers[0].config.enableImports()
545 await servers[0].config.disableTranscoding()
546 })
547
548 it('Should run filter:api.video.upload.video-attribute.result', async function () {
549 for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) {
550 const { id } = await servers[0].videos.upload({ attributes: { name: 'video', description: 'upload' }, mode })
551
552 const video = await servers[0].videos.get({ id })
553 expect(video.description).to.equal('upload - filter:api.video.upload.video-attribute.result')
554 }
555 })
556
557 it('Should run filter:api.video.import-url.video-attribute.result', async function () {
558 const attributes = {
559 name: 'video',
560 description: 'import url',
561 channelId: servers[0].store.channel.id,
562 targetUrl: FIXTURE_URLS.goodVideo,
563 privacy: VideoPrivacy.PUBLIC
564 }
565 const { video: { id } } = await servers[0].imports.importVideo({ attributes })
566
567 const video = await servers[0].videos.get({ id })
568 expect(video.description).to.equal('import url - filter:api.video.import-url.video-attribute.result')
569 })
570
571 it('Should run filter:api.video.import-torrent.video-attribute.result', async function () {
572 const attributes = {
573 name: 'video',
574 description: 'import torrent',
575 channelId: servers[0].store.channel.id,
576 magnetUri: FIXTURE_URLS.magnet,
577 privacy: VideoPrivacy.PUBLIC
578 }
579 const { video: { id } } = await servers[0].imports.importVideo({ attributes })
580
581 const video = await servers[0].videos.get({ id })
582 expect(video.description).to.equal('import torrent - filter:api.video.import-torrent.video-attribute.result')
583 })
584
585 it('Should run filter:api.video.live.video-attribute.result', async function () {
586 const fields = {
587 name: 'live',
588 description: 'live',
589 channelId: servers[0].store.channel.id,
590 privacy: VideoPrivacy.PUBLIC
591 }
592 const { id } = await servers[0].live.create({ fields })
593
594 const video = await servers[0].videos.get({ id })
595 expect(video.description).to.equal('live - filter:api.video.live.video-attribute.result')
596 })
597 })
598
599 describe('Stats filters', function () {
600
601 it('Should run filter:api.server.stats.get.result', async function () {
602 const data = await servers[0].stats.get()
603
604 expect((data as any).customStats).to.equal(14)
605 })
606
607 })
608
540 after(async function () { 609 after(async function () {
541 await cleanupTests(servers) 610 await cleanupTests(servers)
542 }) 611 })
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts
index ae625114d..9dd3f08c9 100644
--- a/server/tools/peertube-plugins.ts
+++ b/server/tools/peertube-plugins.ts
@@ -31,6 +31,7 @@ program
31 .option('-p, --password <token>', 'Password') 31 .option('-p, --password <token>', 'Password')
32 .option('-P --path <path>', 'Install from a path') 32 .option('-P --path <path>', 'Install from a path')
33 .option('-n, --npm-name <npmName>', 'Install from npm') 33 .option('-n, --npm-name <npmName>', 'Install from npm')
34 .option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)')
34 .action((options, command) => installPluginCLI(command, options)) 35 .action((options, command) => installPluginCLI(command, options))
35 36
36program 37program
@@ -109,7 +110,7 @@ async function installPluginCLI (command: Command, options: OptionValues) {
109 await assignToken(server, username, password) 110 await assignToken(server, username, password)
110 111
111 try { 112 try {
112 await server.plugins.install({ npmName: options.npmName, path: options.path }) 113 await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion })
113 } catch (err) { 114 } catch (err) {
114 console.error('Cannot install plugin.', err) 115 console.error('Cannot install plugin.', err)
115 process.exit(-1) 116 process.exit(-1)
diff --git a/shared/extra-utils/server/config-command.ts b/shared/extra-utils/server/config-command.ts
index 7a768b4df..a061ca89e 100644
--- a/shared/extra-utils/server/config-command.ts
+++ b/shared/extra-utils/server/config-command.ts
@@ -194,6 +194,18 @@ export class ConfigCommand extends AbstractCommand {
194 whitelisted: true 194 whitelisted: true
195 } 195 }
196 }, 196 },
197 client: {
198 videos: {
199 miniature: {
200 preferAuthorDisplayName: false
201 }
202 },
203 menu: {
204 login: {
205 redirectOnSingleExternalAuth: false
206 }
207 }
208 },
197 cache: { 209 cache: {
198 previews: { 210 previews: {
199 size: 2 211 size: 2
diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts
index b944475a2..9bf24afff 100644
--- a/shared/extra-utils/server/plugins-command.ts
+++ b/shared/extra-utils/server/plugins-command.ts
@@ -158,15 +158,16 @@ export class PluginsCommand extends AbstractCommand {
158 install (options: OverrideCommandOptions & { 158 install (options: OverrideCommandOptions & {
159 path?: string 159 path?: string
160 npmName?: string 160 npmName?: string
161 pluginVersion?: string
161 }) { 162 }) {
162 const { npmName, path } = options 163 const { npmName, path, pluginVersion } = options
163 const apiPath = '/api/v1/plugins/install' 164 const apiPath = '/api/v1/plugins/install'
164 165
165 return this.postBodyRequest({ 166 return this.postBodyRequest({
166 ...options, 167 ...options,
167 168
168 path: apiPath, 169 path: apiPath,
169 fields: { npmName, path }, 170 fields: { npmName, path, pluginVersion },
170 implicitToken: true, 171 implicitToken: true,
171 defaultExpectedStatus: HttpStatusCode.OK_200 172 defaultExpectedStatus: HttpStatusCode.OK_200
172 }) 173 })
diff --git a/shared/extra-utils/server/server.ts b/shared/extra-utils/server/server.ts
index 31224ebe9..9da293877 100644
--- a/shared/extra-utils/server/server.ts
+++ b/shared/extra-utils/server/server.ts
@@ -220,10 +220,11 @@ export class PeerTubeServer {
220 220
221 return new Promise<void>((res, rej) => { 221 return new Promise<void>((res, rej) => {
222 const self = this 222 const self = this
223 let aggregatedLogs = ''
223 224
224 this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions) 225 this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
225 226
226 const onPeerTubeExit = () => rej(new Error('Process exited')) 227 const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs))
227 const onParentExit = () => { 228 const onParentExit = () => {
228 if (!this.app || !this.app.pid) return 229 if (!this.app || !this.app.pid) return
229 230
@@ -238,10 +239,13 @@ export class PeerTubeServer {
238 this.app.stdout.on('data', function onStdout (data) { 239 this.app.stdout.on('data', function onStdout (data) {
239 let dontContinue = false 240 let dontContinue = false
240 241
242 const log: string = data.toString()
243 aggregatedLogs += log
244
241 // Capture things if we want to 245 // Capture things if we want to
242 for (const key of Object.keys(regexps)) { 246 for (const key of Object.keys(regexps)) {
243 const regexp = regexps[key] 247 const regexp = regexps[key]
244 const matches = data.toString().match(regexp) 248 const matches = log.match(regexp)
245 if (matches !== null) { 249 if (matches !== null) {
246 if (key === 'client_id') self.store.client.id = matches[1] 250 if (key === 'client_id') self.store.client.id = matches[1]
247 else if (key === 'client_secret') self.store.client.secret = matches[1] 251 else if (key === 'client_secret') self.store.client.secret = matches[1]
@@ -252,7 +256,7 @@ export class PeerTubeServer {
252 256
253 // Check if all required sentences are here 257 // Check if all required sentences are here
254 for (const key of Object.keys(serverRunString)) { 258 for (const key of Object.keys(serverRunString)) {
255 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true 259 if (log.includes(key)) serverRunString[key] = true
256 if (serverRunString[key] === false) dontContinue = true 260 if (serverRunString[key] === false) dontContinue = true
257 } 261 }
258 262
@@ -260,7 +264,7 @@ export class PeerTubeServer {
260 if (dontContinue === true) return 264 if (dontContinue === true) return
261 265
262 if (options.hideLogs === false) { 266 if (options.hideLogs === false) {
263 console.log(data.toString()) 267 console.log(log)
264 } else { 268 } else {
265 process.removeListener('exit', onParentExit) 269 process.removeListener('exit', onParentExit)
266 self.app.stdout.removeListener('data', onStdout) 270 self.app.stdout.removeListener('data', onStdout)
diff --git a/shared/extra-utils/users/blocklist-command.ts b/shared/extra-utils/users/blocklist-command.ts
index 14491a1ae..2e7ed074d 100644
--- a/shared/extra-utils/users/blocklist-command.ts
+++ b/shared/extra-utils/users/blocklist-command.ts
@@ -1,6 +1,6 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { AccountBlock, HttpStatusCode, ResultList, ServerBlock } from '@shared/models' 3import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared' 4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5 5
6type ListBlocklistOptions = OverrideCommandOptions & { 6type ListBlocklistOptions = OverrideCommandOptions & {
@@ -37,6 +37,29 @@ export class BlocklistCommand extends AbstractCommand {
37 37
38 // --------------------------------------------------------------------------- 38 // ---------------------------------------------------------------------------
39 39
40 getStatus (options: OverrideCommandOptions & {
41 accounts?: string[]
42 hosts?: string[]
43 }) {
44 const { accounts, hosts } = options
45
46 const path = '/api/v1/blocklist/status'
47
48 return this.getRequestBody<BlockStatus>({
49 ...options,
50
51 path,
52 query: {
53 accounts,
54 hosts
55 },
56 implicitToken: false,
57 defaultExpectedStatus: HttpStatusCode.OK_200
58 })
59 }
60
61 // ---------------------------------------------------------------------------
62
40 addToMyBlocklist (options: OverrideCommandOptions & { 63 addToMyBlocklist (options: OverrideCommandOptions & {
41 account?: string 64 account?: string
42 server?: string 65 server?: string
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 4d2784dde..c05c2be6c 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -217,6 +217,7 @@ async function completeVideoCheck (
217 expect(torrent.files).to.be.an('array') 217 expect(torrent.files).to.be.an('array')
218 expect(torrent.files.length).to.equal(1) 218 expect(torrent.files.length).to.equal(1)
219 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 219 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
220 expect(torrent.files[0].name).to.equal(`${videoDetails.name} ${file.resolution.id}p${extension}`)
220 } 221 }
221 222
222 expect(videoDetails.thumbnailPath).to.exist 223 expect(videoDetails.thumbnailPath).to.exist
diff --git a/shared/models/moderation/block-status.model.ts b/shared/models/moderation/block-status.model.ts
new file mode 100644
index 000000000..597312757
--- /dev/null
+++ b/shared/models/moderation/block-status.model.ts
@@ -0,0 +1,15 @@
1export interface BlockStatus {
2 accounts: {
3 [ handle: string ]: {
4 blockedByServer: boolean
5 blockedByUser?: boolean
6 }
7 }
8
9 hosts: {
10 [ host: string ]: {
11 blockedByServer: boolean
12 blockedByUser?: boolean
13 }
14 }
15}
diff --git a/shared/models/moderation/index.ts b/shared/models/moderation/index.ts
index 8b6042e97..f8e6d351c 100644
--- a/shared/models/moderation/index.ts
+++ b/shared/models/moderation/index.ts
@@ -1,3 +1,4 @@
1export * from './abuse' 1export * from './abuse'
2export * from './block-status.model'
2export * from './account-block.model' 3export * from './account-block.model'
3export * from './server-block.model' 4export * from './server-block.model'
diff --git a/shared/models/plugins/client/index.ts b/shared/models/plugins/client/index.ts
index c500185c9..f3e3fcbcf 100644
--- a/shared/models/plugins/client/index.ts
+++ b/shared/models/plugins/client/index.ts
@@ -4,4 +4,5 @@ export * from './plugin-element-placeholder.type'
4export * from './plugin-selector-id.type' 4export * from './plugin-selector-id.type'
5export * from './register-client-form-field.model' 5export * from './register-client-form-field.model'
6export * from './register-client-hook.model' 6export * from './register-client-hook.model'
7export * from './register-client-route.model'
7export * from './register-client-settings-script.model' 8export * from './register-client-settings-script.model'
diff --git a/shared/models/plugins/client/plugin-selector-id.type.ts b/shared/models/plugins/client/plugin-selector-id.type.ts
index b74dffbef..8d23314b5 100644
--- a/shared/models/plugins/client/plugin-selector-id.type.ts
+++ b/shared/models/plugins/client/plugin-selector-id.type.ts
@@ -1 +1,10 @@
1export type PluginSelectorId = 'login-form' 1export type PluginSelectorId =
2 'login-form' |
3 'menu-user-dropdown-language-item' |
4 'about-instance-features' |
5 'about-instance-statistics' |
6 'about-instance-moderation' |
7 'about-menu-instance' |
8 'about-menu-peertube' |
9 'about-menu-network' |
10 'about-instance-other-information'
diff --git a/shared/models/plugins/client/register-client-route.model.ts b/shared/models/plugins/client/register-client-route.model.ts
new file mode 100644
index 000000000..271b67834
--- /dev/null
+++ b/shared/models/plugins/client/register-client-route.model.ts
@@ -0,0 +1,7 @@
1export interface RegisterClientRouteOptions {
2 route: string
3
4 onMount (options: {
5 rootEl: HTMLElement
6 }): void
7}
diff --git a/shared/models/plugins/client/register-client-settings-script.model.ts b/shared/models/plugins/client/register-client-settings-script.model.ts
index 481ceef96..117ca4739 100644
--- a/shared/models/plugins/client/register-client-settings-script.model.ts
+++ b/shared/models/plugins/client/register-client-settings-script.model.ts
@@ -1,6 +1,6 @@
1import { RegisterServerSettingOptions } from '../server' 1import { RegisterServerSettingOptions } from '../server'
2 2
3export interface RegisterClientSettingsScript { 3export interface RegisterClientSettingsScriptOptions {
4 isSettingHidden (options: { 4 isSettingHidden (options: {
5 setting: RegisterServerSettingOptions 5 setting: RegisterServerSettingOptions
6 formValues: { [name: string]: any } 6 formValues: { [name: string]: any }
diff --git a/shared/models/plugins/server/api/install-plugin.model.ts b/shared/models/plugins/server/api/install-plugin.model.ts
index 5a268ebe1..a1d009a00 100644
--- a/shared/models/plugins/server/api/install-plugin.model.ts
+++ b/shared/models/plugins/server/api/install-plugin.model.ts
@@ -1,4 +1,5 @@
1export interface InstallOrUpdatePlugin { 1export interface InstallOrUpdatePlugin {
2 npmName?: string 2 npmName?: string
3 pluginVersion?: string
3 path?: string 4 path?: string
4} 5}
diff --git a/shared/models/plugins/server/server-hook.model.ts b/shared/models/plugins/server/server-hook.model.ts
index 3ab910197..056c41a7f 100644
--- a/shared/models/plugins/server/server-hook.model.ts
+++ b/shared/models/plugins/server/server-hook.model.ts
@@ -53,6 +53,12 @@ export const serverFilterHookObject = {
53 'filter:api.video-thread.create.accept.result': true, 53 'filter:api.video-thread.create.accept.result': true,
54 'filter:api.video-comment-reply.create.accept.result': true, 54 'filter:api.video-comment-reply.create.accept.result': true,
55 55
56 // Filter attributes when creating video object
57 'filter:api.video.upload.video-attribute.result': true,
58 'filter:api.video.import-url.video-attribute.result': true,
59 'filter:api.video.import-torrent.video-attribute.result': true,
60 'filter:api.video.live.video-attribute.result': true,
61
56 // Filter params/result used to list threads of a specific video 62 // Filter params/result used to list threads of a specific video
57 // (used by the video watch page) 63 // (used by the video watch page)
58 'filter:api.video-threads.list.params': true, 64 'filter:api.video-threads.list.params': true,
@@ -63,6 +69,9 @@ export const serverFilterHookObject = {
63 'filter:api.video-thread-comments.list.params': true, 69 'filter:api.video-thread-comments.list.params': true,
64 'filter:api.video-thread-comments.list.result': true, 70 'filter:api.video-thread-comments.list.result': true,
65 71
72 // Filter get stats result
73 'filter:api.server.stats.get.result': true,
74
66 // Filter result used to check if we need to auto blacklist a video 75 // Filter result used to check if we need to auto blacklist a video
67 // (fired when a local or remote video is created or updated) 76 // (fired when a local or remote video is created or updated)
68 'filter:video.auto-blacklist.result': true, 77 'filter:video.auto-blacklist.result': true,
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 3ed932494..52d3d9588 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -52,6 +52,20 @@ export interface CustomConfig {
52 } 52 }
53 } 53 }
54 54
55 client: {
56 videos: {
57 miniature: {
58 preferAuthorDisplayName: boolean
59 }
60 }
61
62 menu: {
63 login: {
64 redirectOnSingleExternalAuth: boolean
65 }
66 }
67 }
68
55 cache: { 69 cache: {
56 previews: { 70 previews: {
57 size: number 71 size: number
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index e75eefd47..9f17276e0 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -39,6 +39,12 @@ export interface ServerConfig {
39 preferAuthorDisplayName: boolean 39 preferAuthorDisplayName: boolean
40 } 40 }
41 } 41 }
42
43 menu: {
44 login: {
45 redirectOnSingleExternalAuth: boolean
46 }
47 }
42 } 48 }
43 49
44 webadmin: { 50 webadmin: {
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index cfba7b361..1d5581072 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -3792,6 +3792,39 @@ paths:
3792 '500': 3792 '500':
3793 description: search index unavailable 3793 description: search index unavailable
3794 3794
3795 /blocklist/status:
3796 get:
3797 tags:
3798 - Account Blocks
3799 - Server Blocks
3800 summary: Get block status of accounts/hosts
3801 parameters:
3802 -
3803 name: 'accounts'
3804 in: query
3805 description: 'Check if these accounts are blocked'
3806 example: [ 'goofy@example.com', 'donald@example.com' ]
3807 schema:
3808 type: array
3809 items:
3810 type: string
3811 -
3812 name: 'hosts'
3813 in: query
3814 description: 'Check if these hosts are blocked'
3815 example: [ 'example.com' ]
3816 schema:
3817 type: array
3818 items:
3819 type: string
3820 responses:
3821 '200':
3822 description: successful operation
3823 content:
3824 'application/json':
3825 schema:
3826 $ref: '#/components/schemas/BlockStatus'
3827
3795 /server/blocklist/accounts: 3828 /server/blocklist/accounts:
3796 get: 3829 get:
3797 tags: 3830 tags:
@@ -5134,6 +5167,29 @@ components:
5134 label: 5167 label:
5135 type: string 5168 type: string
5136 5169
5170 BlockStatus:
5171 properties:
5172 accounts:
5173 type: object
5174 additionalProperties:
5175 x-additionalPropertiesName: account
5176 type: object
5177 properties:
5178 blockedByServer:
5179 type: boolean
5180 blockedByUser:
5181 type: boolean
5182 hosts:
5183 type: object
5184 additionalProperties:
5185 x-additionalPropertiesName: host
5186 type: object
5187 properties:
5188 blockedByServer:
5189 type: boolean
5190 blockedByUser:
5191 type: boolean
5192
5137 NSFWPolicy: 5193 NSFWPolicy:
5138 type: string 5194 type: string
5139 enum: 5195 enum:
diff --git a/support/doc/development/ci.md b/support/doc/development/ci.md
new file mode 100644
index 000000000..7d6eef197
--- /dev/null
+++ b/support/doc/development/ci.md
@@ -0,0 +1,40 @@
1# Continuous integration
2
3PeerTube uses Github Actions as a CI platform.
4CI tasks are described in `.github/workflows`.
5
6## benchmark.yml
7
8*Scheduled*
9
10Run various benchmarks (build, API etc) and upload results on https://builds.joinpeertube.org/peertube-stats/ to be publicly consumed.
11
12## codeql.yml
13
14*Scheduled, on push on develop and on pull request*
15
16Run CodeQL task to throw code security issues in Github. https://lgtm.com/projects/g/Chocobozzz/PeerTube can also be used.
17
18## docker.yml
19
20*Scheduled and on push on master*
21
22Build `chocobozzz/peertube-webserver:latest`, `chocobozzz/peertube:production-...`, `chocobozzz/peertube:v-...` (only latest PeerTube tag) and `chocobozzz/peertube:develop-...` Docker images. Scheduled to automatically upgrade image software (Debian security issues etc).
23
24## nightly.yml
25
26*Scheduled*
27
28Build PeerTube nightly build (`develop` branch) and upload the release on https://builds.joinpeertube.org/nightly.
29
30## stats.yml
31
32*On push on develop*
33
34Create various PeerTube stats (line of codes, build size, lighthouse report) and upload results on https://builds.joinpeertube.org/peertube-stats/ to be publicly consumed.
35
36## test.yml
37
38*Scheduled, on push and pull request*
39
40Run PeerTube lint and tests.
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md
index 3785246a7..4a0d318a7 100644
--- a/support/doc/plugins/guide.md
+++ b/support/doc/plugins/guide.md
@@ -686,7 +686,11 @@ async function register ({ registerVideoField, peertubeHelpers }) {
686 name: 'my-field-name, 686 name: 'my-field-name,
687 label: 'My added field', 687 label: 'My added field',
688 descriptionHTML: 'Optional description', 688 descriptionHTML: 'Optional description',
689
690 // type: 'input' | 'input-checkbox' | 'input-password' | 'input-textarea' | 'markdown-text' | 'markdown-enhanced' | 'select' | 'html'
691 // /!\ 'input-checkbox' could send "false" and "true" strings instead of boolean
689 type: 'input-textarea', 692 type: 'input-textarea',
693
690 default: '', 694 default: '',
691 // Optional, to hide a field depending on the current form state 695 // Optional, to hide a field depending on the current form state
692 // liveVideo is in the options object when the user is creating/updating a live 696 // liveVideo is in the options object when the user is creating/updating a live
diff --git a/support/doc/production.md b/support/doc/production.md
index 790e43f31..e711f0997 100644
--- a/support/doc/production.md
+++ b/support/doc/production.md
@@ -8,36 +8,36 @@
8Please don't install PeerTube for production on a device behind a low bandwidth connection (example: your ADSL link). 8Please don't install PeerTube for production on a device behind a low bandwidth connection (example: your ADSL link).
9If you want information about the appropriate hardware to run PeerTube, please see the [FAQ](https://joinpeertube.org/en_US/faq#should-i-have-a-big-server-to-run-peertube). 9If you want information about the appropriate hardware to run PeerTube, please see the [FAQ](https://joinpeertube.org/en_US/faq#should-i-have-a-big-server-to-run-peertube).
10 10
11### Dependencies 11### :hammer: Dependencies
12 12
13**Follow the steps of the [dependencies guide](dependencies.md).** 13Follow the steps of the [dependencies guide](dependencies.md).
14 14
15### PeerTube user 15### :construction_worker: PeerTube user
16 16
17Create a `peertube` user with `/var/www/peertube` home: 17Create a `peertube` user with `/var/www/peertube` home:
18 18
19``` 19```bash
20$ sudo useradd -m -d /var/www/peertube -s /bin/bash -p peertube peertube 20$ sudo useradd -m -d /var/www/peertube -s /bin/bash -p peertube peertube
21``` 21```
22 22
23Set its password: 23Set its password:
24``` 24```bash
25$ sudo passwd peertube 25$ sudo passwd peertube
26``` 26```
27 27
28**On FreeBSD** 28**On FreeBSD**
29 29
30``` 30```bash
31$ sudo pw useradd -n peertube -d /var/www/peertube -s /usr/local/bin/bash -m 31$ sudo pw useradd -n peertube -d /var/www/peertube -s /usr/local/bin/bash -m
32$ sudo passwd peertube 32$ sudo passwd peertube
33``` 33```
34or use `adduser` to create it interactively. 34or use `adduser` to create it interactively.
35 35
36### Database 36### :card_file_box: Database
37 37
38Create the production database and a peertube user inside PostgreSQL: 38Create the production database and a peertube user inside PostgreSQL:
39 39
40``` 40```bash
41$ cd /var/www/peertube 41$ cd /var/www/peertube
42$ sudo -u postgres createuser -P peertube 42$ sudo -u postgres createuser -P peertube
43``` 43```
@@ -45,58 +45,58 @@ $ sudo -u postgres createuser -P peertube
45Here you should enter a password for PostgreSQL `peertube` user, that should be copied in `production.yaml` file. 45Here you should enter a password for PostgreSQL `peertube` user, that should be copied in `production.yaml` file.
46Don't just hit enter else it will be empty. 46Don't just hit enter else it will be empty.
47 47
48``` 48```bash
49$ sudo -u postgres createdb -O peertube -E UTF8 -T template0 peertube_prod 49$ sudo -u postgres createdb -O peertube -E UTF8 -T template0 peertube_prod
50``` 50```
51 51
52Then enable extensions PeerTube needs: 52Then enable extensions PeerTube needs:
53 53
54``` 54```bash
55$ sudo -u postgres psql -c "CREATE EXTENSION pg_trgm;" peertube_prod 55$ sudo -u postgres psql -c "CREATE EXTENSION pg_trgm;" peertube_prod
56$ sudo -u postgres psql -c "CREATE EXTENSION unaccent;" peertube_prod 56$ sudo -u postgres psql -c "CREATE EXTENSION unaccent;" peertube_prod
57``` 57```
58 58
59### Prepare PeerTube directory 59### :page_facing_up: Prepare PeerTube directory
60 60
61Fetch the latest tagged version of Peertube 61Fetch the latest tagged version of Peertube
62``` 62```bash
63$ VERSION=$(curl -s https://api.github.com/repos/chocobozzz/peertube/releases/latest | grep tag_name | cut -d '"' -f 4) && echo "Latest Peertube version is $VERSION" 63$ VERSION=$(curl -s https://api.github.com/repos/chocobozzz/peertube/releases/latest | grep tag_name | cut -d '"' -f 4) && echo "Latest Peertube version is $VERSION"
64``` 64```
65 65
66Open the peertube directory, create a few required directories 66Open the peertube directory, create a few required directories
67``` 67```bash
68$ cd /var/www/peertube 68$ cd /var/www/peertube
69$ sudo -u peertube mkdir config storage versions 69$ sudo -u peertube mkdir config storage versions
70$ sudo -u peertube chmod 750 config/ 70$ sudo -u peertube chmod 750 config/
71``` 71```
72 72
73Download the latest version of the Peertube client, unzip it and remove the zip 73Download the latest version of the Peertube client, unzip it and remove the zip
74``` 74```bash
75$ cd /var/www/peertube/versions 75$ cd /var/www/peertube/versions
76$ sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip" 76$ sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip"
77$ sudo -u peertube unzip -q peertube-${VERSION}.zip && sudo -u peertube rm peertube-${VERSION}.zip 77$ sudo -u peertube unzip -q peertube-${VERSION}.zip && sudo -u peertube rm peertube-${VERSION}.zip
78``` 78```
79 79
80Install Peertube: 80Install Peertube:
81``` 81```bash
82$ cd /var/www/peertube 82$ cd /var/www/peertube
83$ sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest 83$ sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest
84$ cd ./peertube-latest && sudo -H -u peertube yarn install --production --pure-lockfile 84$ cd ./peertube-latest && sudo -H -u peertube yarn install --production --pure-lockfile
85``` 85```
86 86
87### PeerTube configuration 87### :wrench: PeerTube configuration
88 88
89Copy the default configuration file that contains the default configuration provided by PeerTube. 89Copy the default configuration file that contains the default configuration provided by PeerTube.
90You **must not** update this file. 90You **must not** update this file.
91 91
92``` 92```bash
93$ cd /var/www/peertube 93$ cd /var/www/peertube
94$ sudo -u peertube cp peertube-latest/config/default.yaml config/default.yaml 94$ sudo -u peertube cp peertube-latest/config/default.yaml config/default.yaml
95``` 95```
96 96
97Now copy the production example configuration: 97Now copy the production example configuration:
98 98
99``` 99```bash
100$ cd /var/www/peertube 100$ cd /var/www/peertube
101$ sudo -u peertube cp peertube-latest/config/production.yaml.example config/production.yaml 101$ sudo -u peertube cp peertube-latest/config/production.yaml.example config/production.yaml
102``` 102```
@@ -107,20 +107,20 @@ Keys defined in `config/production.yaml` will override keys defined in `config/d
107 107
108**PeerTube does not support webserver host change**. Even though [PeerTube CLI can help you to switch hostname](https://docs.joinpeertube.org/maintain-tools?id=update-hostjs) there's no official support for that since it is a risky operation that might result in unforeseen errors. 108**PeerTube does not support webserver host change**. Even though [PeerTube CLI can help you to switch hostname](https://docs.joinpeertube.org/maintain-tools?id=update-hostjs) there's no official support for that since it is a risky operation that might result in unforeseen errors.
109 109
110### Webserver 110### :truck: Webserver
111 111
112We only provide official configuration files for Nginx. 112We only provide official configuration files for Nginx.
113 113
114Copy the nginx configuration template: 114Copy the nginx configuration template:
115 115
116``` 116```bash
117$ sudo cp /var/www/peertube/peertube-latest/support/nginx/peertube /etc/nginx/sites-available/peertube 117$ sudo cp /var/www/peertube/peertube-latest/support/nginx/peertube /etc/nginx/sites-available/peertube
118``` 118```
119 119
120Then set the domain for the webserver configuration file. 120Then set the domain for the webserver configuration file.
121Replace `[peertube-domain]` with the domain for the peertube server. 121Replace `[peertube-domain]` with the domain for the peertube server.
122 122
123``` 123```bash
124$ sudo sed -i 's/${WEBSERVER_HOST}/[peertube-domain]/g' /etc/nginx/sites-available/peertube 124$ sudo sed -i 's/${WEBSERVER_HOST}/[peertube-domain]/g' /etc/nginx/sites-available/peertube
125$ sudo sed -i 's/${PEERTUBE_HOST}/127.0.0.1:9000/g' /etc/nginx/sites-available/peertube 125$ sudo sed -i 's/${PEERTUBE_HOST}/127.0.0.1:9000/g' /etc/nginx/sites-available/peertube
126``` 126```
@@ -128,19 +128,19 @@ $ sudo sed -i 's/${PEERTUBE_HOST}/127.0.0.1:9000/g' /etc/nginx/sites-available/p
128Then modify the webserver configuration file. Please pay attention to the `alias` keys of the static locations. 128Then modify the webserver configuration file. Please pay attention to the `alias` keys of the static locations.
129It should correspond to the paths of your storage directories (set in the configuration file inside the `storage` key). 129It should correspond to the paths of your storage directories (set in the configuration file inside the `storage` key).
130 130
131``` 131```bash
132$ sudo vim /etc/nginx/sites-available/peertube 132$ sudo vim /etc/nginx/sites-available/peertube
133``` 133```
134 134
135Activate the configuration file: 135Activate the configuration file:
136 136
137``` 137```bash
138$ sudo ln -s /etc/nginx/sites-available/peertube /etc/nginx/sites-enabled/peertube 138$ sudo ln -s /etc/nginx/sites-available/peertube /etc/nginx/sites-enabled/peertube
139``` 139```
140 140
141To generate the certificate for your domain as required to make https work you can use [Let's Encrypt](https://letsencrypt.org/): 141To generate the certificate for your domain as required to make https work you can use [Let's Encrypt](https://letsencrypt.org/):
142 142
143``` 143```bash
144$ sudo systemctl stop nginx 144$ sudo systemctl stop nginx
145$ sudo certbot certonly --standalone --post-hook "systemctl restart nginx" 145$ sudo certbot certonly --standalone --post-hook "systemctl restart nginx"
146$ sudo systemctl reload nginx 146$ sudo systemctl reload nginx
@@ -148,14 +148,14 @@ $ sudo systemctl reload nginx
148 148
149Now you have the certificates you can reload nginx: 149Now you have the certificates you can reload nginx:
150 150
151``` 151```bash
152$ sudo systemctl reload nginx 152$ sudo systemctl reload nginx
153``` 153```
154 154
155Certbot should have installed a cron to automatically renew your certificate. 155Certbot should have installed a cron to automatically renew your certificate.
156Since our nginx template supports webroot renewal, we suggest you to update the renewal config file to use the `webroot` authenticator: 156Since our nginx template supports webroot renewal, we suggest you to update the renewal config file to use the `webroot` authenticator:
157 157
158``` 158```bash
159$ # Replace authenticator = standalone by authenticator = webroot 159$ # Replace authenticator = standalone by authenticator = webroot
160$ # Add webroot_path = /var/www/certbot 160$ # Add webroot_path = /var/www/certbot
161$ sudo vim /etc/letsencrypt/renewal/your-domain.com.conf 161$ sudo vim /etc/letsencrypt/renewal/your-domain.com.conf
@@ -164,15 +164,15 @@ $ sudo vim /etc/letsencrypt/renewal/your-domain.com.conf
164**FreeBSD** 164**FreeBSD**
165On FreeBSD you can use [Dehydrated](https://dehydrated.io/) `security/dehydrated` for [Let's Encrypt](https://letsencrypt.org/) 165On FreeBSD you can use [Dehydrated](https://dehydrated.io/) `security/dehydrated` for [Let's Encrypt](https://letsencrypt.org/)
166 166
167``` 167```bash
168$ sudo pkg install dehydrated 168$ sudo pkg install dehydrated
169``` 169```
170 170
171### TCP/IP Tuning 171### :alembic: TCP/IP Tuning
172 172
173**On Linux** 173**On Linux**
174 174
175``` 175```bash
176$ sudo cp /var/www/peertube/peertube-latest/support/sysctl.d/30-peertube-tcp.conf /etc/sysctl.d/ 176$ sudo cp /var/www/peertube/peertube-latest/support/sysctl.d/30-peertube-tcp.conf /etc/sysctl.d/
177$ sudo sysctl -p /etc/sysctl.d/30-peertube-tcp.conf 177$ sudo sysctl -p /etc/sysctl.d/30-peertube-tcp.conf
178``` 178```
@@ -181,36 +181,36 @@ Your distro may enable this by default, but at least Debian 9 does not, and the
181scheduler is quite prone to "Buffer Bloat" and extreme latency when dealing with slower client 181scheduler is quite prone to "Buffer Bloat" and extreme latency when dealing with slower client
182links as we often encounter in a video server. 182links as we often encounter in a video server.
183 183
184### systemd 184### :bricks: systemd
185 185
186If your OS uses systemd, copy the configuration template: 186If your OS uses systemd, copy the configuration template:
187 187
188``` 188```bash
189$ sudo cp /var/www/peertube/peertube-latest/support/systemd/peertube.service /etc/systemd/system/ 189$ sudo cp /var/www/peertube/peertube-latest/support/systemd/peertube.service /etc/systemd/system/
190``` 190```
191 191
192Check the service file (PeerTube paths and security directives): 192Check the service file (PeerTube paths and security directives):
193 193
194``` 194```bash
195$ sudo vim /etc/systemd/system/peertube.service 195$ sudo vim /etc/systemd/system/peertube.service
196``` 196```
197 197
198 198
199Tell systemd to reload its config: 199Tell systemd to reload its config:
200 200
201``` 201```bash
202$ sudo systemctl daemon-reload 202$ sudo systemctl daemon-reload
203``` 203```
204 204
205If you want to start PeerTube on boot: 205If you want to start PeerTube on boot:
206 206
207``` 207```bash
208$ sudo systemctl enable peertube 208$ sudo systemctl enable peertube
209``` 209```
210 210
211Run: 211Run:
212 212
213``` 213```bash
214$ sudo systemctl start peertube 214$ sudo systemctl start peertube
215$ sudo journalctl -feu peertube 215$ sudo journalctl -feu peertube
216``` 216```
@@ -218,51 +218,51 @@ $ sudo journalctl -feu peertube
218**FreeBSD** 218**FreeBSD**
219On FreeBSD, copy the startup script and update rc.conf: 219On FreeBSD, copy the startup script and update rc.conf:
220 220
221``` 221```bash
222$ sudo install -m 0555 /var/www/peertube/peertube-latest/support/freebsd/peertube /usr/local/etc/rc.d/ 222$ sudo install -m 0555 /var/www/peertube/peertube-latest/support/freebsd/peertube /usr/local/etc/rc.d/
223$ sudo sysrc peertube_enable="YES" 223$ sudo sysrc peertube_enable="YES"
224``` 224```
225 225
226Run: 226Run:
227 227
228``` 228```bash
229$ sudo service peertube start 229$ sudo service peertube start
230``` 230```
231 231
232### OpenRC 232### :bricks: OpenRC
233 233
234If your OS uses OpenRC, copy the service script: 234If your OS uses OpenRC, copy the service script:
235 235
236``` 236```bash
237$ sudo cp /var/www/peertube/peertube-latest/support/init.d/peertube /etc/init.d/ 237$ sudo cp /var/www/peertube/peertube-latest/support/init.d/peertube /etc/init.d/
238``` 238```
239 239
240If you want to start PeerTube on boot: 240If you want to start PeerTube on boot:
241 241
242``` 242```bash
243$ sudo rc-update add peertube default 243$ sudo rc-update add peertube default
244``` 244```
245 245
246Run and print last logs: 246Run and print last logs:
247 247
248``` 248```bash
249$ sudo /etc/init.d/peertube start 249$ sudo /etc/init.d/peertube start
250$ tail -f /var/log/peertube/peertube.log 250$ tail -f /var/log/peertube/peertube.log
251``` 251```
252 252
253### Administrator 253### :technologist: Administrator
254 254
255The administrator password is automatically generated and can be found in the PeerTube 255The administrator password is automatically generated and can be found in the PeerTube
256logs (path defined in `production.yaml`). You can also set another password with: 256logs (path defined in `production.yaml`). You can also set another password with:
257 257
258``` 258```bash
259$ cd /var/www/peertube/peertube-latest && NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run reset-password -- -u root 259$ cd /var/www/peertube/peertube-latest && NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run reset-password -- -u root
260``` 260```
261 261
262Alternatively you can set the environment variable `PT_INITIAL_ROOT_PASSWORD`, 262Alternatively you can set the environment variable `PT_INITIAL_ROOT_PASSWORD`,
263to your own administrator password, although it must be 6 characters or more. 263to your own administrator password, although it must be 6 characters or more.
264 264
265### What now? 265### :tada: What now?
266 266
267Now your instance is up you can: 267Now your instance is up you can:
268 268
@@ -279,7 +279,7 @@ Now your instance is up you can:
279 279
280The password it asks is PeerTube's database user password. 280The password it asks is PeerTube's database user password.
281 281
282``` 282```bash
283$ cd /var/www/peertube/peertube-latest/scripts && sudo -H -u peertube ./upgrade.sh 283$ cd /var/www/peertube/peertube-latest/scripts && sudo -H -u peertube ./upgrade.sh
284$ sudo systemctl restart peertube # Or use your OS command to restart PeerTube if you don't use systemd 284$ sudo systemctl restart peertube # Or use your OS command to restart PeerTube if you don't use systemd
285``` 285```
@@ -288,7 +288,7 @@ $ sudo systemctl restart peertube # Or use your OS command to restart PeerTube i
288 288
289Make a SQL backup 289Make a SQL backup
290 290
291``` 291```bash
292$ SQL_BACKUP_PATH="backup/sql-peertube_prod-$(date -Im).bak" && \ 292$ SQL_BACKUP_PATH="backup/sql-peertube_prod-$(date -Im).bak" && \
293 cd /var/www/peertube && sudo -u peertube mkdir -p backup && \ 293 cd /var/www/peertube && sudo -u peertube mkdir -p backup && \
294 sudo -u postgres pg_dump -F c peertube_prod | sudo -u peertube tee "$SQL_BACKUP_PATH" >/dev/null 294 sudo -u postgres pg_dump -F c peertube_prod | sudo -u peertube tee "$SQL_BACKUP_PATH" >/dev/null
@@ -296,13 +296,13 @@ $ SQL_BACKUP_PATH="backup/sql-peertube_prod-$(date -Im).bak" && \
296 296
297Fetch the latest tagged version of Peertube: 297Fetch the latest tagged version of Peertube:
298 298
299``` 299```bash
300$ VERSION=$(curl -s https://api.github.com/repos/chocobozzz/peertube/releases/latest | grep tag_name | cut -d '"' -f 4) && echo "Latest Peertube version is $VERSION" 300$ VERSION=$(curl -s https://api.github.com/repos/chocobozzz/peertube/releases/latest | grep tag_name | cut -d '"' -f 4) && echo "Latest Peertube version is $VERSION"
301``` 301```
302 302
303Download the new version and unzip it: 303Download the new version and unzip it:
304 304
305``` 305```bash
306$ cd /var/www/peertube/versions && \ 306$ cd /var/www/peertube/versions && \
307 sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip" && \ 307 sudo -u peertube wget -q "https://github.com/Chocobozzz/PeerTube/releases/download/${VERSION}/peertube-${VERSION}.zip" && \
308 sudo -u peertube unzip -o peertube-${VERSION}.zip && \ 308 sudo -u peertube unzip -o peertube-${VERSION}.zip && \
@@ -311,21 +311,21 @@ $ cd /var/www/peertube/versions && \
311 311
312Install node dependencies: 312Install node dependencies:
313 313
314``` 314```bash
315$ cd /var/www/peertube/versions/peertube-${VERSION} && \ 315$ cd /var/www/peertube/versions/peertube-${VERSION} && \
316 sudo -H -u peertube yarn install --production --pure-lockfile 316 sudo -H -u peertube yarn install --production --pure-lockfile
317``` 317```
318 318
319Copy new configuration defaults values and update your configuration file: 319Copy new configuration defaults values and update your configuration file:
320 320
321``` 321```bash
322$ sudo -u peertube cp /var/www/peertube/versions/peertube-${VERSION}/config/default.yaml /var/www/peertube/config/default.yaml 322$ sudo -u peertube cp /var/www/peertube/versions/peertube-${VERSION}/config/default.yaml /var/www/peertube/config/default.yaml
323$ diff /var/www/peertube/versions/peertube-${VERSION}/config/production.yaml.example /var/www/peertube/config/production.yaml 323$ diff /var/www/peertube/versions/peertube-${VERSION}/config/production.yaml.example /var/www/peertube/config/production.yaml
324``` 324```
325 325
326Change the link to point to the latest version: 326Change the link to point to the latest version:
327 327
328``` 328```bash
329$ cd /var/www/peertube && \ 329$ cd /var/www/peertube && \
330 sudo unlink ./peertube-latest && \ 330 sudo unlink ./peertube-latest && \
331 sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest 331 sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest
@@ -335,7 +335,7 @@ $ cd /var/www/peertube && \
335 335
336Check changes in nginx configuration: 336Check changes in nginx configuration:
337 337
338``` 338```bash
339$ cd /var/www/peertube/versions 339$ cd /var/www/peertube/versions
340$ diff "$(ls --sort=t | head -2 | tail -1)/support/nginx/peertube" "$(ls --sort=t | head -1)/support/nginx/peertube" 340$ diff "$(ls --sort=t | head -2 | tail -1)/support/nginx/peertube" "$(ls --sort=t | head -1)/support/nginx/peertube"
341``` 341```
@@ -344,7 +344,7 @@ $ diff "$(ls --sort=t | head -2 | tail -1)/support/nginx/peertube" "$(ls --sort=
344 344
345Check changes in systemd configuration: 345Check changes in systemd configuration:
346 346
347``` 347```bash
348$ cd /var/www/peertube/versions 348$ cd /var/www/peertube/versions
349$ diff "$(ls --sort=t | head -2 | tail -1)/support/systemd/peertube.service" "$(ls --sort=t | head -1)/support/systemd/peertube.service" 349$ diff "$(ls --sort=t | head -2 | tail -1)/support/systemd/peertube.service" "$(ls --sort=t | head -1)/support/systemd/peertube.service"
350``` 350```
@@ -353,19 +353,19 @@ $ diff "$(ls --sort=t | head -2 | tail -1)/support/systemd/peertube.service" "$(
353 353
354If you changed your nginx configuration: 354If you changed your nginx configuration:
355 355
356``` 356```bash
357$ sudo systemctl reload nginx 357$ sudo systemctl reload nginx
358``` 358```
359 359
360If you changed your systemd configuration: 360If you changed your systemd configuration:
361 361
362``` 362```bash
363$ sudo systemctl daemon-reload 363$ sudo systemctl daemon-reload
364``` 364```
365 365
366Restart PeerTube and check the logs: 366Restart PeerTube and check the logs:
367 367
368``` 368```bash
369$ sudo systemctl restart peertube && sudo journalctl -fu peertube 369$ sudo systemctl restart peertube && sudo journalctl -fu peertube
370``` 370```
371 371
@@ -373,7 +373,7 @@ $ sudo systemctl restart peertube && sudo journalctl -fu peertube
373 373
374Change `peertube-latest` destination to the previous version and restore your SQL backup: 374Change `peertube-latest` destination to the previous version and restore your SQL backup:
375 375
376``` 376```bash
377$ OLD_VERSION="v0.42.42" && SQL_BACKUP_PATH="backup/sql-peertube_prod-2018-01-19T10:18+01:00.bak" && \ 377$ OLD_VERSION="v0.42.42" && SQL_BACKUP_PATH="backup/sql-peertube_prod-2018-01-19T10:18+01:00.bak" && \
378 cd /var/www/peertube && sudo -u peertube unlink ./peertube-latest && \ 378 cd /var/www/peertube && sudo -u peertube unlink ./peertube-latest && \
379 sudo -u peertube ln -s "versions/peertube-$OLD_VERSION" peertube-latest && \ 379 sudo -u peertube ln -s "versions/peertube-$OLD_VERSION" peertube-latest && \
diff --git a/support/docker/production/Dockerfile.bullseye b/support/docker/production/Dockerfile.bullseye
index 7b2650538..ec06d6b1d 100644
--- a/support/docker/production/Dockerfile.bullseye
+++ b/support/docker/production/Dockerfile.bullseye
@@ -21,10 +21,10 @@ WORKDIR /app
21 21
22USER peertube 22USER peertube
23 23
24RUN yarn install --pure-lockfile \ 24RUN yarn install --pure-lockfile --network-timeout 600000 \
25 && npm run build -- $NPM_RUN_BUILD_OPTS \ 25 && npm run build -- $NPM_RUN_BUILD_OPTS \
26 && rm -r ./node_modules ./client/node_modules \ 26 && rm -r ./node_modules ./client/node_modules \
27 && yarn install --pure-lockfile --production \ 27 && yarn install --pure-lockfile --production --network-timeout 600000 \
28 && yarn cache clean 28 && yarn cache clean
29 29
30USER root 30USER root
diff --git a/yarn.lock b/yarn.lock
index ee35c5c7e..3612892c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2613,11 +2613,6 @@ call-me-maybe@^1.0.1:
2613 resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" 2613 resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
2614 integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= 2614 integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
2615 2615
2616callsite@^1.0.0:
2617 version "1.0.0"
2618 resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
2619 integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
2620
2621callsites@^3.0.0: 2616callsites@^3.0.0:
2622 version "3.1.0" 2617 version "3.1.0"
2623 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 2618 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -3229,13 +3224,6 @@ debuglog@^1.0.0:
3229 resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" 3224 resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
3230 integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= 3225 integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
3231 3226
3232decache@^4.6.0:
3233 version "4.6.0"
3234 resolved "https://registry.yarnpkg.com/decache/-/decache-4.6.0.tgz#87026bc6e696759e82d57a3841c4e251a30356e8"
3235 integrity sha512-PppOuLiz+DFeaUvFXEYZjLxAkKiMYH/do/b/MxpDe/8AgKBi5GhZxridoVIbBq72GDbL36e4p0Ce2jTGUwwU+w==
3236 dependencies:
3237 callsite "^1.0.0"
3238
3239decamelize@^1.2.0: 3227decamelize@^1.2.0:
3240 version "1.2.0" 3228 version "1.2.0"
3241 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 3229 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -5939,10 +5927,10 @@ ms@2.1.3, ms@^2.1.1:
5939 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 5927 resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
5940 integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 5928 integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
5941 5929
5942multer@^1.1.0: 5930multer@^1.4.4:
5943 version "1.4.3" 5931 version "1.4.4"
5944 resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.3.tgz#4db352d6992e028ac0eacf7be45c6efd0264297b" 5932 resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4.tgz#e2bc6cac0df57a8832b858d7418ccaa8ebaf7d8c"
5945 integrity sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg== 5933 integrity sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==
5946 dependencies: 5934 dependencies:
5947 append-field "^1.0.0" 5935 append-field "^1.0.0"
5948 busboy "^0.2.11" 5936 busboy "^0.2.11"