Merge remote-tracking branch 'origin' into feat-4596-edit-channel
This commit is contained in:
commit
2a8b94b4e4
46
.github/actions/reusable-deploy/action.yml
vendored
Normal file
46
.github/actions/reusable-deploy/action.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: "Reusable deploy on builds.joinpeertube.org"
|
||||
|
||||
description: "Reusable deploy on builds.joinpeertube.org"
|
||||
|
||||
inputs:
|
||||
source:
|
||||
required: true
|
||||
description: "Source file/files/directory/directories to deploy"
|
||||
destination:
|
||||
required: true
|
||||
description: "Destination directory on builds.joinpeertube.org"
|
||||
knownHosts:
|
||||
required: true
|
||||
description: "Known hosts"
|
||||
deployKey:
|
||||
required: true
|
||||
description: "Deploy key"
|
||||
deployUser:
|
||||
required: true
|
||||
description: "Deploy user"
|
||||
deployHost:
|
||||
required: true
|
||||
description: "Deploy host"
|
||||
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- name: "Deploy"
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
|
||||
echo "Adding ssh key to known hosts"
|
||||
echo -e "${{ inputs.knownHosts }}" > ~/.ssh/known_hosts;
|
||||
|
||||
eval `ssh-agent -s`
|
||||
|
||||
echo "Adding ssh deploy key"
|
||||
ssh-add <(echo "${{ inputs.deployKey }}");
|
||||
|
||||
echo "Uploading files"
|
||||
|
||||
scp ${{ inputs.source }} ${{ inputs.deployUser }}@${{ inputs.deployHost }}:../../web/${{ inputs.destination }};
|
31
.github/actions/reusable-prepare-peertube-build/action.yml
vendored
Normal file
31
.github/actions/reusable-prepare-peertube-build/action.yml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
name: "Reusable prepare PeerTube build"
|
||||
|
||||
description: "Reusable prepare PeerTube build"
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
required: true
|
||||
description: 'NodeJS version'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-node-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile
|
16
.github/actions/reusable-prepare-peertube-run/action.yml
vendored
Normal file
16
.github/actions/reusable-prepare-peertube-run/action.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: "Reusable prepare PeerTube run"
|
||||
description: "Reusable prepare PeerTube run"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- name: Setup system dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get install postgresql-client-common redis-tools parallel
|
||||
wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
|
||||
tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
|
||||
mkdir -p $HOME/bin
|
||||
cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
|
||||
echo "$HOME/bin" >> $GITHUB_PATH
|
69
.github/workflows/benchmark.yml
vendored
69
.github/workflows/benchmark.yml
vendored
|
@ -29,48 +29,15 @@ jobs:
|
|||
env:
|
||||
PGUSER: peertube
|
||||
PGHOST: localhost
|
||||
NODE_PENDING_JOB_WAIT: 500
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Setup system dependencies
|
||||
run: |
|
||||
sudo apt-get install postgresql-client-common redis-tools parallel
|
||||
wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
|
||||
tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
|
||||
mkdir -p $HOME/bin
|
||||
cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
|
||||
echo "$HOME/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-node-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- name: Cache fixtures
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
fixtures
|
||||
key: ${{ runner.OS }}-fixtures-${{ matrix.test_suite }}-${{ hashFiles('fixtures/*') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-fixtures-${{ matrix.test_suite }}-
|
||||
${{ runner.OS }}-fixtures-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- uses: './.github/actions/reusable-prepare-peertube-run'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
@ -111,27 +78,11 @@ jobs:
|
|||
cat benchmark.json build-time.json startup-time.json
|
||||
|
||||
- name: Upload benchmark result
|
||||
env:
|
||||
STATS_DEPLOYEMENT_KNOWN_HOSTS: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
|
||||
STATS_DEPLOYEMENT_KEY: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
|
||||
STATS_DEPLOYEMENT_USER: ${{ secrets.STATS_DEPLOYEMENT_USER }}
|
||||
STATS_DEPLOYEMENT_HOST: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
if [ ! -z ${STATS_DEPLOYEMENT_KNOWN_HOSTS+x} ]; then
|
||||
echo "Adding ssh key to known hosts"
|
||||
echo -e "${STATS_DEPLOYEMENT_KNOWN_HOSTS}" > ~/.ssh/known_hosts;
|
||||
fi
|
||||
|
||||
eval `ssh-agent -s`
|
||||
|
||||
if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
|
||||
echo "Adding ssh reployement key"
|
||||
ssh-add <(echo "${STATS_DEPLOYEMENT_KEY}");
|
||||
fi
|
||||
|
||||
if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
|
||||
echo "Uploading files"
|
||||
scp benchmark.json build-time.json startup-time.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
|
||||
fi
|
||||
uses: './.github/actions/reusable-deploy'
|
||||
with:
|
||||
source: benchmark.json build-time.json startup-time.json
|
||||
destination: peertube-stats
|
||||
knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
|
||||
deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
|
||||
deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
|
||||
deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
|
||||
|
|
68
.github/workflows/codeql.yml
vendored
Normal file
68
.github/workflows/codeql.yml
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop, next ]
|
||||
schedule:
|
||||
- cron: '36 9 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/workflows/codeql/codeql-config.yml
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
4
.github/workflows/codeql/codeql-config.yml
vendored
Normal file
4
.github/workflows/codeql/codeql-config.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
name: "PeerTube CodeQL config"
|
||||
|
||||
paths-ignore:
|
||||
- server/tests
|
70
.github/workflows/docker.yml
vendored
Normal file
70
.github/workflows/docker.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
jobs:
|
||||
generate-matrix:
|
||||
name: Generate matrix for Docker build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
- name: Set matrix for build
|
||||
id: set-matrix
|
||||
run: |
|
||||
# FIXME: https://github.com/actions/checkout/issues/290
|
||||
git fetch --force --tags
|
||||
|
||||
one="{ \"file\": \"./support/docker/production/Dockerfile.bullseye\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bullseye\" }"
|
||||
two="{ \"file\": \"./support/docker/production/Dockerfile.buster\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-buster,chocobozzz/peertube:$(git describe --abbrev=0)-buster\" }"
|
||||
three="{ \"file\": \"./support/docker/production/Dockerfile.nginx\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube-webserver:latest\" }"
|
||||
|
||||
matrix="[$one,$two,$three]"
|
||||
echo ::set-output name=matrix::{\"include\":$(echo $matrix)}
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs: generate-matrix
|
||||
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Checkout develop
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ matrix.ref }}
|
||||
-
|
||||
name: Docker build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: '.'
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: ${{ matrix.file }}
|
||||
tags: ${{ matrix.tags }}
|
33
.github/workflows/nightly.yml
vendored
Normal file
33
.github/workflows/nightly.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
name: Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
jobs:
|
||||
|
||||
nightly:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout develop
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- name: Build
|
||||
run: npm run nightly
|
||||
|
||||
- uses: './.github/actions/reusable-deploy'
|
||||
with:
|
||||
source: ./peertube-nightly-*
|
||||
destination: nightly
|
||||
knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
|
||||
deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
|
||||
deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
|
||||
deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
|
50
.github/workflows/stats.yml
vendored
50
.github/workflows/stats.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: "Stats"
|
||||
name: Stats
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -20,24 +20,10 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||
with:
|
||||
node-version: '14.x'
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-node-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Angular bundlewatch
|
||||
uses: jackyef/bundlewatch-gh-action@master
|
||||
with:
|
||||
|
@ -73,27 +59,11 @@ jobs:
|
|||
|
||||
- name: Upload stats
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
STATS_DEPLOYEMENT_KNOWN_HOSTS: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
|
||||
STATS_DEPLOYEMENT_KEY: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
|
||||
STATS_DEPLOYEMENT_USER: ${{ secrets.STATS_DEPLOYEMENT_USER }}
|
||||
STATS_DEPLOYEMENT_HOST: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
if [ ! -z ${STATS_DEPLOYEMENT_KNOWN_HOSTS+x} ]; then
|
||||
echo "Adding ssh key to known hosts"
|
||||
echo -e "${STATS_DEPLOYEMENT_KNOWN_HOSTS}" > ~/.ssh/known_hosts;
|
||||
fi
|
||||
|
||||
eval `ssh-agent -s`
|
||||
|
||||
if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
|
||||
echo "Adding ssh reployement key"
|
||||
ssh-add <(echo "${STATS_DEPLOYEMENT_KEY}");
|
||||
fi
|
||||
|
||||
if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
|
||||
echo "Uploading files"
|
||||
scp lighthouse.json client-build-stats.json scc.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
|
||||
fi
|
||||
uses: './.github/actions/reusable-deploy'
|
||||
with:
|
||||
source: lighthouse.json client-build-stats.json scc.json
|
||||
destination: peertube-stats
|
||||
knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
|
||||
deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
|
||||
deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
|
||||
deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
|
||||
|
|
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
|
@ -1,4 +1,4 @@
|
|||
name: Test Suite
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -50,29 +50,11 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- uses: './.github/actions/reusable-prepare-peertube-build'
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Setup system dependencies
|
||||
run: |
|
||||
sudo apt-get install postgresql-client-common redis-tools parallel
|
||||
wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
|
||||
tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
|
||||
mkdir -p $HOME/bin
|
||||
cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
|
||||
echo "$HOME/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.OS }}-node-
|
||||
${{ runner.OS }}-
|
||||
- uses: './.github/actions/reusable-prepare-peertube-run'
|
||||
|
||||
- name: Cache fixtures
|
||||
uses: actions/cache@v2
|
||||
|
@ -85,9 +67,6 @@ jobs:
|
|||
${{ runner.OS }}-fixtures-
|
||||
${{ runner.OS }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Set env test variable (schedule)
|
||||
if: github.event_name != 'schedule'
|
||||
run: |
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
image: chocobozzz/peertube-ci:14
|
||||
|
||||
stages:
|
||||
- clients
|
||||
- docker-nightly
|
||||
|
||||
cache:
|
||||
key: yarn
|
||||
paths:
|
||||
- .yarn-cache
|
||||
- cached-fixtures
|
||||
|
||||
# build-openapi-clients:
|
||||
# stage: clients
|
||||
# only:
|
||||
# refs:
|
||||
# - master
|
||||
# - schedules
|
||||
# changes:
|
||||
# - support/doc/api/openapi.yaml
|
||||
# script:
|
||||
# - apt-get update -qq
|
||||
# - apt-get -yqqq install openjdk-8-jre
|
||||
# - yarn install --pure-lockfile
|
||||
# - scripts/openapi-peertube-version.sh
|
||||
# - scripts/openapi-clients.sh
|
||||
|
||||
build-nightly:
|
||||
stage: docker-nightly
|
||||
only:
|
||||
- schedules
|
||||
script:
|
||||
- yarn install --pure-lockfile --cache-folder .yarn-cache
|
||||
- npm run nightly
|
||||
- mkdir "${HOME}/.ssh"
|
||||
- chmod 700 "${HOME}/.ssh"
|
||||
- if [ ! -z ${DEPLOYEMENT_KNOWN_HOSTS+x} ]; then echo -e "${DEPLOYEMENT_KNOWN_HOSTS}" > ${HOME}/.ssh/known_hosts; fi
|
||||
- eval `ssh-agent -s`
|
||||
- if [ ! -z ${DEPLOYEMENT_KEY+x} ]; then ssh-add <(echo "${DEPLOYEMENT_KEY}"); fi
|
||||
- if [ ! -z ${DEPLOYEMENT_KEY+x} ]; then scp ./peertube-nightly-* ${DEPLOYEMENT_USER}@${DEPLOYEMENT_HOST}:../../web/nightly; fi
|
||||
|
||||
.docker: &docker
|
||||
stage: docker-nightly
|
||||
cache: {}
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$CI_REGISTRY_AUTH\",\"email\":\"$CI_REGISTRY_EMAIL\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $DOCKERFILE --destination $DOCKER_IMAGE_NAME
|
||||
|
||||
build-docker-develop:
|
||||
<<: *docker
|
||||
only:
|
||||
- schedules
|
||||
variables:
|
||||
DOCKER_IMAGE_NAME: chocobozzz/peertube:develop-bullseye
|
||||
DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
|
||||
|
||||
build-docker-webserver:
|
||||
<<: *docker
|
||||
only:
|
||||
- schedules
|
||||
variables:
|
||||
DOCKER_IMAGE_NAME: chocobozzz/peertube-webserver
|
||||
DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.nginx
|
||||
|
||||
build-docker-tag:
|
||||
<<: *docker
|
||||
only:
|
||||
- tags
|
||||
variables:
|
||||
DOCKER_IMAGE_NAME: chocobozzz/peertube:$CI_COMMIT_TAG-bullseye
|
||||
DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
|
||||
|
||||
build-docker-master:
|
||||
<<: *docker
|
||||
only:
|
||||
- master
|
||||
variables:
|
||||
DOCKER_IMAGE_NAME: chocobozzz/peertube:production-bullseye
|
||||
DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
|
|
@ -116,95 +116,99 @@
|
|||
<my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container>
|
||||
</div>
|
||||
|
||||
<div class="anchor" id="moderation"></div>
|
||||
<a
|
||||
*ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
MODERATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block moderation-information" *ngIf="html.moderationInformation">
|
||||
<div class="anchor" id="moderation-information"></div>
|
||||
<div myPluginSelector pluginSelectorId="about-instance-moderation">
|
||||
<div class="anchor" id="moderation"></div>
|
||||
<a
|
||||
*ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation-information"
|
||||
fragment="moderation"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Moderation information</h3>
|
||||
<h2 i18n class="middle-title">
|
||||
MODERATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="html.moderationInformation"></div>
|
||||
<div class="block moderation-information" *ngIf="html.moderationInformation">
|
||||
<div class="anchor" id="moderation-information"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="moderation-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Moderation information</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="html.moderationInformation"></div>
|
||||
</div>
|
||||
|
||||
<div class="block code-of-conduct" *ngIf="html.codeOfConduct">
|
||||
<div class="anchor" id="code-of-conduct"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="code-of-conduct"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Code of conduct</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="html.codeOfConduct"></div>
|
||||
</div>
|
||||
|
||||
<div class="block terms">
|
||||
<div class="anchor" id="terms"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="terms"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Terms</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="html.terms"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block code-of-conduct" *ngIf="html.codeOfConduct">
|
||||
<div class="anchor" id="code-of-conduct"></div>
|
||||
<div myPluginSelector pluginSelectorId="about-instance-other-information">
|
||||
<div class="anchor" id="other-information"></div>
|
||||
<a
|
||||
*ngIf="html.hardwareInformation"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="code-of-conduct"
|
||||
fragment="other-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Code of conduct</h3>
|
||||
<h2 i18n class="middle-title">
|
||||
OTHER INFORMATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="html.codeOfConduct"></div>
|
||||
</div>
|
||||
<div class="block hardware-information" *ngIf="html.hardwareInformation">
|
||||
<div class="anchor" id="hardware-information"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="hardware-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Hardware information</h3>
|
||||
</a>
|
||||
|
||||
<div class="block terms">
|
||||
<div class="anchor" id="terms"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="terms"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Terms</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="html.terms"></div>
|
||||
</div>
|
||||
|
||||
<div class="anchor" id="other-information"></div>
|
||||
<a
|
||||
*ngIf="html.hardwareInformation"
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="other-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h2 i18n class="middle-title">
|
||||
OTHER INFORMATION
|
||||
</h2>
|
||||
</a>
|
||||
|
||||
<div class="block hardware-information" *ngIf="html.hardwareInformation">
|
||||
<div class="anchor" id="hardware-information"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
routerLink="/about/instance"
|
||||
fragment="hardware-information"
|
||||
#anchorLink
|
||||
(click)="onClickCopyLink(anchorLink)">
|
||||
<h3 i18n class="section-title">Hardware information</h3>
|
||||
</a>
|
||||
|
||||
<div [innerHTML]="html.hardwareInformation"></div>
|
||||
<div [innerHTML]="html.hardwareInformation"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 col-xl-6">
|
||||
<div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
|
||||
<h2 class="sr-only" i18n>FEATURES</h2>
|
||||
<my-instance-features-table></my-instance-features-table>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="col" myPluginSelector pluginSelectorId="about-instance-statistics">
|
||||
<div class="anchor" id="statistics"></div>
|
||||
<a
|
||||
class="anchor-link"
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
|
||||
|
||||
<div class="links">
|
||||
<a i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a>
|
||||
<a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a>
|
||||
|
||||
<a i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a>
|
||||
<a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a>
|
||||
|
||||
<a i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a>
|
||||
<a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -19,10 +19,8 @@
|
|||
></my-user-moderation-dropdown>
|
||||
|
||||
<span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
|
||||
<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
|
||||
<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
|
||||
<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
|
||||
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
|
||||
|
||||
<my-account-block-badges [account]="account"></my-account-block-badges>
|
||||
</div>
|
||||
|
||||
<div class="actor-handle">
|
||||
|
@ -85,5 +83,5 @@
|
|||
</div>
|
||||
|
||||
<ng-container *ngIf="prependModerationActions">
|
||||
<my-account-report #accountReportModal [account]="account"></my-account-report>
|
||||
<my-account-report #accountReportModal></my-account-report>
|
||||
</ng-container>
|
||||
|
|
|
@ -30,16 +30,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
my-user-moderation-dropdown,
|
||||
.badge {
|
||||
@include margin-left(10px);
|
||||
my-user-moderation-dropdown {
|
||||
margin: 0 10px;
|
||||
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 13px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
|
@ -64,6 +58,10 @@ my-user-moderation-dropdown,
|
|||
@include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
|
||||
}
|
||||
|
||||
.actor-display-name {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
grid-column: 1 / 3;
|
||||
max-width: 1000px;
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
VideoChannelService,
|
||||
VideoService
|
||||
} from '@app/shared/shared-main'
|
||||
import { AccountReportComponent } from '@app/shared/shared-moderation'
|
||||
import { AccountReportComponent, BlocklistService } from '@app/shared/shared-moderation'
|
||||
import { HttpStatusCode, User, UserRight } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
|
@ -52,6 +52,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
private authService: AuthService,
|
||||
private videoService: VideoService,
|
||||
private markdown: MarkdownService,
|
||||
private blocklist: BlocklistService,
|
||||
private screenService: ScreenService
|
||||
) {
|
||||
}
|
||||
|
@ -159,10 +160,11 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
this.updateModerationActions()
|
||||
this.loadUserIfNeeded(account)
|
||||
this.loadAccountVideosCount()
|
||||
this.loadAccountBlockStatus()
|
||||
}
|
||||
|
||||
private showReportModal () {
|
||||
this.accountReportModal.show()
|
||||
this.accountReportModal.show(this.account)
|
||||
}
|
||||
|
||||
private loadUserIfNeeded (account: Account) {
|
||||
|
@ -217,4 +219,9 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
this.accountVideosCount = res.total
|
||||
})
|
||||
}
|
||||
|
||||
private loadAccountBlockStatus () {
|
||||
this.blocklist.getStatus({ accounts: [ this.account.nameWithHostForced ], hosts: [ this.account.host ] })
|
||||
.subscribe(status => this.account.updateBlockStatus(status))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,36 @@
|
|||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="client">
|
||||
|
||||
<ng-container formGroupName="videos">
|
||||
<ng-container formGroupName="miniature">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientVideosMiniaturePreferAuthorDisplayName" formControlName="preferAuthorDisplayName"
|
||||
i18n-labelText labelText="Prefer author display name in video miniature"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="menu">
|
||||
<ng-container formGroupName="login">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="clientMenuLoginRedirectOnSingleExternalAuth" formControlName="redirectOnSingleExternalAuth"
|
||||
i18n-labelText labelText="Redirect users on single external auth when users click on the login button in menu"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span *ngIf="countExternalAuth() === 0" i18n>⚠️ You don't have any external auth plugin enabled.</span>
|
||||
<span *ngIf="countExternalAuth() > 1" i18n>⚠️ You have multiple external auth plugins enabled.</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -276,7 +306,7 @@
|
|||
<div class="form-group col-12 col-lg-4 col-xl-3">
|
||||
<div i18n class="inner-form-title">VIDEO CHANNELS</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
|
||||
<div class="form-group" formGroupName="videoChannels">
|
||||
<label i18n for="videoChannelsMaxPerUser">Max video channels per user</label>
|
||||
|
|
|
@ -36,6 +36,10 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
|
|||
}
|
||||
}
|
||||
|
||||
countExternalAuth () {
|
||||
return this.serverConfig.plugin.registeredExternalAuths.length
|
||||
}
|
||||
|
||||
getVideoQuotaOptions () {
|
||||
return this.configService.videoQuotaOptions
|
||||
}
|
||||
|
|
|
@ -106,6 +106,18 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
whitelisted: null
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: null
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: null
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: CACHE_PREVIEWS_SIZE_VALIDATOR
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
i18n-labelText labelText="Allow additional extensions"
|
||||
>
|
||||
<ng-container ngProjectAs="description">
|
||||
<span i18n>Allows users to upload {{ additionalVideoExtensions }} videos.</span>
|
||||
<span i18n>Allows users to upload videos with additional extensions than .mp4, .ogv and .webm (for example: .avi, .mov, .mkv etc).</span>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
|
|
@ -48,7 +48,8 @@
|
|||
<input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
|
||||
|
||||
<div class="additionnal-links">
|
||||
<a i18n class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
|
||||
<a i18n role="button" class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
|
||||
|
||||
<div *ngIf="signupAllowed" class="signup-link">
|
||||
<span>·</span>
|
||||
<a i18n routerLink="/signup" class="create-an-account">Create an account</a>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { environment } from 'src/environments/environment'
|
||||
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
|
||||
|
@ -7,6 +7,7 @@ import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/
|
|||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
||||
import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PluginsManager } from '@root-helpers/plugins-manager'
|
||||
import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
|
@ -98,7 +99,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
|
|||
}
|
||||
|
||||
getAuthHref (auth: RegisteredExternalAuthConfig) {
|
||||
return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
|
||||
return PluginsManager.getExternalAuthHref(auth)
|
||||
}
|
||||
|
||||
login () {
|
||||
|
|
|
@ -23,14 +23,16 @@
|
|||
<div class="section-label" i18n>OWNER ACCOUNT</div>
|
||||
|
||||
<div class="avatar-row">
|
||||
<my-actor-avatar class="account-avatar" [account]="videoChannel.ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
|
||||
<my-actor-avatar class="account-avatar" [account]="ownerAccount" [internalHref]="getAccountUrl()"></my-actor-avatar>
|
||||
|
||||
<div class="actor-info">
|
||||
<h4>
|
||||
<a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a>
|
||||
<a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ ownerAccount.displayName }}</a>
|
||||
</h4>
|
||||
|
||||
<div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
|
||||
|
||||
<my-account-block-badges [account]="ownerAccount"></my-account-block-badges>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@ import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators
|
|||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
|
||||
import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
|
||||
import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
|
||||
import { BlocklistService } from '@app/shared/shared-moderation'
|
||||
import { SupportModalComponent } from '@app/shared/shared-support-modal'
|
||||
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
|
||||
import { HttpStatusCode, UserRight } from '@shared/models'
|
||||
|
@ -18,6 +19,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
|
|||
@ViewChild('supportModal') supportModal: SupportModalComponent
|
||||
|
||||
videoChannel: VideoChannel
|
||||
ownerAccount: Account
|
||||
hotkeys: Hotkey[]
|
||||
links: ListOverflowItem[] = []
|
||||
isChannelManageable = false
|
||||
|
@ -38,7 +40,8 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
|
|||
private restExtractor: RestExtractor,
|
||||
private hotkeysService: HotkeysService,
|
||||
private screenService: ScreenService,
|
||||
private markdown: MarkdownService
|
||||
private markdown: MarkdownService,
|
||||
private blocklist: BlocklistService
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
|
@ -58,8 +61,10 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
|
|||
|
||||
// After the markdown renderer to avoid layout changes
|
||||
this.videoChannel = videoChannel
|
||||
this.ownerAccount = new Account(this.videoChannel.ownerAccount)
|
||||
|
||||
this.loadChannelVideosCount()
|
||||
this.loadOwnerBlockStatus()
|
||||
})
|
||||
|
||||
this.hotkeys = [
|
||||
|
@ -129,4 +134,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
|
|||
sort: '-publishedAt'
|
||||
}).subscribe(res => this.channelVideosCount = res.total)
|
||||
}
|
||||
|
||||
private loadOwnerBlockStatus () {
|
||||
this.blocklist.getStatus({ accounts: [ this.ownerAccount.nameWithHostForced ], hosts: [ this.ownerAccount.host ] })
|
||||
.subscribe(status => this.ownerAccount.updateBlockStatus(status))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,16 @@ import { NgModule } from '@angular/core'
|
|||
import { SharedFormModule } from '@app/shared/shared-forms'
|
||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
||||
import { SharedSupportModal } from '@app/shared/shared-support-modal'
|
||||
import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
|
||||
import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
|
||||
import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
|
||||
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
|
||||
import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
|
||||
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
|
||||
import { VideoChannelsRoutingModule } from './video-channels-routing.module'
|
||||
import { VideoChannelsComponent } from './video-channels.component'
|
||||
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
|
||||
import { MyVideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
|
||||
import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
|
||||
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
|
||||
|
@ -27,7 +28,8 @@ import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
|
|||
SharedGlobalIconModule,
|
||||
SharedSupportModal,
|
||||
SharedActorImageModule,
|
||||
SharedActorImageEditModule
|
||||
SharedActorImageEditModule,
|
||||
SharedModerationModule
|
||||
],
|
||||
|
||||
declarations: [
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { truncate } from 'lodash-es'
|
||||
import { UploadState, UploadxOptions, UploadxService } from 'ngx-uploadx'
|
||||
import { isIOS } from 'src/assets/player/utils'
|
||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
||||
import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
|
@ -10,7 +12,6 @@ import { LoadingBarService } from '@ngx-loading-bar/core'
|
|||
import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
|
||||
import { UploaderXFormData } from './uploaderx-form-data'
|
||||
import { VideoSend } from './video-send'
|
||||
import { isIOS } from 'src/assets/player/utils'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-upload',
|
||||
|
@ -281,6 +282,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
channelId: this.firstStepChannelId,
|
||||
nsfw: this.serverConfig.instance.isNSFW,
|
||||
privacy: this.highestPrivacy.toString(),
|
||||
name: this.buildVideoFilename(file.name),
|
||||
filename: file.name,
|
||||
previewfile: previewfile as any
|
||||
}
|
||||
|
@ -311,8 +313,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
}
|
||||
|
||||
private closeFirstStep (filename: string) {
|
||||
const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
|
||||
const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension
|
||||
const name = this.buildVideoFilename(filename)
|
||||
|
||||
this.form.patchValue({
|
||||
name,
|
||||
|
@ -369,4 +370,18 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
|
||||
return extensions.some(e => filename.endsWith(e))
|
||||
}
|
||||
|
||||
private buildVideoFilename (filename: string) {
|
||||
const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
|
||||
let name = nameWithoutExtension.length < 3
|
||||
? filename
|
||||
: nameWithoutExtension
|
||||
|
||||
const videoNameMaxSize = 110
|
||||
if (name.length > videoNameMaxSize) {
|
||||
name = truncate(name, { length: videoNameMaxSize, omission: '' })
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="d-inline-flex position-relative" id="typeahead-container">
|
||||
<input
|
||||
type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, playlists, channels…"
|
||||
[(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)" (keydown.enter)="doSearch()"
|
||||
type="search" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, playlists, channels…"
|
||||
[(ngModel)]="search" (ngModelChange)="onSearchChange()" (keydown)="handleKey($event)"
|
||||
aria-label="Search" autocomplete="off"
|
||||
>
|
||||
<my-global-icon
|
||||
|
@ -14,7 +14,7 @@
|
|||
<ul [hidden]="!search || !areSuggestionsOpened" role="listbox" class="p-0 m-0">
|
||||
<li
|
||||
*ngFor="let result of results; let i = index" class="suggestion d-flex flex-justify-start flex-items-center p-0 f5"
|
||||
role="option" aria-selected="true" (mouseenter)="onSuggestionHover(i)" (click)="onSuggestionlicked(result)"
|
||||
role="option" aria-selected="true" (mouseenter)="onSuggestionHover(i)" (click)="onSuggestionClicked(result)"
|
||||
>
|
||||
<my-suggestion [result]="result" [highlight]="search"></my-suggestion>
|
||||
</li>
|
||||
|
|
|
@ -152,7 +152,7 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewChecked, OnDes
|
|||
}
|
||||
}
|
||||
|
||||
onSuggestionlicked (payload: SuggestionPayload) {
|
||||
onSuggestionClicked (payload: SuggestionPayload) {
|
||||
this.doSearch(this.buildSearchTarget(payload))
|
||||
}
|
||||
|
||||
|
@ -170,6 +170,11 @@ export class SearchTypeaheadComponent implements OnInit, AfterViewChecked, OnDes
|
|||
|
||||
this.keyboardEventsManager.onKeydown(event)
|
||||
break
|
||||
|
||||
case 'Enter':
|
||||
event.stopPropagation()
|
||||
this.doSearch()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,10 @@
|
|||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()">
|
||||
<a
|
||||
myPluginSelector pluginSelectorId="menu-user-dropdown-language-item"
|
||||
ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"
|
||||
>
|
||||
<my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
|
||||
<span i18n>Interface:</span>
|
||||
<span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span>
|
||||
|
@ -96,7 +99,9 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="!isLoggedIn" class="login-buttons-block">
|
||||
<a i18n routerLink="/login" class="peertube-button-link orange-button">Login</a>
|
||||
<a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a>
|
||||
<a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a>
|
||||
|
||||
<a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
|
|||
import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
|
||||
import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
|
||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PluginsManager } from '@root-helpers/plugins-manager'
|
||||
import { HTMLServerConfig, ServerConfig, UserRight, VideoConstant } from '@shared/models'
|
||||
|
||||
const logger = debug('peertube:menu:MenuComponent')
|
||||
|
@ -129,6 +130,15 @@ export class MenuComponent implements OnInit {
|
|||
.subscribe(() => this.openQuickSettings())
|
||||
}
|
||||
|
||||
getExternalLoginHref () {
|
||||
if (this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined
|
||||
|
||||
const externalAuths = this.serverConfig.plugin.registeredExternalAuths
|
||||
if (externalAuths.length !== 1) return undefined
|
||||
|
||||
return PluginsManager.getExternalAuthHref(externalAuths[0])
|
||||
}
|
||||
|
||||
isRegistrationAllowed () {
|
||||
if (!this.serverConfig) return false
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Account as ServerAccount, ActorImage } from '@shared/models'
|
||||
import { Account as ServerAccount, ActorImage, BlockStatus } from '@shared/models'
|
||||
import { Actor } from './actor.model'
|
||||
|
||||
export class Account extends Actor implements ServerAccount {
|
||||
|
@ -49,4 +49,11 @@ export class Account extends Actor implements ServerAccount {
|
|||
resetAvatar () {
|
||||
this.avatar = null
|
||||
}
|
||||
|
||||
updateBlockStatus (blockStatus: BlockStatus) {
|
||||
this.mutedByInstance = blockStatus.accounts[this.nameWithHostForced].blockedByServer
|
||||
this.mutedByUser = blockStatus.accounts[this.nameWithHostForced].blockedByUser
|
||||
this.mutedServerByUser = blockStatus.hosts[this.host].blockedByUser
|
||||
this.mutedServerByInstance = blockStatus.hosts[this.host].blockedByServer
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
|
||||
<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
|
||||
<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
|
||||
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
|
|
@ -0,0 +1,9 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.badge {
|
||||
@include margin-right(10px);
|
||||
|
||||
height: fit-content;
|
||||
font-size: 12px;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { Account } from '../shared-main'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-block-badges',
|
||||
styleUrls: [ './account-block-badges.component.scss' ],
|
||||
templateUrl: './account-block-badges.component.html'
|
||||
})
|
||||
export class AccountBlockBadgesComponent {
|
||||
@Input() account: Account
|
||||
}
|
|
@ -3,7 +3,7 @@ import { catchError, map } from 'rxjs/operators'
|
|||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { RestExtractor, RestPagination, RestService } from '@app/core'
|
||||
import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '@shared/models'
|
||||
import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { Account } from '../shared-main'
|
||||
import { AccountBlock } from './account-block.model'
|
||||
|
@ -12,6 +12,7 @@ export enum BlocklistComponentType { Account, Instance }
|
|||
|
||||
@Injectable()
|
||||
export class BlocklistService {
|
||||
static BASE_BLOCKLIST_URL = environment.apiUrl + '/api/v1/blocklist'
|
||||
static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist'
|
||||
static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist'
|
||||
|
||||
|
@ -21,6 +22,23 @@ export class BlocklistService {
|
|||
private restService: RestService
|
||||
) { }
|
||||
|
||||
/** ********************* Blocklist status ***********************/
|
||||
|
||||
getStatus (options: {
|
||||
accounts?: string[]
|
||||
hosts?: string[]
|
||||
}) {
|
||||
const { accounts, hosts } = options
|
||||
|
||||
let params = new HttpParams()
|
||||
|
||||
if (accounts) params = this.restService.addArrayParams(params, 'accounts', accounts)
|
||||
if (hosts) params = this.restService.addArrayParams(params, 'hosts', hosts)
|
||||
|
||||
return this.authHttp.get<BlockStatus>(BlocklistService.BASE_BLOCKLIST_URL + '/status', { params })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
/** ********************* User -> Account blocklist ***********************/
|
||||
|
||||
getUserAccountBlocklist (options: { pagination: RestPagination, sort: SortMeta, search?: string }) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * from './report-modals'
|
||||
|
||||
export * from './abuse.service'
|
||||
export * from './account-block-badges.component'
|
||||
export * from './account-block.model'
|
||||
export * from './account-blocklist.component'
|
||||
export * from './batch-domains-modal.component'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mapValues, pickBy } from 'lodash-es'
|
||||
import { Component, Input, OnInit, ViewChild } from '@angular/core'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
|
||||
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
|
||||
|
@ -16,13 +16,12 @@ import { AbuseService } from '../abuse.service'
|
|||
styleUrls: [ './report.component.scss' ]
|
||||
})
|
||||
export class AccountReportComponent extends FormReactive implements OnInit {
|
||||
@Input() account: Account = null
|
||||
|
||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||
|
||||
error: string = null
|
||||
predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
|
||||
modalTitle: string
|
||||
account: Account = null
|
||||
|
||||
private openedModal: NgbModalRef
|
||||
|
||||
|
@ -48,8 +47,6 @@ export class AccountReportComponent extends FormReactive implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.modalTitle = $localize`Report ${this.account.displayName}`
|
||||
|
||||
this.buildForm({
|
||||
reason: ABUSE_REASON_VALIDATOR,
|
||||
predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
|
||||
|
@ -58,7 +55,11 @@ export class AccountReportComponent extends FormReactive implements OnInit {
|
|||
this.predefinedReasons = this.abuseService.getPrefefinedReasons('account')
|
||||
}
|
||||
|
||||
show () {
|
||||
show (account: Account) {
|
||||
this.account = account
|
||||
|
||||
this.modalTitle = $localize`Report ${this.account.displayName}`
|
||||
|
||||
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import { UserBanModalComponent } from './user-ban-modal.component'
|
|||
import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
|
||||
import { VideoBlockComponent } from './video-block.component'
|
||||
import { VideoBlockService } from './video-block.service'
|
||||
import { AccountBlockBadgesComponent } from './account-block-badges.component'
|
||||
import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
|
||||
|
||||
@NgModule({
|
||||
|
@ -31,7 +32,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
|
|||
VideoReportComponent,
|
||||
BatchDomainsModalComponent,
|
||||
CommentReportComponent,
|
||||
AccountReportComponent
|
||||
AccountReportComponent,
|
||||
AccountBlockBadgesComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -41,7 +43,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image
|
|||
VideoReportComponent,
|
||||
BatchDomainsModalComponent,
|
||||
CommentReportComponent,
|
||||
AccountReportComponent
|
||||
AccountReportComponent,
|
||||
AccountBlockBadgesComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
|
|
|
@ -289,13 +289,13 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
|
|||
{
|
||||
label: $localize`Mute the instance`,
|
||||
description: $localize`Hide any content from that instance for you.`,
|
||||
isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
|
||||
isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === false,
|
||||
handler: ({ account }) => this.blockServerByUser(account.host)
|
||||
},
|
||||
{
|
||||
label: $localize`Unmute the instance`,
|
||||
description: $localize`Show back content from that instance for you.`,
|
||||
isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
|
||||
isDisplayed: ({ account }) => !account.userId && account.mutedServerByUser === true,
|
||||
handler: ({ account }) => this.unblockServerByUser(account.host)
|
||||
},
|
||||
{
|
||||
|
|
|
@ -146,7 +146,10 @@ class Html5Hlsjs {
|
|||
}
|
||||
|
||||
duration () {
|
||||
return this._duration || this.videoElement.duration || 0
|
||||
if (this._duration === Infinity) return Infinity
|
||||
if (!isNaN(this.videoElement.duration)) return this.videoElement.duration
|
||||
|
||||
return this._duration || 0
|
||||
}
|
||||
|
||||
seekable () {
|
||||
|
@ -366,6 +369,7 @@ class Html5Hlsjs {
|
|||
|
||||
this.isLive = data.details.live
|
||||
this.dvrDuration = data.details.totalduration
|
||||
|
||||
this._duration = this.isLive ? Infinity : data.details.totalduration
|
||||
})
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
RegisterClientHookOptions,
|
||||
RegisterClientSettingsScript,
|
||||
RegisterClientVideoFieldOptions,
|
||||
RegisteredExternalAuthConfig,
|
||||
ServerConfigPlugin
|
||||
} from '../../../shared/models'
|
||||
import { environment } from '../environments/environment'
|
||||
|
@ -78,6 +79,11 @@ class PluginsManager {
|
|||
return isTheme ? '/themes' : '/plugins'
|
||||
}
|
||||
|
||||
static getExternalAuthHref (auth: RegisteredExternalAuthConfig) {
|
||||
return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
|
||||
|
||||
}
|
||||
|
||||
loadPluginsList (config: HTMLServerConfig) {
|
||||
for (const plugin of config.plugin.registered) {
|
||||
this.addPlugin(plugin)
|
||||
|
|
|
@ -82,6 +82,12 @@ client:
|
|||
# By default PeerTube client displays author username
|
||||
prefer_author_display_name: false
|
||||
|
||||
menu:
|
||||
login:
|
||||
# If you enable only one external auth plugin
|
||||
# You can automatically redirect your users on this external platform when they click on the login button
|
||||
redirect_on_single_external_auth: false
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
||||
|
|
|
@ -80,6 +80,12 @@ client:
|
|||
# By default PeerTube client displays author username
|
||||
prefer_author_display_name: false
|
||||
|
||||
menu:
|
||||
login:
|
||||
# If you enable only one external auth plugin
|
||||
# You can automatically redirect your users on this external platform when they click on the login button
|
||||
redirect_on_single_external_auth: false
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing...
|
||||
|
|
|
@ -155,6 +155,7 @@ search:
|
|||
federation:
|
||||
videos:
|
||||
federate_unlisted: true
|
||||
cleanup_remote_interactions: false
|
||||
|
||||
views:
|
||||
videos:
|
||||
|
|
|
@ -90,7 +90,6 @@
|
|||
"cookie-parser": "^1.4.3",
|
||||
"cors": "^2.8.1",
|
||||
"create-torrent": "^5.0.0",
|
||||
"decache": "^4.6.0",
|
||||
"deep-object-diff": "^1.1.0",
|
||||
"email-templates": "^8.0.3",
|
||||
"execa": "^5.1.1",
|
||||
|
|
108
server/controllers/api/blocklist.ts
Normal file
108
server/controllers/api/blocklist.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import express from 'express'
|
||||
import { handleToNameAndHost } from '@server/helpers/actors'
|
||||
import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
|
||||
import { MActorAccountId, MUserAccountId } from '@server/types/models'
|
||||
import { BlockStatus } from '@shared/models'
|
||||
import { asyncMiddleware, blocklistStatusValidator, optionalAuthenticate } from '../../middlewares'
|
||||
import { logger } from '@server/helpers/logger'
|
||||
|
||||
const blocklistRouter = express.Router()
|
||||
|
||||
blocklistRouter.get('/status',
|
||||
optionalAuthenticate,
|
||||
blocklistStatusValidator,
|
||||
asyncMiddleware(getBlocklistStatus)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
blocklistRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getBlocklistStatus (req: express.Request, res: express.Response) {
|
||||
const hosts = req.query.hosts as string[]
|
||||
const accounts = req.query.accounts as string[]
|
||||
const user = res.locals.oauth?.token.User
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const byAccountIds = [ serverActor.Account.id ]
|
||||
if (user) byAccountIds.push(user.Account.id)
|
||||
|
||||
const status: BlockStatus = {
|
||||
accounts: {},
|
||||
hosts: {}
|
||||
}
|
||||
|
||||
const baseOptions = {
|
||||
byAccountIds,
|
||||
user,
|
||||
serverActor,
|
||||
status
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
populateServerBlocklistStatus({ ...baseOptions, hosts }),
|
||||
populateAccountBlocklistStatus({ ...baseOptions, accounts })
|
||||
])
|
||||
|
||||
return res.json(status)
|
||||
}
|
||||
|
||||
async function populateServerBlocklistStatus (options: {
|
||||
byAccountIds: number[]
|
||||
user?: MUserAccountId
|
||||
serverActor: MActorAccountId
|
||||
hosts: string[]
|
||||
status: BlockStatus
|
||||
}) {
|
||||
const { byAccountIds, user, serverActor, hosts, status } = options
|
||||
|
||||
if (!hosts || hosts.length === 0) return
|
||||
|
||||
const serverBlocklistStatus = await ServerBlocklistModel.getBlockStatus(byAccountIds, hosts)
|
||||
|
||||
logger.debug('Got server blocklist status.', { serverBlocklistStatus, byAccountIds, hosts })
|
||||
|
||||
for (const host of hosts) {
|
||||
const block = serverBlocklistStatus.find(b => b.host === host)
|
||||
|
||||
status.hosts[host] = getStatus(block, serverActor, user)
|
||||
}
|
||||
}
|
||||
|
||||
async function populateAccountBlocklistStatus (options: {
|
||||
byAccountIds: number[]
|
||||
user?: MUserAccountId
|
||||
serverActor: MActorAccountId
|
||||
accounts: string[]
|
||||
status: BlockStatus
|
||||
}) {
|
||||
const { byAccountIds, user, serverActor, accounts, status } = options
|
||||
|
||||
if (!accounts || accounts.length === 0) return
|
||||
|
||||
const accountBlocklistStatus = await AccountBlocklistModel.getBlockStatus(byAccountIds, accounts)
|
||||
|
||||
logger.debug('Got account blocklist status.', { accountBlocklistStatus, byAccountIds, accounts })
|
||||
|
||||
for (const account of accounts) {
|
||||
const sanitizedHandle = handleToNameAndHost(account)
|
||||
|
||||
const block = accountBlocklistStatus.find(b => b.name === sanitizedHandle.name && b.host === sanitizedHandle.host)
|
||||
|
||||
status.accounts[sanitizedHandle.handle] = getStatus(block, serverActor, user)
|
||||
}
|
||||
}
|
||||
|
||||
function getStatus (block: { accountId: number }, serverActor: MActorAccountId, user?: MUserAccountId) {
|
||||
return {
|
||||
blockedByServer: !!(block && block.accountId === serverActor.Account.id),
|
||||
blockedByUser: !!(block && user && block.accountId === user.Account.id)
|
||||
}
|
||||
}
|
|
@ -169,6 +169,18 @@ function customConfig (): CustomConfig {
|
|||
whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: CONFIG.CACHE.PREVIEWS.SIZE
|
||||
|
|
|
@ -6,6 +6,7 @@ import { badRequest } from '../../helpers/express-utils'
|
|||
import { CONFIG } from '../../initializers/config'
|
||||
import { abuseRouter } from './abuse'
|
||||
import { accountsRouter } from './accounts'
|
||||
import { blocklistRouter } from './blocklist'
|
||||
import { bulkRouter } from './bulk'
|
||||
import { configRouter } from './config'
|
||||
import { customPageRouter } from './custom-page'
|
||||
|
@ -49,6 +50,7 @@ apiRouter.use('/search', searchRouter)
|
|||
apiRouter.use('/overviews', overviewsRouter)
|
||||
apiRouter.use('/plugins', pluginRouter)
|
||||
apiRouter.use('/custom-pages', customPageRouter)
|
||||
apiRouter.use('/blocklist', blocklistRouter)
|
||||
apiRouter.use('/ping', pong)
|
||||
apiRouter.use('/*', badRequest)
|
||||
|
||||
|
|
|
@ -144,8 +144,13 @@ async function installPlugin (req: express.Request, res: express.Response) {
|
|||
|
||||
const fromDisk = !!body.path
|
||||
const toInstall = body.npmName || body.path
|
||||
|
||||
const pluginVersion = body.pluginVersion && body.npmName
|
||||
? body.pluginVersion
|
||||
: undefined
|
||||
|
||||
try {
|
||||
const plugin = await PluginManager.Instance.install(toInstall, undefined, fromDisk)
|
||||
const plugin = await PluginManager.Instance.install(toInstall, pluginVersion, fromDisk)
|
||||
|
||||
return res.json(plugin.toFormattedJSON())
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'multer'
|
||||
import express from 'express'
|
||||
import { handlesToNameAndHost } from '@server/helpers/actors'
|
||||
import { pickCommonVideoQuery } from '@server/helpers/query'
|
||||
import { sendUndoFollow } from '@server/lib/activitypub/send'
|
||||
import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
|
||||
|
@ -7,7 +8,6 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
|
|||
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
|
||||
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
|
||||
import { getFormattedObjects } from '../../../helpers/utils'
|
||||
import { WEBSERVER } from '../../../initializers/constants'
|
||||
import { sequelizeTypescript } from '../../../initializers/database'
|
||||
import { JobQueue } from '../../../lib/job-queue'
|
||||
import {
|
||||
|
@ -89,28 +89,23 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
|
|||
const uris = req.query.uris as string[]
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const handles = uris.map(u => {
|
||||
let [ name, host ] = u.split('@')
|
||||
if (host === WEBSERVER.HOST) host = null
|
||||
const sanitizedHandles = handlesToNameAndHost(uris)
|
||||
|
||||
return { name, host, uri: u }
|
||||
})
|
||||
|
||||
const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, handles)
|
||||
const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles)
|
||||
|
||||
const existObject: { [id: string ]: boolean } = {}
|
||||
for (const handle of handles) {
|
||||
for (const sanitizedHandle of sanitizedHandles) {
|
||||
const obj = results.find(r => {
|
||||
const server = r.ActorFollowing.Server
|
||||
|
||||
return r.ActorFollowing.preferredUsername === handle.name &&
|
||||
return r.ActorFollowing.preferredUsername === sanitizedHandle.name &&
|
||||
(
|
||||
(!server && !handle.host) ||
|
||||
(server.host === handle.host)
|
||||
(!server && !sanitizedHandle.host) ||
|
||||
(server.host === sanitizedHandle.host)
|
||||
)
|
||||
})
|
||||
|
||||
existObject[handle.uri] = obj !== undefined
|
||||
existObject[sanitizedHandle.handle] = obj !== undefined
|
||||
}
|
||||
|
||||
return res.json(existObject)
|
||||
|
|
|
@ -8,6 +8,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
|||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
||||
import { generateWebTorrentVideoFilename } from '@server/lib/paths'
|
||||
import { Redis } from '@server/lib/redis'
|
||||
import { uploadx } from '@server/lib/uploadx'
|
||||
import {
|
||||
addMoveToObjectStorageJob,
|
||||
addOptimizeOrMergeAudioJob,
|
||||
|
@ -19,7 +20,6 @@ import { VideoPathManager } from '@server/lib/video-path-manager'
|
|||
import { buildNextVideoState } from '@server/lib/video-state'
|
||||
import { openapiOperationDoc } from '@server/middlewares/doc'
|
||||
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { Uploadx } from '@uploadx/core'
|
||||
import { VideoCreate, VideoState } from '../../../../shared'
|
||||
import { HttpStatusCode } from '../../../../shared/models'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
|
||||
|
@ -41,8 +41,8 @@ import {
|
|||
authenticate,
|
||||
videosAddLegacyValidator,
|
||||
videosAddResumableInitValidator,
|
||||
videosResumableUploadIdValidator,
|
||||
videosAddResumableValidator
|
||||
videosAddResumableValidator,
|
||||
videosResumableUploadIdValidator
|
||||
} from '../../../middlewares'
|
||||
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
|
@ -52,9 +52,6 @@ const lTags = loggerTagsFactory('api', 'video')
|
|||
const auditLogger = auditLoggerFactory('videos')
|
||||
const uploadRouter = express.Router()
|
||||
|
||||
const uploadx = new Uploadx({ directory: getResumableUploadPath() })
|
||||
uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
|
||||
|
||||
const reqVideoFileAdd = createReqFiles(
|
||||
[ 'videofile', 'thumbnailfile', 'previewfile' ],
|
||||
Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
|
||||
|
|
|
@ -98,7 +98,7 @@ function buildOEmbed (options: {
|
|||
thumbnailUrl = undefined
|
||||
}
|
||||
|
||||
const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` +
|
||||
const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts allow-popups" ` +
|
||||
`title="${embedTitle}" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`
|
||||
|
||||
const json: any = {
|
||||
|
|
17
server/helpers/actors.ts
Normal file
17
server/helpers/actors.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { WEBSERVER } from '@server/initializers/constants'
|
||||
|
||||
function handleToNameAndHost (handle: string) {
|
||||
let [ name, host ] = handle.split('@')
|
||||
if (host === WEBSERVER.HOST) host = null
|
||||
|
||||
return { name, host, handle }
|
||||
}
|
||||
|
||||
function handlesToNameAndHost (handles: string[]) {
|
||||
return handles.map(h => handleToNameAndHost(h))
|
||||
}
|
||||
|
||||
export {
|
||||
handleToNameAndHost,
|
||||
handlesToNameAndHost
|
||||
}
|
78
server/helpers/decache.ts
Normal file
78
server/helpers/decache.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Thanks: https://github.com/dwyl/decache
|
||||
// We reuse this file to also uncache plugin base path
|
||||
|
||||
import { extname } from 'path'
|
||||
|
||||
function decachePlugin (pluginPath: string, libraryPath: string) {
|
||||
const moduleName = find(libraryPath)
|
||||
|
||||
if (!moduleName) return
|
||||
|
||||
searchCache(moduleName, function (mod) {
|
||||
delete require.cache[mod.id]
|
||||
})
|
||||
|
||||
removeCachedPath(pluginPath)
|
||||
}
|
||||
|
||||
function decacheModule (name: string) {
|
||||
const moduleName = find(name)
|
||||
|
||||
if (!moduleName) return
|
||||
|
||||
searchCache(moduleName, function (mod) {
|
||||
delete require.cache[mod.id]
|
||||
})
|
||||
|
||||
removeCachedPath(moduleName)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
decacheModule,
|
||||
decachePlugin
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function find (moduleName: string) {
|
||||
try {
|
||||
return require.resolve(moduleName)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function searchCache (moduleName: string, callback: (current: NodeModule) => void) {
|
||||
const resolvedModule = require.resolve(moduleName)
|
||||
let mod: NodeModule
|
||||
const visited = {}
|
||||
|
||||
if (resolvedModule && ((mod = require.cache[resolvedModule]) !== undefined)) {
|
||||
// Recursively go over the results
|
||||
(function run (current) {
|
||||
visited[current.id] = true
|
||||
|
||||
current.children.forEach(function (child) {
|
||||
if (extname(child.filename) !== '.node' && !visited[child.id]) {
|
||||
run(child)
|
||||
}
|
||||
})
|
||||
|
||||
// Call the specified callback providing the
|
||||
// found module
|
||||
callback(current)
|
||||
})(mod)
|
||||
}
|
||||
};
|
||||
|
||||
function removeCachedPath (pluginPath: string) {
|
||||
const pathCache = (module.constructor as any)._pathCache
|
||||
|
||||
Object.keys(pathCache).forEach(function (cacheKey) {
|
||||
if (cacheKey.includes(pluginPath)) {
|
||||
delete pathCache[cacheKey]
|
||||
}
|
||||
})
|
||||
}
|
|
@ -33,6 +33,7 @@ function checkMissedConfig () {
|
|||
'transcoding.resolutions.2160p',
|
||||
'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled',
|
||||
'trending.videos.interval_days',
|
||||
'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth',
|
||||
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
|
||||
'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
|
||||
'services.twitter.username', 'services.twitter.whitelisted',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import bytes from 'bytes'
|
||||
import { IConfig } from 'config'
|
||||
import decache from 'decache'
|
||||
import { dirname, join } from 'path'
|
||||
import { decacheModule } from '@server/helpers/decache'
|
||||
import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
|
||||
import { BroadcastMessageLevel } from '@shared/models/server'
|
||||
import { VideosRedundancyStrategy } from '../../shared/models'
|
||||
|
@ -63,6 +63,11 @@ const CONFIG = {
|
|||
MINIATURE: {
|
||||
get PREFER_AUTHOR_DISPLAY_NAME () { return config.get<boolean>('client.videos.miniature.prefer_author_display_name') }
|
||||
}
|
||||
},
|
||||
MENU: {
|
||||
LOGIN: {
|
||||
get REDIRECT_ON_SINGLE_EXTERNAL_AUTH () { return config.get<boolean>('client.menu.login.redirect_on_single_external_auth') }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -497,7 +502,7 @@ export function reloadConfig () {
|
|||
delete require.cache[fileName]
|
||||
}
|
||||
|
||||
decache('config')
|
||||
decacheModule('config')
|
||||
}
|
||||
|
||||
purge()
|
||||
|
|
|
@ -223,7 +223,7 @@ const SCHEDULER_INTERVALS_MS = {
|
|||
REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day
|
||||
REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day
|
||||
UPDATE_INBOX_STATS: 1000 * 60, // 1 minute
|
||||
REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 * 16 // 16 hours
|
||||
REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 // 1 hour
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -40,12 +40,12 @@ async function isBlockedByServerOrAccount (targetAccount: MAccountServer, userAc
|
|||
|
||||
if (userAccount) sourceAccounts.push(userAccount.id)
|
||||
|
||||
const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, targetAccount.id)
|
||||
const accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, targetAccount.id)
|
||||
if (accountMutedHash[serverAccountId] || (userAccount && accountMutedHash[userAccount.id])) {
|
||||
return true
|
||||
}
|
||||
|
||||
const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, targetAccount.Actor.serverId)
|
||||
const instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, targetAccount.Actor.serverId)
|
||||
if (instanceMutedHash[serverAccountId] || (userAccount && instanceMutedHash[userAccount.id])) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -350,10 +350,6 @@ class ClientHtml {
|
|||
return join(__dirname, '../../../client/dist/standalone/videos/embed.html')
|
||||
}
|
||||
|
||||
private static addHtmlLang (htmlStringPage: string, paramLang: string) {
|
||||
return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`)
|
||||
}
|
||||
|
||||
private static addManifestContentHash (htmlStringPage: string) {
|
||||
return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST)
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ async function processVideoTranscoding (job: Job) {
|
|||
|
||||
if (!handler) {
|
||||
await moveToFailedTranscodingState(video)
|
||||
await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
|
||||
|
||||
throw new Error('Cannot find transcoding handler for ' + payload.type)
|
||||
}
|
||||
|
@ -62,12 +63,20 @@ async function processVideoTranscoding (job: Job) {
|
|||
} catch (error) {
|
||||
await moveToFailedTranscodingState(video)
|
||||
|
||||
await VideoJobInfoModel.decrease(video.uuid, 'pendingTranscode')
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
processVideoTranscoding
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Job handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -119,7 +128,7 @@ async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTrans
|
|||
|
||||
logger.info('Merge audio transcoding job for %s ended.', video.uuid, lTags(video.uuid))
|
||||
|
||||
await onVideoFileOptimizer(video, payload, 'video', user)
|
||||
await onVideoFirstWebTorrentTranscoding(video, payload, 'video', user)
|
||||
}
|
||||
|
||||
async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) {
|
||||
|
@ -129,7 +138,7 @@ async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodi
|
|||
|
||||
logger.info('Optimize transcoding job for %s ended.', video.uuid, lTags(video.uuid))
|
||||
|
||||
await onVideoFileOptimizer(video, payload, transcodeType, user)
|
||||
await onVideoFirstWebTorrentTranscoding(video, payload, transcodeType, user)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -159,7 +168,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
|
|||
await retryTransactionWrapper(moveToNextState, video, payload.isNewVideo)
|
||||
}
|
||||
|
||||
async function onVideoFileOptimizer (
|
||||
async function onVideoFirstWebTorrentTranscoding (
|
||||
videoArg: MVideoWithFile,
|
||||
payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload,
|
||||
transcodeType: TranscodeOptionsType,
|
||||
|
@ -211,6 +220,8 @@ async function onNewWebTorrentFileResolution (
|
|||
await retryTransactionWrapper(moveToNextState, video, payload.isNewVideo)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function createHlsJobIfEnabled (user: MUserId, payload: {
|
||||
videoUUID: string
|
||||
resolution: number
|
||||
|
@ -241,16 +252,6 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
|
|||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
processVideoTranscoding,
|
||||
createHlsJobIfEnabled,
|
||||
onNewWebTorrentFileResolution
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function createLowerResolutionsJobs (options: {
|
||||
video: MVideoFullLight
|
||||
user: MUserId
|
||||
|
|
|
@ -135,13 +135,20 @@ class MuxingSession extends EventEmitter {
|
|||
})
|
||||
: getLiveMuxingCommand(this.inputUrl, outPath, this.streamingPlaylist.playlistFilename)
|
||||
|
||||
logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags)
|
||||
logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags())
|
||||
|
||||
this.watchTSFiles(outPath)
|
||||
this.watchMasterFile(outPath)
|
||||
|
||||
let ffmpegShellCommand: string
|
||||
this.ffmpegCommand.on('start', cmdline => {
|
||||
ffmpegShellCommand = cmdline
|
||||
|
||||
logger.debug('Running ffmpeg command for live', { ffmpegShellCommand, ...this.lTags() })
|
||||
})
|
||||
|
||||
this.ffmpegCommand.on('error', (err, stdout, stderr) => {
|
||||
this.onFFmpegError(err, stdout, stderr, outPath)
|
||||
this.onFFmpegError({ err, stdout, stderr, outPath, ffmpegShellCommand })
|
||||
})
|
||||
|
||||
this.ffmpegCommand.on('end', () => this.onFFmpegEnded(outPath))
|
||||
|
@ -161,19 +168,27 @@ class MuxingSession extends EventEmitter {
|
|||
this.hasClientSocketInBadHealthWithCache.clear()
|
||||
}
|
||||
|
||||
private onFFmpegError (err: any, stdout: string, stderr: string, outPath: string) {
|
||||
private onFFmpegError (options: {
|
||||
err: any
|
||||
stdout: string
|
||||
stderr: string
|
||||
outPath: string
|
||||
ffmpegShellCommand: string
|
||||
}) {
|
||||
const { err, stdout, stderr, outPath, ffmpegShellCommand } = options
|
||||
|
||||
this.onFFmpegEnded(outPath)
|
||||
|
||||
// Don't care that we killed the ffmpeg process
|
||||
if (err?.message?.includes('Exiting normally')) return
|
||||
|
||||
logger.error('Live transcoding error.', { err, stdout, stderr, ...this.lTags })
|
||||
logger.error('Live transcoding error.', { err, stdout, stderr, ffmpegShellCommand, ...this.lTags() })
|
||||
|
||||
this.emit('ffmpeg-error', ({ sessionId: this.sessionId }))
|
||||
}
|
||||
|
||||
private onFFmpegEnded (outPath: string) {
|
||||
logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.inputUrl, this.lTags)
|
||||
logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', this.inputUrl, this.lTags())
|
||||
|
||||
setTimeout(() => {
|
||||
// Wait latest segments generation, and close watchers
|
||||
|
@ -188,7 +203,7 @@ class MuxingSession extends EventEmitter {
|
|||
.catch(err => {
|
||||
logger.error(
|
||||
'Cannot close watchers of %s or process remaining hash segments.', outPath,
|
||||
{ err, ...this.lTags }
|
||||
{ err, ...this.lTags() }
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -203,7 +218,7 @@ class MuxingSession extends EventEmitter {
|
|||
this.emit('master-playlist-created', { videoId: this.videoId })
|
||||
|
||||
this.masterWatcher.close()
|
||||
.catch(err => logger.error('Cannot close master watcher of %s.', outPath, { err, ...this.lTags }))
|
||||
.catch(err => logger.error('Cannot close master watcher of %s.', outPath, { err, ...this.lTags() }))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -215,7 +230,7 @@ class MuxingSession extends EventEmitter {
|
|||
const playlistIdMatcher = /^([\d+])-/
|
||||
|
||||
const addHandler = async segmentPath => {
|
||||
logger.debug('Live add handler of %s.', segmentPath, this.lTags)
|
||||
logger.debug('Live add handler of %s.', segmentPath, this.lTags())
|
||||
|
||||
const playlistId = basename(segmentPath).match(playlistIdMatcher)[0]
|
||||
|
||||
|
@ -259,7 +274,7 @@ class MuxingSession extends EventEmitter {
|
|||
|
||||
return canUpload !== true
|
||||
} catch (err) {
|
||||
logger.error('Cannot stat %s or check quota of %d.', segmentPath, this.user.id, { err, ...this.lTags })
|
||||
logger.error('Cannot stat %s or check quota of %d.', segmentPath, this.user.id, { err, ...this.lTags() })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,7 +292,7 @@ class MuxingSession extends EventEmitter {
|
|||
})
|
||||
|
||||
VideoFileModel.customUpsert(file, 'streaming-playlist', null)
|
||||
.catch(err => logger.error('Cannot create file for live streaming.', { err, ...this.lTags }))
|
||||
.catch(err => logger.error('Cannot create file for live streaming.', { err, ...this.lTags() }))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,14 +328,14 @@ class MuxingSession extends EventEmitter {
|
|||
if (this.saveReplay) {
|
||||
await this.addSegmentToReplay(hlsVideoPath, previousSegment)
|
||||
}
|
||||
}).catch(err => logger.error('Cannot process segments in %s', hlsVideoPath, { err, ...this.lTags }))
|
||||
}).catch(err => logger.error('Cannot process segments in %s', hlsVideoPath, { err, ...this.lTags() }))
|
||||
}
|
||||
|
||||
private hasClientSocketInBadHealth (sessionId: string) {
|
||||
const rtmpSession = this.context.sessions.get(sessionId)
|
||||
|
||||
if (!rtmpSession) {
|
||||
logger.warn('Cannot get session %s to check players socket health.', sessionId, this.lTags)
|
||||
logger.warn('Cannot get session %s to check players socket health.', sessionId, this.lTags())
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -328,7 +343,7 @@ class MuxingSession extends EventEmitter {
|
|||
const playerSession = this.context.sessions.get(playerSessionId)
|
||||
|
||||
if (!playerSession) {
|
||||
logger.error('Cannot get player session %s to check socket health.', playerSession, this.lTags)
|
||||
logger.error('Cannot get player session %s to check socket health.', playerSession, this.lTags())
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -349,7 +364,7 @@ class MuxingSession extends EventEmitter {
|
|||
|
||||
await appendFile(dest, data)
|
||||
} catch (err) {
|
||||
logger.error('Cannot copy segment %s to replay directory.', segmentPath, { err, ...this.lTags })
|
||||
logger.error('Cannot copy segment %s to replay directory.', segmentPath, { err, ...this.lTags() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,8 +47,8 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU
|
|||
|
||||
const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ])
|
||||
|
||||
this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, this.payload.accountId)
|
||||
this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, this.payload.Account.Actor.serverId)
|
||||
this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByAccounts(sourceAccounts, this.payload.accountId)
|
||||
this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByAccounts(sourceAccounts, this.payload.Account.Actor.serverId)
|
||||
}
|
||||
|
||||
log () {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import decache from 'decache'
|
||||
import express from 'express'
|
||||
import { createReadStream, createWriteStream } from 'fs'
|
||||
import { ensureDir, outputFile, readJSON } from 'fs-extra'
|
||||
import { basename, join } from 'path'
|
||||
import { decachePlugin } from '@server/helpers/decache'
|
||||
import { MOAuthTokenUser, MUser } from '@server/types/models'
|
||||
import { getCompleteLocale } from '@shared/core-utils'
|
||||
import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models'
|
||||
|
@ -420,7 +420,7 @@ export class PluginManager implements ServerHook {
|
|||
|
||||
// Delete cache if needed
|
||||
const modulePath = join(pluginPath, packageJSON.library)
|
||||
decache(modulePath)
|
||||
decachePlugin(pluginPath, modulePath)
|
||||
const library: PluginLibrary = require(modulePath)
|
||||
|
||||
if (!isLibraryCodeValid(library)) {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { map } from 'bluebird'
|
||||
import { readdir, remove, stat } from 'fs-extra'
|
||||
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||
import { getResumableUploadPath } from '@server/helpers/upload'
|
||||
import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants'
|
||||
import { METAFILE_EXTNAME } from '@uploadx/core'
|
||||
import { uploadx } from '../uploadx'
|
||||
import { AbstractScheduler } from './abstract-scheduler'
|
||||
|
||||
const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner')
|
||||
|
@ -22,36 +20,17 @@ export class RemoveDanglingResumableUploadsScheduler extends AbstractScheduler {
|
|||
}
|
||||
|
||||
protected async internalExecute () {
|
||||
const path = getResumableUploadPath()
|
||||
const files = await readdir(path)
|
||||
logger.debug('Removing dangling resumable uploads', lTags())
|
||||
|
||||
const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME))
|
||||
|
||||
if (metafiles.length === 0) return
|
||||
|
||||
logger.debug('Reading resumable video upload folder %s with %d files', path, metafiles.length, lTags())
|
||||
const now = new Date().getTime()
|
||||
|
||||
try {
|
||||
await map(metafiles, metafile => {
|
||||
return this.deleteIfOlderThan(metafile, this.lastExecutionTimeMs)
|
||||
}, { concurrency: 5 })
|
||||
// Remove files that were not updated since the last execution
|
||||
await uploadx.storage.purge(now - this.lastExecutionTimeMs)
|
||||
} catch (error) {
|
||||
logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() })
|
||||
} finally {
|
||||
this.lastExecutionTimeMs = new Date().getTime()
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteIfOlderThan (metafile: string, olderThan: number) {
|
||||
const metafilePath = getResumableUploadPath(metafile)
|
||||
const statResult = await stat(metafilePath)
|
||||
|
||||
// Delete uploads that started since a long time
|
||||
if (statResult.ctimeMs < olderThan) {
|
||||
await remove(metafilePath)
|
||||
|
||||
const datafile = metafilePath.replace(new RegExp(`${METAFILE_EXTNAME}$`), '')
|
||||
await remove(datafile)
|
||||
this.lastExecutionTimeMs = now
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,11 @@ class ServerConfigManager {
|
|||
miniature: {
|
||||
preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
10
server/lib/uploadx.ts
Normal file
10
server/lib/uploadx.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import express from 'express'
|
||||
import { getResumableUploadPath } from '@server/helpers/upload'
|
||||
import { Uploadx } from '@uploadx/core'
|
||||
|
||||
const uploadx = new Uploadx({ directory: getResumableUploadPath() })
|
||||
uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
|
||||
|
||||
export {
|
||||
uploadx
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor'
|
||||
import { toArray } from '@server/helpers/custom-validators/misc'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
|
||||
import { isHostValid } from '../../helpers/custom-validators/servers'
|
||||
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { WEBSERVER } from '../../initializers/constants'
|
||||
import { AccountBlocklistModel } from '../../models/account/account-blocklist'
|
||||
|
@ -123,6 +125,26 @@ const unblockServerByServerValidator = [
|
|||
}
|
||||
]
|
||||
|
||||
const blocklistStatusValidator = [
|
||||
query('hosts')
|
||||
.optional()
|
||||
.customSanitizer(toArray)
|
||||
.custom(isEachUniqueHostValid).withMessage('Should have a valid hosts array'),
|
||||
|
||||
query('accounts')
|
||||
.optional()
|
||||
.customSanitizer(toArray)
|
||||
.custom(areValidActorHandles).withMessage('Should have a valid accounts array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking blocklistStatusValidator parameters', { query: req.query })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
|
@ -131,7 +153,8 @@ export {
|
|||
unblockAccountByAccountValidator,
|
||||
unblockServerByAccountValidator,
|
||||
unblockAccountByServerValidator,
|
||||
unblockServerByServerValidator
|
||||
unblockServerByServerValidator,
|
||||
blocklistStatusValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -116,6 +116,9 @@ const installOrUpdatePluginValidator = [
|
|||
body('npmName')
|
||||
.optional()
|
||||
.custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
|
||||
body('pluginVersion')
|
||||
.optional()
|
||||
.custom(isPluginVersionValid).withMessage('Should have a valid plugin version'),
|
||||
body('path')
|
||||
.optional()
|
||||
.custom(isSafePath).withMessage('Should have a valid safe path'),
|
||||
|
@ -129,6 +132,9 @@ const installOrUpdatePluginValidator = [
|
|||
if (!body.path && !body.npmName) {
|
||||
return res.fail({ message: 'Should have either a npmName or a path' })
|
||||
}
|
||||
if (body.pluginVersion && !body.npmName) {
|
||||
return res.fail({ message: 'Should have a npmName when specifying a pluginVersion' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
|
|
@ -211,7 +211,7 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
|
|||
const videoFileMetadata = {
|
||||
mimetype: req.headers['x-upload-content-type'] as string,
|
||||
size: +req.headers['x-upload-content-length'],
|
||||
originalname: req.body.name
|
||||
originalname: req.body.filename
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Op } from 'sequelize'
|
||||
import { Op, QueryTypes } from 'sequelize'
|
||||
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { handlesToNameAndHost } from '@server/helpers/actors'
|
||||
import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/core-utils'
|
||||
import { AccountBlock } from '../../../shared/models'
|
||||
import { ActorModel } from '../actor/actor'
|
||||
import { ServerModel } from '../server/server'
|
||||
import { getSort, searchAttribute } from '../utils'
|
||||
import { createSafeIn, getSort, searchAttribute } from '../utils'
|
||||
import { AccountModel } from './account'
|
||||
|
||||
enum ScopeNames {
|
||||
|
@ -77,7 +78,7 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
|
|||
})
|
||||
BlockedAccount: AccountModel
|
||||
|
||||
static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) {
|
||||
static isAccountMutedByAccounts (accountIds: number[], targetAccountId: number) {
|
||||
const query = {
|
||||
attributes: [ 'accountId', 'id' ],
|
||||
where: {
|
||||
|
@ -187,6 +188,39 @@ export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountB
|
|||
.then(entries => entries.map(e => `${e.BlockedAccount.Actor.preferredUsername}@${e.BlockedAccount.Actor.Server.host}`))
|
||||
}
|
||||
|
||||
static getBlockStatus (byAccountIds: number[], handles: string[]): Promise<{ name: string, host: string, accountId: number }[]> {
|
||||
const sanitizedHandles = handlesToNameAndHost(handles)
|
||||
|
||||
const localHandles = sanitizedHandles.filter(h => !h.host)
|
||||
.map(h => h.name)
|
||||
|
||||
const remoteHandles = sanitizedHandles.filter(h => !!h.host)
|
||||
.map(h => ([ h.name, h.host ]))
|
||||
|
||||
const handlesWhere: string[] = []
|
||||
|
||||
if (localHandles.length !== 0) {
|
||||
handlesWhere.push(`("actor"."preferredUsername" IN (:localHandles) AND "server"."id" IS NULL)`)
|
||||
}
|
||||
|
||||
if (remoteHandles.length !== 0) {
|
||||
handlesWhere.push(`(("actor"."preferredUsername", "server"."host") IN (:remoteHandles))`)
|
||||
}
|
||||
|
||||
const rawQuery = `SELECT "accountBlocklist"."accountId", "actor"."preferredUsername" AS "name", "server"."host" ` +
|
||||
`FROM "accountBlocklist" ` +
|
||||
`INNER JOIN "account" ON "account"."id" = "accountBlocklist"."targetAccountId" ` +
|
||||
`INNER JOIN "actor" ON "actor"."id" = "account"."actorId" ` +
|
||||
`LEFT JOIN "server" ON "server"."id" = "actor"."serverId" ` +
|
||||
`WHERE "accountBlocklist"."accountId" IN (${createSafeIn(AccountBlocklistModel.sequelize, byAccountIds)}) ` +
|
||||
`AND (${handlesWhere.join(' OR ')})`
|
||||
|
||||
return AccountBlocklistModel.sequelize.query(rawQuery, {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
replacements: { byAccountIds, localHandles, remoteHandles }
|
||||
})
|
||||
}
|
||||
|
||||
toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock {
|
||||
return {
|
||||
byAccount: this.ByAccount.toFormattedJSON(),
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Op } from 'sequelize'
|
||||
import { Op, QueryTypes } from 'sequelize'
|
||||
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
|
||||
import { AttributesOnly } from '@shared/core-utils'
|
||||
import { ServerBlock } from '@shared/models'
|
||||
import { AccountModel } from '../account/account'
|
||||
import { getSort, searchAttribute } from '../utils'
|
||||
import { createSafeIn, getSort, searchAttribute } from '../utils'
|
||||
import { ServerModel } from './server'
|
||||
|
||||
enum ScopeNames {
|
||||
|
@ -76,7 +76,7 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
|
|||
})
|
||||
BlockedServer: ServerModel
|
||||
|
||||
static isServerMutedByMulti (accountIds: number[], targetServerId: number) {
|
||||
static isServerMutedByAccounts (accountIds: number[], targetServerId: number) {
|
||||
const query = {
|
||||
attributes: [ 'accountId', 'id' ],
|
||||
where: {
|
||||
|
@ -141,6 +141,19 @@ export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlo
|
|||
.then(entries => entries.map(e => e.BlockedServer.host))
|
||||
}
|
||||
|
||||
static getBlockStatus (byAccountIds: number[], hosts: string[]): Promise<{ host: string, accountId: number }[]> {
|
||||
const rawQuery = `SELECT "server"."host", "serverBlocklist"."accountId" ` +
|
||||
`FROM "serverBlocklist" ` +
|
||||
`INNER JOIN "server" ON "server"."id" = "serverBlocklist"."targetServerId" ` +
|
||||
`WHERE "server"."host" IN (:hosts) ` +
|
||||
`AND "serverBlocklist"."accountId" IN (${createSafeIn(ServerBlocklistModel.sequelize, byAccountIds)})`
|
||||
|
||||
return ServerBlocklistModel.sequelize.query(rawQuery, {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
replacements: { hosts }
|
||||
})
|
||||
}
|
||||
|
||||
static listForApi (parameters: {
|
||||
start: number
|
||||
count: number
|
||||
|
|
|
@ -3,6 +3,8 @@ import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, IsInt, Mo
|
|||
import { AttributesOnly } from '@shared/core-utils'
|
||||
import { VideoModel } from './video'
|
||||
|
||||
export type VideoJobInfoColumnType = 'pendingMove' | 'pendingTranscode'
|
||||
|
||||
@Table({
|
||||
tableName: 'videoJobInfo',
|
||||
indexes: [
|
||||
|
@ -57,7 +59,7 @@ export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfo
|
|||
return VideoJobInfoModel.findOne({ where, transaction })
|
||||
}
|
||||
|
||||
static async increaseOrCreate (videoUUID: string, column: 'pendingMove' | 'pendingTranscode'): Promise<number> {
|
||||
static async increaseOrCreate (videoUUID: string, column: VideoJobInfoColumnType): Promise<number> {
|
||||
const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } }
|
||||
|
||||
const [ { pendingMove } ] = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(`
|
||||
|
@ -79,7 +81,7 @@ export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfo
|
|||
return pendingMove
|
||||
}
|
||||
|
||||
static async decrease (videoUUID: string, column: 'pendingMove' | 'pendingTranscode'): Promise<number> {
|
||||
static async decrease (videoUUID: string, column: VideoJobInfoColumnType): Promise<number> {
|
||||
const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } }
|
||||
|
||||
const [ { pendingMove } ] = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(`
|
||||
|
|
|
@ -481,6 +481,78 @@ describe('Test blocklist API validators', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When getting blocklist status', function () {
|
||||
const path = '/api/v1/blocklist/status'
|
||||
|
||||
it('Should fail with a bad token', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: 'false',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad accounts field', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
accounts: 1
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
accounts: [ 1 ]
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad hosts field', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
hosts: 1
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
hosts: [ 1 ]
|
||||
},
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {},
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: {
|
||||
hosts: [ 'example.com' ],
|
||||
accounts: [ 'john@example.com' ]
|
||||
},
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
|
|
|
@ -54,6 +54,18 @@ describe('Test config API validators', function () {
|
|||
whitelisted: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: false
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: false
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: 2
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('Test server plugins API validators', function () {
|
|||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
this.timeout(60000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
|
|
|
@ -254,6 +254,45 @@ describe('Test blocklist', function () {
|
|||
}
|
||||
})
|
||||
|
||||
it('Should get blocked status', async function () {
|
||||
const remoteHandle = 'user2@' + servers[1].host
|
||||
const localHandle = 'user1@' + servers[0].host
|
||||
const unknownHandle = 'user5@' + servers[0].host
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ accounts: [ remoteHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(1)
|
||||
expect(status.accounts[remoteHandle].blockedByUser).to.be.false
|
||||
expect(status.accounts[remoteHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(1)
|
||||
expect(status.accounts[remoteHandle].blockedByUser).to.be.true
|
||||
expect(status.accounts[remoteHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(3)
|
||||
|
||||
for (const handle of [ localHandle, remoteHandle ]) {
|
||||
expect(status.accounts[handle].blockedByUser).to.be.true
|
||||
expect(status.accounts[handle].blockedByServer).to.be.false
|
||||
}
|
||||
|
||||
expect(status.accounts[unknownHandle].blockedByUser).to.be.false
|
||||
expect(status.accounts[unknownHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not allow a remote blocked user to comment my videos', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
|
@ -434,6 +473,35 @@ describe('Test blocklist', function () {
|
|||
expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
|
||||
})
|
||||
|
||||
it('Should get blocklist status', async function () {
|
||||
const blockedServer = servers[1].host
|
||||
const notBlockedServer = 'example.com'
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(0)
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(2)
|
||||
expect(status.hosts[blockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[blockedServer].blockedByServer).to.be.false
|
||||
|
||||
expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
|
||||
}
|
||||
|
||||
{
|
||||
const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(0)
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(2)
|
||||
expect(status.hosts[blockedServer].blockedByUser).to.be.true
|
||||
expect(status.hosts[blockedServer].blockedByServer).to.be.false
|
||||
|
||||
expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the remote server', async function () {
|
||||
await command.removeFromMyBlocklist({ server: 'localhost:' + servers[1].port })
|
||||
})
|
||||
|
@ -575,6 +643,27 @@ describe('Test blocklist', function () {
|
|||
}
|
||||
})
|
||||
|
||||
it('Should get blocked status', async function () {
|
||||
const remoteHandle = 'user2@' + servers[1].host
|
||||
const localHandle = 'user1@' + servers[0].host
|
||||
const unknownHandle = 'user5@' + servers[0].host
|
||||
|
||||
for (const token of [ undefined, servers[0].accessToken ]) {
|
||||
const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(3)
|
||||
|
||||
for (const handle of [ localHandle, remoteHandle ]) {
|
||||
expect(status.accounts[handle].blockedByUser).to.be.false
|
||||
expect(status.accounts[handle].blockedByServer).to.be.true
|
||||
}
|
||||
|
||||
expect(status.accounts[unknownHandle].blockedByUser).to.be.false
|
||||
expect(status.accounts[unknownHandle].blockedByServer).to.be.false
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the remote account', async function () {
|
||||
await command.removeFromServerBlocklist({ account: 'user2@localhost:' + servers[1].port })
|
||||
})
|
||||
|
@ -620,6 +709,7 @@ describe('Test blocklist', function () {
|
|||
})
|
||||
|
||||
describe('When managing server blocklist', function () {
|
||||
|
||||
it('Should list all videos', async function () {
|
||||
for (const token of [ userModeratorToken, servers[0].accessToken ]) {
|
||||
await checkAllVideos(servers[0], token)
|
||||
|
@ -713,6 +803,23 @@ describe('Test blocklist', function () {
|
|||
expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
|
||||
})
|
||||
|
||||
it('Should get blocklist status', async function () {
|
||||
const blockedServer = servers[1].host
|
||||
const notBlockedServer = 'example.com'
|
||||
|
||||
for (const token of [ undefined, servers[0].accessToken ]) {
|
||||
const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] })
|
||||
expect(Object.keys(status.accounts)).to.have.lengthOf(0)
|
||||
|
||||
expect(Object.keys(status.hosts)).to.have.lengthOf(2)
|
||||
expect(status.hosts[blockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[blockedServer].blockedByServer).to.be.true
|
||||
|
||||
expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
|
||||
expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
|
||||
}
|
||||
})
|
||||
|
||||
it('Should unblock the remote server', async function () {
|
||||
await command.removeFromServerBlocklist({ server: 'localhost:' + servers[1].port })
|
||||
})
|
||||
|
|
|
@ -267,7 +267,7 @@ describe('Test user notifications', function () {
|
|||
})
|
||||
|
||||
it('Should send a notification when an imported video is transcoded', async function () {
|
||||
this.timeout(50000)
|
||||
this.timeout(120000)
|
||||
|
||||
const name = 'video import ' + buildUUID()
|
||||
|
||||
|
|
|
@ -43,6 +43,9 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
|||
expect(data.services.twitter.username).to.equal('@Chocobozzz')
|
||||
expect(data.services.twitter.whitelisted).to.be.false
|
||||
|
||||
expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false
|
||||
expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false
|
||||
|
||||
expect(data.cache.previews.size).to.equal(1)
|
||||
expect(data.cache.captions.size).to.equal(1)
|
||||
expect(data.cache.torrents.size).to.equal(1)
|
||||
|
@ -138,6 +141,9 @@ function checkUpdatedConfig (data: CustomConfig) {
|
|||
expect(data.services.twitter.username).to.equal('@Kuja')
|
||||
expect(data.services.twitter.whitelisted).to.be.true
|
||||
|
||||
expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.true
|
||||
expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.true
|
||||
|
||||
expect(data.cache.previews.size).to.equal(2)
|
||||
expect(data.cache.captions.size).to.equal(3)
|
||||
expect(data.cache.torrents.size).to.equal(4)
|
||||
|
@ -246,6 +252,18 @@ const newCustomConfig: CustomConfig = {
|
|||
whitelisted: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: true
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: true
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: 2
|
||||
|
|
|
@ -56,7 +56,7 @@ describe('Test services', function () {
|
|||
const oembedUrl = server.url + basePath + video.uuid + suffix
|
||||
|
||||
const res = await server.services.getOEmbed({ oembedUrl })
|
||||
const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
|
||||
const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
|
||||
`title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
|
||||
'frameborder="0" allowfullscreen></iframe>'
|
||||
const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath
|
||||
|
@ -79,7 +79,7 @@ describe('Test services', function () {
|
|||
const oembedUrl = server.url + basePath + playlistUUID + suffix
|
||||
|
||||
const res = await server.services.getOEmbed({ oembedUrl })
|
||||
const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
|
||||
const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
|
||||
`title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` +
|
||||
'frameborder="0" allowfullscreen></iframe>'
|
||||
|
||||
|
@ -103,7 +103,7 @@ describe('Test services', function () {
|
|||
const maxWidth = 50
|
||||
|
||||
const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth })
|
||||
const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' +
|
||||
const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' +
|
||||
`title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
|
||||
'frameborder="0" allowfullscreen></iframe>'
|
||||
|
||||
|
|
|
@ -207,6 +207,25 @@ describe('Test CLI wrapper', function () {
|
|||
|
||||
expect(res).to.not.contain('peertube-plugin-hello-world')
|
||||
})
|
||||
|
||||
it('Should install a plugin in requested version', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world --plugin-version 0.0.17`)
|
||||
})
|
||||
|
||||
it('Should list installed plugins, in correct version', async function () {
|
||||
const res = await cliCommand.execWithEnv(`${cmd} plugins list`)
|
||||
|
||||
expect(res).to.contain('peertube-plugin-hello-world')
|
||||
expect(res).to.contain('0.0.17')
|
||||
})
|
||||
|
||||
it('Should uninstall the plugin again', async function () {
|
||||
const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`)
|
||||
|
||||
expect(res).to.not.contain('peertube-plugin-hello-world')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Manage video redundancies', function () {
|
||||
|
|
|
@ -31,6 +31,7 @@ program
|
|||
.option('-p, --password <token>', 'Password')
|
||||
.option('-P --path <path>', 'Install from a path')
|
||||
.option('-n, --npm-name <npmName>', 'Install from npm')
|
||||
.option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)')
|
||||
.action((options, command) => installPluginCLI(command, options))
|
||||
|
||||
program
|
||||
|
@ -109,7 +110,7 @@ async function installPluginCLI (command: Command, options: OptionValues) {
|
|||
await assignToken(server, username, password)
|
||||
|
||||
try {
|
||||
await server.plugins.install({ npmName: options.npmName, path: options.path })
|
||||
await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion })
|
||||
} catch (err) {
|
||||
console.error('Cannot install plugin.', err)
|
||||
process.exit(-1)
|
||||
|
|
|
@ -194,6 +194,18 @@ export class ConfigCommand extends AbstractCommand {
|
|||
whitelisted: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: false
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: false
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: 2
|
||||
|
|
|
@ -158,15 +158,16 @@ export class PluginsCommand extends AbstractCommand {
|
|||
install (options: OverrideCommandOptions & {
|
||||
path?: string
|
||||
npmName?: string
|
||||
pluginVersion?: string
|
||||
}) {
|
||||
const { npmName, path } = options
|
||||
const { npmName, path, pluginVersion } = options
|
||||
const apiPath = '/api/v1/plugins/install'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: apiPath,
|
||||
fields: { npmName, path },
|
||||
fields: { npmName, path, pluginVersion },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
|
|
@ -220,10 +220,11 @@ export class PeerTubeServer {
|
|||
|
||||
return new Promise<void>((res, rej) => {
|
||||
const self = this
|
||||
let aggregatedLogs = ''
|
||||
|
||||
this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
|
||||
|
||||
const onPeerTubeExit = () => rej(new Error('Process exited'))
|
||||
const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs))
|
||||
const onParentExit = () => {
|
||||
if (!this.app || !this.app.pid) return
|
||||
|
||||
|
@ -238,10 +239,13 @@ export class PeerTubeServer {
|
|||
this.app.stdout.on('data', function onStdout (data) {
|
||||
let dontContinue = false
|
||||
|
||||
const log: string = data.toString()
|
||||
aggregatedLogs += log
|
||||
|
||||
// Capture things if we want to
|
||||
for (const key of Object.keys(regexps)) {
|
||||
const regexp = regexps[key]
|
||||
const matches = data.toString().match(regexp)
|
||||
const matches = log.match(regexp)
|
||||
if (matches !== null) {
|
||||
if (key === 'client_id') self.store.client.id = matches[1]
|
||||
else if (key === 'client_secret') self.store.client.secret = matches[1]
|
||||
|
@ -252,7 +256,7 @@ export class PeerTubeServer {
|
|||
|
||||
// Check if all required sentences are here
|
||||
for (const key of Object.keys(serverRunString)) {
|
||||
if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
|
||||
if (log.includes(key)) serverRunString[key] = true
|
||||
if (serverRunString[key] === false) dontContinue = true
|
||||
}
|
||||
|
||||
|
@ -260,7 +264,7 @@ export class PeerTubeServer {
|
|||
if (dontContinue === true) return
|
||||
|
||||
if (options.hideLogs === false) {
|
||||
console.log(data.toString())
|
||||
console.log(log)
|
||||
} else {
|
||||
process.removeListener('exit', onParentExit)
|
||||
self.app.stdout.removeListener('data', onStdout)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { AccountBlock, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
|
||||
import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||
|
||||
type ListBlocklistOptions = OverrideCommandOptions & {
|
||||
|
@ -37,6 +37,29 @@ export class BlocklistCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getStatus (options: OverrideCommandOptions & {
|
||||
accounts?: string[]
|
||||
hosts?: string[]
|
||||
}) {
|
||||
const { accounts, hosts } = options
|
||||
|
||||
const path = '/api/v1/blocklist/status'
|
||||
|
||||
return this.getRequestBody<BlockStatus>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
accounts,
|
||||
hosts
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
addToMyBlocklist (options: OverrideCommandOptions & {
|
||||
account?: string
|
||||
server?: string
|
||||
|
|
15
shared/models/moderation/block-status.model.ts
Normal file
15
shared/models/moderation/block-status.model.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface BlockStatus {
|
||||
accounts: {
|
||||
[ handle: string ]: {
|
||||
blockedByServer: boolean
|
||||
blockedByUser?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
hosts: {
|
||||
[ host: string ]: {
|
||||
blockedByServer: boolean
|
||||
blockedByUser?: boolean
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './abuse'
|
||||
export * from './block-status.model'
|
||||
export * from './account-block.model'
|
||||
export * from './server-block.model'
|
||||
|
|
|
@ -1 +1,10 @@
|
|||
export type PluginSelectorId = 'login-form'
|
||||
export type PluginSelectorId =
|
||||
'login-form' |
|
||||
'menu-user-dropdown-language-item' |
|
||||
'about-instance-features' |
|
||||
'about-instance-statistics' |
|
||||
'about-instance-moderation' |
|
||||
'about-menu-instance' |
|
||||
'about-menu-peertube' |
|
||||
'about-menu-network' |
|
||||
'about-instance-other-information'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export interface InstallOrUpdatePlugin {
|
||||
npmName?: string
|
||||
pluginVersion?: string
|
||||
path?: string
|
||||
}
|
||||
|
|
|
@ -52,6 +52,20 @@ export interface CustomConfig {
|
|||
}
|
||||
}
|
||||
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: boolean
|
||||
}
|
||||
}
|
||||
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cache: {
|
||||
previews: {
|
||||
size: number
|
||||
|
|
|
@ -39,6 +39,12 @@ export interface ServerConfig {
|
|||
preferAuthorDisplayName: boolean
|
||||
}
|
||||
}
|
||||
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webadmin: {
|
||||
|
|
|
@ -3792,6 +3792,39 @@ paths:
|
|||
'500':
|
||||
description: search index unavailable
|
||||
|
||||
/blocklist/status:
|
||||
get:
|
||||
tags:
|
||||
- Account Blocks
|
||||
- Server Blocks
|
||||
summary: Get block status of accounts/hosts
|
||||
parameters:
|
||||
-
|
||||
name: 'accounts'
|
||||
in: query
|
||||
description: 'Check if these accounts are blocked'
|
||||
example: [ 'goofy@example.com', 'donald@example.com' ]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
-
|
||||
name: 'hosts'
|
||||
in: query
|
||||
description: 'Check if these hosts are blocked'
|
||||
example: [ 'example.com' ]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlockStatus'
|
||||
|
||||
/server/blocklist/accounts:
|
||||
get:
|
||||
tags:
|
||||
|
@ -5134,6 +5167,29 @@ components:
|
|||
label:
|
||||
type: string
|
||||
|
||||
BlockStatus:
|
||||
properties:
|
||||
accounts:
|
||||
type: object
|
||||
additionalProperties:
|
||||
x-additionalPropertiesName: account
|
||||
type: object
|
||||
properties:
|
||||
blockedByServer:
|
||||
type: boolean
|
||||
blockedByUser:
|
||||
type: boolean
|
||||
hosts:
|
||||
type: object
|
||||
additionalProperties:
|
||||
x-additionalPropertiesName: host
|
||||
type: object
|
||||
properties:
|
||||
blockedByServer:
|
||||
type: boolean
|
||||
blockedByUser:
|
||||
type: boolean
|
||||
|
||||
NSFWPolicy:
|
||||
type: string
|
||||
enum:
|
||||
|
|
40
support/doc/development/ci.md
Normal file
40
support/doc/development/ci.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Continuous integration
|
||||
|
||||
PeerTube uses Github Actions as a CI platform.
|
||||
CI tasks are described in `.github/workflows`.
|
||||
|
||||
## benchmark.yml
|
||||
|
||||
*Scheduled*
|
||||
|
||||
Run various benchmarks (build, API etc) and upload results on https://builds.joinpeertube.org/peertube-stats/ to be publicly consumed.
|
||||
|
||||
## codeql.yml
|
||||
|
||||
*Scheduled, on push on develop and on pull request*
|
||||
|
||||
Run CodeQL task to throw code security issues in Github. https://lgtm.com/projects/g/Chocobozzz/PeerTube can also be used.
|
||||
|
||||
## docker.yml
|
||||
|
||||
*Scheduled and on push on master*
|
||||
|
||||
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).
|
||||
|
||||
## nightly.yml
|
||||
|
||||
*Scheduled*
|
||||
|
||||
Build PeerTube nightly build (`develop` branch) and upload the release on https://builds.joinpeertube.org/nightly.
|
||||
|
||||
## stats.yml
|
||||
|
||||
*On push on develop*
|
||||
|
||||
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.
|
||||
|
||||
## test.yml
|
||||
|
||||
*Scheduled, on push and pull request*
|
||||
|
||||
Run PeerTube lint and tests.
|
|
@ -21,10 +21,10 @@ WORKDIR /app
|
|||
|
||||
USER peertube
|
||||
|
||||
RUN yarn install --pure-lockfile \
|
||||
RUN yarn install --pure-lockfile --network-timeout 600000 \
|
||||
&& npm run build -- $NPM_RUN_BUILD_OPTS \
|
||||
&& rm -r ./node_modules ./client/node_modules \
|
||||
&& yarn install --pure-lockfile --production \
|
||||
&& yarn install --pure-lockfile --production --network-timeout 600000 \
|
||||
&& yarn cache clean
|
||||
|
||||
USER root
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -2613,11 +2613,6 @@ call-me-maybe@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
|
||||
integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
|
||||
|
||||
callsite@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
|
||||
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
|
||||
|
||||
callsites@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
|
@ -3229,13 +3224,6 @@ debuglog@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
|
||||
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
|
||||
|
||||
decache@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/decache/-/decache-4.6.0.tgz#87026bc6e696759e82d57a3841c4e251a30356e8"
|
||||
integrity sha512-PppOuLiz+DFeaUvFXEYZjLxAkKiMYH/do/b/MxpDe/8AgKBi5GhZxridoVIbBq72GDbL36e4p0Ce2jTGUwwU+w==
|
||||
dependencies:
|
||||
callsite "^1.0.0"
|
||||
|
||||
decamelize@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
|
|
Loading…
Reference in New Issue
Block a user