--- /dev/null
+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 }};
--- /dev/null
+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
--- /dev/null
+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
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: |
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 }}
--- /dev/null
+# 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
--- /dev/null
+name: "PeerTube CodeQL config"
+
+paths-ignore:
+ - server/tests
--- /dev/null
+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 }}
--- /dev/null
+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 }}
-name: "Stats"
+name: Stats
on:
push:
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:
- 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 }}
-name: Test Suite
+name: Test
on:
push:
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
${{ 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: |
+++ /dev/null
-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
<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>
+ <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 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.moderationInformation"></div>
+ </div>
- <div [innerHTML]="html.codeOfConduct"></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 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.codeOfConduct"></div>
+ </div>
- <div [innerHTML]="html.terms"></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 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 [innerHTML]="html.terms"></div>
+ </div>
+ </div>
- <div class="block hardware-information" *ngIf="html.hardwareInformation">
- <div class="anchor" id="hardware-information"></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="hardware-information"
+ fragment="other-information"
#anchorLink
(click)="onClickCopyLink(anchorLink)">
- <h3 i18n class="section-title">Hardware information</h3>
+ <h2 i18n class="middle-title">
+ OTHER INFORMATION
+ </h2>
</a>
- <div [innerHTML]="html.hardwareInformation"></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 [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"
<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>
></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">
}
}
-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 {
@include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
}
+.actor-display-name {
+ align-items: center;
+}
+
.description {
grid-column: 1 / 3;
max-width: 1000px;
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({
private authService: AuthService,
private videoService: VideoService,
private markdown: MarkdownService,
+ private blocklist: BlocklistService,
private screenService: ScreenService
) {
}
this.updateModerationActions()
this.loadUserIfNeeded(account)
this.loadAccountVideosCount()
+ this.loadAccountBlockStatus()
}
private showReportModal () {
this.accountVideosCount = res.total
})
}
+
+ private loadAccountBlockStatus () {
+ this.blocklist.getStatus({ accounts: [ this.account.nameWithHostForced ], hosts: [ this.account.host ] })
+ .subscribe(status => this.account.updateBlockStatus(status))
+ }
}
</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>
<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>
}
}
+ countExternalAuth () {
+ return this.serverConfig.plugin.registeredExternalAuths.length
+ }
+
getVideoQuotaOptions () {
return this.configService.videoQuotaOptions
}
whitelisted: null
}
},
+ client: {
+ videos: {
+ miniature: {
+ preferAuthorDisplayName: null
+ }
+ },
+ menu: {
+ login: {
+ redirectOnSingleExternalAuth: null
+ }
+ }
+ },
cache: {
previews: {
size: CACHE_PREVIEWS_SIZE_VALIDATOR
<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>
-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'
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({
}
getAuthHref (auth: RegisteredExternalAuthConfig) {
- return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
+ return PluginsManager.getExternalAuthHref(auth)
}
login () {
<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>
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 } from '@shared/models'
@ViewChild('supportModal') supportModal: SupportModalComponent
videoChannel: VideoChannel
+ ownerAccount: Account
hotkeys: Hotkey[]
links: ListOverflowItem[] = []
isChannelManageable = false
private restExtractor: RestExtractor,
private hotkeysService: HotkeysService,
private screenService: ScreenService,
- private markdown: MarkdownService
+ private markdown: MarkdownService,
+ private blocklist: BlocklistService
) { }
ngOnInit () {
// After the markdown renderer to avoid layout changes
this.videoChannel = videoChannel
+ this.ownerAccount = new Account(this.videoChannel.ownerAccount)
this.loadChannelVideosCount()
+ this.loadOwnerBlockStatus()
})
this.hotkeys = [
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))
+ }
}
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'
@NgModule({
imports: [
SharedUserSubscriptionModule,
SharedGlobalIconModule,
SharedSupportModal,
- SharedActorImageModule
+ SharedActorImageModule,
+ SharedModerationModule
],
declarations: [
<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>
</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>
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')
.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
-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 {
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
+ }
}
--- /dev/null
+<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>
--- /dev/null
+@use '_variables' as *;
+@use '_mixins' as *;
+
+.badge {
+ @include margin-right(10px);
+
+ height: fit-content;
+ font-size: 12px;
+}
--- /dev/null
+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
+}
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'
@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'
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 }) {
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'
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({
VideoReportComponent,
BatchDomainsModalComponent,
CommentReportComponent,
- AccountReportComponent
+ AccountReportComponent,
+ AccountBlockBadgesComponent
],
exports: [
VideoReportComponent,
BatchDomainsModalComponent,
CommentReportComponent,
- AccountReportComponent
+ AccountReportComponent,
+ AccountBlockBadgesComponent
],
providers: [
{
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)
},
{
}
duration () {
- return this._duration || this.videoElement.duration || 0
+ if (!isNaN(this.videoElement.duration)) return this.videoElement.duration
+
+ return this._duration || 0
}
seekable () {
this.isLive = data.details.live
this.dvrDuration = data.details.totalduration
+
this._duration = this.isLive ? Infinity : data.details.totalduration
})
RegisterClientHookOptions,
RegisterClientSettingsScript,
RegisterClientVideoFieldOptions,
+ RegisteredExternalAuthConfig,
ServerConfigPlugin
} from '../../../shared/models'
import { environment } from '../environments/environment'
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)
# 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...
# 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...
federation:
videos:
federate_unlisted: true
+ cleanup_remote_interactions: false
views:
videos:
"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",
--- /dev/null
+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)
+ }
+}
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
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'
apiRouter.use('/overviews', overviewsRouter)
apiRouter.use('/plugins', pluginRouter)
apiRouter.use('/custom-pages', customPageRouter)
+apiRouter.use('/blocklist', blocklistRouter)
apiRouter.use('/ping', pong)
apiRouter.use('/*', badRequest)
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) {
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'
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 {
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)
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,
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'
authenticate,
videosAddLegacyValidator,
videosAddResumableInitValidator,
- videosResumableUploadIdValidator,
- videosAddResumableValidator
+ videosAddResumableValidator,
+ videosResumableUploadIdValidator
} from '../../../middlewares'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
import { VideoModel } from '../../../models/video/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),
--- /dev/null
+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
+}
--- /dev/null
+// 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]
+ }
+ })
+}
'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',
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'
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') }
+ }
}
},
delete require.cache[fileName]
}
- decache('config')
+ decacheModule('config')
}
purge()
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
}
// ---------------------------------------------------------------------------
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
}
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)
}
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 () {
-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'
// Delete cache if needed
const modulePath = join(pluginPath, packageJSON.library)
- decache(modulePath)
+ decachePlugin(pluginPath, modulePath)
const library: PluginLibrary = require(modulePath)
if (!isLibraryCodeValid(library)) {
-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')
}
protected async internalExecute () {
- const path = getResumableUploadPath()
- const files = await readdir(path)
-
- const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME))
+ logger.debug('Removing dangling resumable uploads', lTags())
- 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
}
}
miniature: {
preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
}
+ },
+ menu: {
+ login: {
+ redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
+ }
}
},
--- /dev/null
+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
+}
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'
}
]
+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 {
unblockAccountByAccountValidator,
unblockServerByAccountValidator,
unblockAccountByServerValidator,
- unblockServerByServerValidator
+ unblockServerByServerValidator,
+ blocklistStatusValidator
}
// ---------------------------------------------------------------------------
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'),
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()
}
-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 {
})
BlockedAccount: AccountModel
- static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) {
+ static isAccountMutedByAccounts (accountIds: number[], targetAccountId: number) {
const query = {
attributes: [ 'accountId', 'id' ],
where: {
.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(),
-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 {
})
BlockedServer: ServerModel
- static isServerMutedByMulti (accountIds: number[], targetServerId: number) {
+ static isServerMutedByAccounts (accountIds: number[], targetServerId: number) {
const query = {
attributes: [ 'accountId', 'id' ],
where: {
.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
})
})
+ 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)
})
whitelisted: true
}
},
+ client: {
+ videos: {
+ miniature: {
+ preferAuthorDisplayName: false
+ }
+ },
+ menu: {
+ login: {
+ redirectOnSingleExternalAuth: false
+ }
+ }
+ },
cache: {
previews: {
size: 2
// ---------------------------------------------------------------
before(async function () {
- this.timeout(30000)
+ this.timeout(60000)
server = await createSingleServer(1)
}
})
+ 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)
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 })
})
}
})
+ 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 })
})
})
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)
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 })
})
})
it('Should send a notification when an imported video is transcoded', async function () {
- this.timeout(50000)
+ this.timeout(120000)
const name = 'video import ' + buildUUID()
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)
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)
whitelisted: true
}
},
+ client: {
+ videos: {
+ miniature: {
+ preferAuthorDisplayName: true
+ }
+ },
+ menu: {
+ login: {
+ redirectOnSingleExternalAuth: true
+ }
+ }
+ },
cache: {
previews: {
size: 2
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 () {
.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
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)
whitelisted: true
}
},
+ client: {
+ videos: {
+ miniature: {
+ preferAuthorDisplayName: false
+ }
+ },
+ menu: {
+ login: {
+ redirectOnSingleExternalAuth: false
+ }
+ }
+ },
cache: {
previews: {
size: 2
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
})
/* 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 & {
// ---------------------------------------------------------------------------
+ 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
--- /dev/null
+export interface BlockStatus {
+ accounts: {
+ [ handle: string ]: {
+ blockedByServer: boolean
+ blockedByUser?: boolean
+ }
+ }
+
+ hosts: {
+ [ host: string ]: {
+ blockedByServer: boolean
+ blockedByUser?: boolean
+ }
+ }
+}
export * from './abuse'
+export * from './block-status.model'
export * from './account-block.model'
export * from './server-block.model'
-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'
export interface InstallOrUpdatePlugin {
npmName?: string
+ pluginVersion?: string
path?: string
}
}
}
+ client: {
+ videos: {
+ miniature: {
+ preferAuthorDisplayName: boolean
+ }
+ }
+
+ menu: {
+ login: {
+ redirectOnSingleExternalAuth: boolean
+ }
+ }
+ }
+
cache: {
previews: {
size: number
preferAuthorDisplayName: boolean
}
}
+
+ menu: {
+ login: {
+ redirectOnSingleExternalAuth: boolean
+ }
+ }
}
webadmin: {
--- /dev/null
+# 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.
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
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"
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"