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