diff options
82 files changed, 1264 insertions, 397 deletions
diff --git a/.github/actions/reusable-deploy/action.yml b/.github/actions/reusable-deploy/action.yml new file mode 100644 index 000000000..bc69a2e43 --- /dev/null +++ b/.github/actions/reusable-deploy/action.yml | |||
@@ -0,0 +1,46 @@ | |||
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/+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/menu/menu.component.html b/client/src/app/menu/menu.component.html index 46dd807ec..9ea991042 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -30,7 +30,10 @@ | |||
30 | 30 | ||
31 | <div class="dropdown-divider"></div> | 31 | <div class="dropdown-divider"></div> |
32 | 32 | ||
33 | <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"> | 33 | <a |
34 | myPluginSelector pluginSelectorId="menu-user-dropdown-language-item" | ||
35 | ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()" | ||
36 | > | ||
34 | <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> | 37 | <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> |
35 | <span i18n>Interface:</span> | 38 | <span i18n>Interface:</span> |
36 | <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span> | 39 | <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span> |
@@ -96,7 +99,9 @@ | |||
96 | </div> | 99 | </div> |
97 | 100 | ||
98 | <div *ngIf="!isLoggedIn" class="login-buttons-block"> | 101 | <div *ngIf="!isLoggedIn" class="login-buttons-block"> |
99 | <a i18n routerLink="/login" class="peertube-button-link orange-button">Login</a> | 102 | <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> |
103 | <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> | ||
104 | |||
100 | <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> | 105 | <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> |
101 | </div> | 106 | </div> |
102 | 107 | ||
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 97f07c956..bcc884878 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -21,6 +21,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component' | |||
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.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/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts index 71c31696a..2aa691f05 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,9 @@ class Html5Hlsjs { | |||
146 | } | 146 | } |
147 | 147 | ||
148 | duration () { | 148 | duration () { |
149 | return this._duration || this.videoElement.duration || 0 | 149 | if (!isNaN(this.videoElement.duration)) return this.videoElement.duration |
150 | |||
151 | return this._duration || 0 | ||
150 | } | 152 | } |
151 | 153 | ||
152 | seekable () { | 154 | seekable () { |
@@ -366,6 +368,7 @@ class Html5Hlsjs { | |||
366 | 368 | ||
367 | this.isLive = data.details.live | 369 | this.isLive = data.details.live |
368 | this.dvrDuration = data.details.totalduration | 370 | this.dvrDuration = data.details.totalduration |
371 | |||
369 | this._duration = this.isLive ? Infinity : data.details.totalduration | 372 | this._duration = this.isLive ? Infinity : data.details.totalduration |
370 | }) | 373 | }) |
371 | 374 | ||
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts index a1b763ff2..9cba63373 100644 --- a/client/src/root-helpers/plugins-manager.ts +++ b/client/src/root-helpers/plugins-manager.ts | |||
@@ -15,6 +15,7 @@ import { | |||
15 | RegisterClientHookOptions, | 15 | RegisterClientHookOptions, |
16 | RegisterClientSettingsScript, | 16 | RegisterClientSettingsScript, |
17 | RegisterClientVideoFieldOptions, | 17 | RegisterClientVideoFieldOptions, |
18 | RegisteredExternalAuthConfig, | ||
18 | ServerConfigPlugin | 19 | ServerConfigPlugin |
19 | } from '../../../shared/models' | 20 | } from '../../../shared/models' |
20 | import { environment } from '../environments/environment' | 21 | import { environment } from '../environments/environment' |
@@ -78,6 +79,11 @@ class PluginsManager { | |||
78 | return isTheme ? '/themes' : '/plugins' | 79 | return isTheme ? '/themes' : '/plugins' |
79 | } | 80 | } |
80 | 81 | ||
82 | static getExternalAuthHref (auth: RegisteredExternalAuthConfig) { | ||
83 | return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | ||
84 | |||
85 | } | ||
86 | |||
81 | loadPluginsList (config: HTMLServerConfig) { | 87 | loadPluginsList (config: HTMLServerConfig) { |
82 | for (const plugin of config.plugin.registered) { | 88 | for (const plugin of config.plugin.registered) { |
83 | this.addPlugin(plugin) | 89 | this.addPlugin(plugin) |
diff --git a/config/default.yaml b/config/default.yaml index b9c725cea..074951117 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -82,6 +82,12 @@ client: | |||
82 | # By default PeerTube client displays author username | 82 | # By default PeerTube client displays author username |
83 | prefer_author_display_name: false | 83 | prefer_author_display_name: false |
84 | 84 | ||
85 | menu: | ||
86 | login: | ||
87 | # If you enable only one external auth plugin | ||
88 | # You can automatically redirect your users on this external platform when they click on the login button | ||
89 | redirect_on_single_external_auth: false | ||
90 | |||
85 | # From the project root directory | 91 | # From the project root directory |
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 6cc00d1ae..b03f5dcdb 100644 --- a/package.json +++ b/package.json | |||
@@ -90,7 +90,6 @@ | |||
90 | "cookie-parser": "^1.4.3", | 90 | "cookie-parser": "^1.4.3", |
91 | "cors": "^2.8.1", | 91 | "cors": "^2.8.1", |
92 | "create-torrent": "^5.0.0", | 92 | "create-torrent": "^5.0.0", |
93 | "decache": "^4.6.0", | ||
94 | "deep-object-diff": "^1.1.0", | 93 | "deep-object-diff": "^1.1.0", |
95 | "email-templates": "^8.0.3", | 94 | "email-templates": "^8.0.3", |
96 | "execa": "^5.1.1", | 95 | "execa": "^5.1.1", |
diff --git a/server/controllers/api/blocklist.ts b/server/controllers/api/blocklist.ts new file mode 100644 index 000000000..1e936ad10 --- /dev/null +++ b/server/controllers/api/blocklist.ts | |||
@@ -0,0 +1,108 @@ | |||
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/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/upload.ts b/server/controllers/api/videos/upload.ts index 6773b500f..c827f6bf0 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts | |||
@@ -8,6 +8,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | |||
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), |
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/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/blocklist.ts b/server/lib/blocklist.ts index d6b684015..98273a6ea 100644 --- a/server/lib/blocklist.ts +++ b/server/lib/blocklist.ts | |||
@@ -40,12 +40,12 @@ async function isBlockedByServerOrAccount (targetAccount: MAccountServer, userAc | |||
40 | 40 | ||
41 | if (userAccount) sourceAccounts.push(userAccount.id) | 41 | if (userAccount) sourceAccounts.push(userAccount.id) |
42 | 42 | ||
43 | const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id) | 43 | const accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, targetAccount.id) |
44 | if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) { | 44 | if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) { |
45 | return true | 45 | return true |
46 | } | 46 | } |
47 | 47 | ||
48 | const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId) | 48 | const instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, targetAccount.Actor.serverId) |
49 | if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) { | 49 | if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) { |
50 | return true | 50 | return true |
51 | } | 51 | } |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 360b4667f..adc3d712e 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -350,10 +350,6 @@ class ClientHtml { | |||
350 | return join(__dirname, '../../../client/dist/standalone/videos/embed.html') | 350 | return join(__dirname, '../../../client/dist/standalone/videos/embed.html') |
351 | } | 351 | } |
352 | 352 | ||
353 | private static addHtmlLang (htmlStringPage: string, paramLang: string) { | ||
354 | return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`) | ||
355 | } | ||
356 | |||
357 | private static addManifestContentHash (htmlStringPage: string) { | 353 | private static addManifestContentHash (htmlStringPage: string) { |
358 | return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) | 354 | return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) |
359 | } | 355 | } |
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts index 4f84d8dea..765cbaad9 100644 --- a/server/lib/notifier/shared/comment/comment-mention.ts +++ b/server/lib/notifier/shared/comment/comment-mention.ts | |||
@@ -47,8 +47,8 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU | |||
47 | 47 | ||
48 | const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ]) | 48 | const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ]) |
49 | 49 | ||
50 | this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, this.payload.accountId) | 50 | this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, this.payload.accountId) |
51 | this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, this.payload.Account.Actor.serverId) | 51 | this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, this.payload.Account.Actor.serverId) |
52 | } | 52 | } |
53 | 53 | ||
54 | log () { | 54 | log () { |
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index d4d2a7edc..6c2f4764e 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -1,8 +1,8 @@ | |||
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/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/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts index 7d5fae5cf..f72a892e2 100644 --- a/server/tests/api/check-params/blocklist.ts +++ b/server/tests/api/check-params/blocklist.ts | |||
@@ -481,6 +481,78 @@ describe('Test blocklist API validators', function () { | |||
481 | }) | 481 | }) |
482 | }) | 482 | }) |
483 | 483 | ||
484 | describe('When getting blocklist status', function () { | ||
485 | const path = '/api/v1/blocklist/status' | ||
486 | |||
487 | it('Should fail with a bad token', async function () { | ||
488 | await makeGetRequest({ | ||
489 | url: server.url, | ||
490 | path, | ||
491 | token: 'false', | ||
492 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
493 | }) | ||
494 | }) | ||
495 | |||
496 | it('Should fail with a bad accounts field', async function () { | ||
497 | await makeGetRequest({ | ||
498 | url: server.url, | ||
499 | path, | ||
500 | query: { | ||
501 | accounts: 1 | ||
502 | }, | ||
503 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
504 | }) | ||
505 | |||
506 | await makeGetRequest({ | ||
507 | url: server.url, | ||
508 | path, | ||
509 | query: { | ||
510 | accounts: [ 1 ] | ||
511 | }, | ||
512 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
513 | }) | ||
514 | }) | ||
515 | |||
516 | it('Should fail with a bad hosts field', async function () { | ||
517 | await makeGetRequest({ | ||
518 | url: server.url, | ||
519 | path, | ||
520 | query: { | ||
521 | hosts: 1 | ||
522 | }, | ||
523 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
524 | }) | ||
525 | |||
526 | await makeGetRequest({ | ||
527 | url: server.url, | ||
528 | path, | ||
529 | query: { | ||
530 | hosts: [ 1 ] | ||
531 | }, | ||
532 | expectedStatus: HttpStatusCode.BAD_REQUEST_400 | ||
533 | }) | ||
534 | }) | ||
535 | |||
536 | it('Should succeed with the correct parameters', async function () { | ||
537 | await makeGetRequest({ | ||
538 | url: server.url, | ||
539 | path, | ||
540 | query: {}, | ||
541 | expectedStatus: HttpStatusCode.OK_200 | ||
542 | }) | ||
543 | |||
544 | await makeGetRequest({ | ||
545 | url: server.url, | ||
546 | path, | ||
547 | query: { | ||
548 | hosts: [ 'example.com' ], | ||
549 | accounts: [ 'john@example.com' ] | ||
550 | }, | ||
551 | expectedStatus: HttpStatusCode.OK_200 | ||
552 | }) | ||
553 | }) | ||
554 | }) | ||
555 | |||
484 | after(async function () { | 556 | after(async function () { |
485 | await cleanupTests(servers) | 557 | await cleanupTests(servers) |
486 | }) | 558 | }) |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index d0cd7722b..a6e87730a 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -54,6 +54,18 @@ describe('Test config API validators', function () { | |||
54 | whitelisted: true | 54 | whitelisted: true |
55 | } | 55 | } |
56 | }, | 56 | }, |
57 | client: { | ||
58 | videos: { | ||
59 | miniature: { | ||
60 | preferAuthorDisplayName: false | ||
61 | } | ||
62 | }, | ||
63 | menu: { | ||
64 | login: { | ||
65 | redirectOnSingleExternalAuth: false | ||
66 | } | ||
67 | } | ||
68 | }, | ||
57 | cache: { | 69 | cache: { |
58 | previews: { | 70 | previews: { |
59 | size: 2 | 71 | size: 2 |
diff --git a/server/tests/api/check-params/plugins.ts b/server/tests/api/check-params/plugins.ts index 33f84ecbc..2c436376c 100644 --- a/server/tests/api/check-params/plugins.ts +++ b/server/tests/api/check-params/plugins.ts | |||
@@ -30,7 +30,7 @@ describe('Test server plugins API validators', function () { | |||
30 | // --------------------------------------------------------------- | 30 | // --------------------------------------------------------------- |
31 | 31 | ||
32 | before(async function () { | 32 | before(async function () { |
33 | this.timeout(30000) | 33 | this.timeout(60000) |
34 | 34 | ||
35 | server = await createSingleServer(1) | 35 | server = await createSingleServer(1) |
36 | 36 | ||
diff --git a/server/tests/api/moderation/blocklist.ts b/server/tests/api/moderation/blocklist.ts index 089af8b15..b3fd8ecac 100644 --- a/server/tests/api/moderation/blocklist.ts +++ b/server/tests/api/moderation/blocklist.ts | |||
@@ -254,6 +254,45 @@ describe('Test blocklist', function () { | |||
254 | } | 254 | } |
255 | }) | 255 | }) |
256 | 256 | ||
257 | it('Should get blocked status', async function () { | ||
258 | const remoteHandle = 'user2@' + servers[1].host | ||
259 | const localHandle = 'user1@' + servers[0].host | ||
260 | const unknownHandle = 'user5@' + servers[0].host | ||
261 | |||
262 | { | ||
263 | const status = await command.getStatus({ accounts: [ remoteHandle ] }) | ||
264 | expect(Object.keys(status.accounts)).to.have.lengthOf(1) | ||
265 | expect(status.accounts[remoteHandle].blockedByUser).to.be.false | ||
266 | expect(status.accounts[remoteHandle].blockedByServer).to.be.false | ||
267 | |||
268 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
269 | } | ||
270 | |||
271 | { | ||
272 | const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] }) | ||
273 | expect(Object.keys(status.accounts)).to.have.lengthOf(1) | ||
274 | expect(status.accounts[remoteHandle].blockedByUser).to.be.true | ||
275 | expect(status.accounts[remoteHandle].blockedByServer).to.be.false | ||
276 | |||
277 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
278 | } | ||
279 | |||
280 | { | ||
281 | const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] }) | ||
282 | expect(Object.keys(status.accounts)).to.have.lengthOf(3) | ||
283 | |||
284 | for (const handle of [ localHandle, remoteHandle ]) { | ||
285 | expect(status.accounts[handle].blockedByUser).to.be.true | ||
286 | expect(status.accounts[handle].blockedByServer).to.be.false | ||
287 | } | ||
288 | |||
289 | expect(status.accounts[unknownHandle].blockedByUser).to.be.false | ||
290 | expect(status.accounts[unknownHandle].blockedByServer).to.be.false | ||
291 | |||
292 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
293 | } | ||
294 | }) | ||
295 | |||
257 | it('Should not allow a remote blocked user to comment my videos', async function () { | 296 | it('Should not allow a remote blocked user to comment my videos', async function () { |
258 | this.timeout(60000) | 297 | this.timeout(60000) |
259 | 298 | ||
@@ -434,6 +473,35 @@ describe('Test blocklist', function () { | |||
434 | expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) | 473 | expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) |
435 | }) | 474 | }) |
436 | 475 | ||
476 | it('Should get blocklist status', async function () { | ||
477 | const blockedServer = servers[1].host | ||
478 | const notBlockedServer = 'example.com' | ||
479 | |||
480 | { | ||
481 | const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] }) | ||
482 | expect(Object.keys(status.accounts)).to.have.lengthOf(0) | ||
483 | |||
484 | expect(Object.keys(status.hosts)).to.have.lengthOf(2) | ||
485 | expect(status.hosts[blockedServer].blockedByUser).to.be.false | ||
486 | expect(status.hosts[blockedServer].blockedByServer).to.be.false | ||
487 | |||
488 | expect(status.hosts[notBlockedServer].blockedByUser).to.be.false | ||
489 | expect(status.hosts[notBlockedServer].blockedByServer).to.be.false | ||
490 | } | ||
491 | |||
492 | { | ||
493 | const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] }) | ||
494 | expect(Object.keys(status.accounts)).to.have.lengthOf(0) | ||
495 | |||
496 | expect(Object.keys(status.hosts)).to.have.lengthOf(2) | ||
497 | expect(status.hosts[blockedServer].blockedByUser).to.be.true | ||
498 | expect(status.hosts[blockedServer].blockedByServer).to.be.false | ||
499 | |||
500 | expect(status.hosts[notBlockedServer].blockedByUser).to.be.false | ||
501 | expect(status.hosts[notBlockedServer].blockedByServer).to.be.false | ||
502 | } | ||
503 | }) | ||
504 | |||
437 | it('Should unblock the remote server', async function () { | 505 | it('Should unblock the remote server', async function () { |
438 | await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port }) | 506 | await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port }) |
439 | }) | 507 | }) |
@@ -575,6 +643,27 @@ describe('Test blocklist', function () { | |||
575 | } | 643 | } |
576 | }) | 644 | }) |
577 | 645 | ||
646 | it('Should get blocked status', async function () { | ||
647 | const remoteHandle = 'user2@' + servers[1].host | ||
648 | const localHandle = 'user1@' + servers[0].host | ||
649 | const unknownHandle = 'user5@' + servers[0].host | ||
650 | |||
651 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
652 | const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] }) | ||
653 | expect(Object.keys(status.accounts)).to.have.lengthOf(3) | ||
654 | |||
655 | for (const handle of [ localHandle, remoteHandle ]) { | ||
656 | expect(status.accounts[handle].blockedByUser).to.be.false | ||
657 | expect(status.accounts[handle].blockedByServer).to.be.true | ||
658 | } | ||
659 | |||
660 | expect(status.accounts[unknownHandle].blockedByUser).to.be.false | ||
661 | expect(status.accounts[unknownHandle].blockedByServer).to.be.false | ||
662 | |||
663 | expect(Object.keys(status.hosts)).to.have.lengthOf(0) | ||
664 | } | ||
665 | }) | ||
666 | |||
578 | it('Should unblock the remote account', async function () { | 667 | it('Should unblock the remote account', async function () { |
579 | await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port }) | 668 | await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port }) |
580 | }) | 669 | }) |
@@ -620,6 +709,7 @@ describe('Test blocklist', function () { | |||
620 | }) | 709 | }) |
621 | 710 | ||
622 | describe('When managing server blocklist', function () { | 711 | describe('When managing server blocklist', function () { |
712 | |||
623 | it('Should list all videos', async function () { | 713 | it('Should list all videos', async function () { |
624 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { | 714 | for (const token of [ userModeratorToken, servers[0].accessToken ]) { |
625 | await checkAllVideos(servers[0], token) | 715 | await checkAllVideos(servers[0], token) |
@@ -713,6 +803,23 @@ describe('Test blocklist', function () { | |||
713 | expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) | 803 | expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) |
714 | }) | 804 | }) |
715 | 805 | ||
806 | it('Should get blocklist status', async function () { | ||
807 | const blockedServer = servers[1].host | ||
808 | const notBlockedServer = 'example.com' | ||
809 | |||
810 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
811 | const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] }) | ||
812 | expect(Object.keys(status.accounts)).to.have.lengthOf(0) | ||
813 | |||
814 | expect(Object.keys(status.hosts)).to.have.lengthOf(2) | ||
815 | expect(status.hosts[blockedServer].blockedByUser).to.be.false | ||
816 | expect(status.hosts[blockedServer].blockedByServer).to.be.true | ||
817 | |||
818 | expect(status.hosts[notBlockedServer].blockedByUser).to.be.false | ||
819 | expect(status.hosts[notBlockedServer].blockedByServer).to.be.false | ||
820 | } | ||
821 | }) | ||
822 | |||
716 | it('Should unblock the remote server', async function () { | 823 | it('Should unblock the remote server', async function () { |
717 | await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port }) | 824 | await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port }) |
718 | }) | 825 | }) |
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts index 468efdf35..9af20843e 100644 --- a/server/tests/api/notifications/user-notifications.ts +++ b/server/tests/api/notifications/user-notifications.ts | |||
@@ -267,7 +267,7 @@ describe('Test user notifications', function () { | |||
267 | }) | 267 | }) |
268 | 268 | ||
269 | it('Should send a notification when an imported video is transcoded', async function () { | 269 | it('Should send a notification when an imported video is transcoded', async function () { |
270 | this.timeout(50000) | 270 | this.timeout(120000) |
271 | 271 | ||
272 | const name = 'video import ' + buildUUID() | 272 | const name = 'video import ' + buildUUID() |
273 | 273 | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index ea524723c..96ec17b0f 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -43,6 +43,9 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { | |||
43 | expect(data.services.twitter.username).to.equal('@Chocobozzz') | 43 | expect(data.services.twitter.username).to.equal('@Chocobozzz') |
44 | expect(data.services.twitter.whitelisted).to.be.false | 44 | expect(data.services.twitter.whitelisted).to.be.false |
45 | 45 | ||
46 | expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false | ||
47 | expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false | ||
48 | |||
46 | expect(data.cache.previews.size).to.equal(1) | 49 | expect(data.cache.previews.size).to.equal(1) |
47 | expect(data.cache.captions.size).to.equal(1) | 50 | expect(data.cache.captions.size).to.equal(1) |
48 | expect(data.cache.torrents.size).to.equal(1) | 51 | expect(data.cache.torrents.size).to.equal(1) |
@@ -138,6 +141,9 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
138 | expect(data.services.twitter.username).to.equal('@Kuja') | 141 | expect(data.services.twitter.username).to.equal('@Kuja') |
139 | expect(data.services.twitter.whitelisted).to.be.true | 142 | expect(data.services.twitter.whitelisted).to.be.true |
140 | 143 | ||
144 | expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.true | ||
145 | expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.true | ||
146 | |||
141 | expect(data.cache.previews.size).to.equal(2) | 147 | expect(data.cache.previews.size).to.equal(2) |
142 | expect(data.cache.captions.size).to.equal(3) | 148 | expect(data.cache.captions.size).to.equal(3) |
143 | expect(data.cache.torrents.size).to.equal(4) | 149 | expect(data.cache.torrents.size).to.equal(4) |
@@ -246,6 +252,18 @@ const newCustomConfig: CustomConfig = { | |||
246 | whitelisted: true | 252 | whitelisted: true |
247 | } | 253 | } |
248 | }, | 254 | }, |
255 | client: { | ||
256 | videos: { | ||
257 | miniature: { | ||
258 | preferAuthorDisplayName: true | ||
259 | } | ||
260 | }, | ||
261 | menu: { | ||
262 | login: { | ||
263 | redirectOnSingleExternalAuth: true | ||
264 | } | ||
265 | } | ||
266 | }, | ||
249 | cache: { | 267 | cache: { |
250 | previews: { | 268 | previews: { |
251 | size: 2 | 269 | size: 2 |
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts index f2a984962..3ac440f84 100644 --- a/server/tests/cli/peertube.ts +++ b/server/tests/cli/peertube.ts | |||
@@ -207,6 +207,25 @@ describe('Test CLI wrapper', function () { | |||
207 | 207 | ||
208 | expect(res).to.not.contain('peertube-plugin-hello-world') | 208 | expect(res).to.not.contain('peertube-plugin-hello-world') |
209 | }) | 209 | }) |
210 | |||
211 | it('Should install a plugin in requested version', async function () { | ||
212 | this.timeout(60000) | ||
213 | |||
214 | await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world --plugin-version 0.0.17`) | ||
215 | }) | ||
216 | |||
217 | it('Should list installed plugins, in correct version', async function () { | ||
218 | const res = await cliCommand.execWithEnv(`${cmd} plugins list`) | ||
219 | |||
220 | expect(res).to.contain('peertube-plugin-hello-world') | ||
221 | expect(res).to.contain('0.0.17') | ||
222 | }) | ||
223 | |||
224 | it('Should uninstall the plugin again', async function () { | ||
225 | const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`) | ||
226 | |||
227 | expect(res).to.not.contain('peertube-plugin-hello-world') | ||
228 | }) | ||
210 | }) | 229 | }) |
211 | 230 | ||
212 | describe('Manage video redundancies', function () { | 231 | describe('Manage video redundancies', function () { |
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts index ae625114d..9dd3f08c9 100644 --- a/server/tools/peertube-plugins.ts +++ b/server/tools/peertube-plugins.ts | |||
@@ -31,6 +31,7 @@ program | |||
31 | .option('-p, --password <token>', 'Password') | 31 | .option('-p, --password <token>', 'Password') |
32 | .option('-P --path <path>', 'Install from a path') | 32 | .option('-P --path <path>', 'Install from a path') |
33 | .option('-n, --npm-name <npmName>', 'Install from npm') | 33 | .option('-n, --npm-name <npmName>', 'Install from npm') |
34 | .option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)') | ||
34 | .action((options, command) => installPluginCLI(command, options)) | 35 | .action((options, command) => installPluginCLI(command, options)) |
35 | 36 | ||
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/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/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/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/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/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/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/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" |