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/+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/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/assets/player/p2p-media-loader/hls-plugin.ts6
-rw-r--r--client/src/root-helpers/plugins-manager.ts6
-rw-r--r--config/default.yaml6
-rw-r--r--config/production.yaml.example6
-rw-r--r--config/test.yaml1
-rw-r--r--package.json1
-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/users/my-subscriptions.ts21
-rw-r--r--server/controllers/api/videos/upload.ts9
-rw-r--r--server/helpers/actors.ts17
-rw-r--r--server/helpers/decache.ts78
-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/blocklist.ts4
-rw-r--r--server/lib/client-html.ts4
-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/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/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/user-notifications.ts2
-rw-r--r--server/tests/api/server/config.ts18
-rw-r--r--server/tests/cli/peertube.ts19
-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/users/blocklist-command.ts25
-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/plugin-selector-id.type.ts11
-rw-r--r--shared/models/plugins/server/api/install-plugin.model.ts1
-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/docker/production/Dockerfile.bullseye4
-rw-r--r--yarn.lock12
83 files changed, 1321 insertions, 397 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/+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/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..bcc884878 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.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/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/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts
index a1b763ff2..9cba63373 100644
--- a/client/src/root-helpers/plugins-manager.ts
+++ b/client/src/root-helpers/plugins-manager.ts
@@ -15,6 +15,7 @@ import {
15 RegisterClientHookOptions, 15 RegisterClientHookOptions,
16 RegisterClientSettingsScript, 16 RegisterClientSettingsScript,
17 RegisterClientVideoFieldOptions, 17 RegisterClientVideoFieldOptions,
18 RegisteredExternalAuthConfig,
18 ServerConfigPlugin 19 ServerConfigPlugin
19} from '../../../shared/models' 20} from '../../../shared/models'
20import { environment } from '../environments/environment' 21import { environment } from '../environments/environment'
@@ -78,6 +79,11 @@ class PluginsManager {
78 return isTheme ? '/themes' : '/plugins' 79 return isTheme ? '/themes' : '/plugins'
79 } 80 }
80 81
82 static getExternalAuthHref (auth: RegisteredExternalAuthConfig) {
83 return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
84
85 }
86
81 loadPluginsList (config: HTMLServerConfig) { 87 loadPluginsList (config: HTMLServerConfig) {
82 for (const plugin of config.plugin.registered) { 88 for (const plugin of config.plugin.registered) {
83 this.addPlugin(plugin) 89 this.addPlugin(plugin)
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 6cc00d1ae..b03f5dcdb 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",
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/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/upload.ts b/server/controllers/api/videos/upload.ts
index 6773b500f..c827f6bf0 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),
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..62c95cfff 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"