]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Merge branch 'release/3.1.0' into develop
authorChocobozzz <me@florianbigard.com>
Thu, 25 Mar 2021 12:55:10 +0000 (13:55 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 25 Mar 2021 12:55:10 +0000 (13:55 +0100)
176 files changed:
.github/FUNDING.yml
.github/workflows/stats.yml
.github/workflows/test.yml
client/src/app/+about/about-follows/about-follows.component.html
client/src/app/+about/about-instance/about-instance.component.html
client/src/app/+accounts/accounts.component.html
client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
client/src/app/+login/login.component.html
client/src/app/+login/login.component.ts
client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
client/src/app/+search/search.component.html
client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
client/src/app/+video-channels/video-channels.component.html
client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
client/src/app/+videos/+video-watch/comment/video-comments.component.html
client/src/app/+videos/+video-watch/comment/video-comments.component.ts
client/src/app/core/notification/peertube-socket.service.ts
client/src/app/core/plugins/hooks.service.ts
client/src/app/core/plugins/plugin.service.ts
client/src/app/header/search-typeahead.component.html
client/src/app/menu/menu.component.scss
client/src/app/modal/instance-config-warning-modal.component.html
client/src/app/shared/shared-forms/input-toggle-hidden.component.html
client/src/app/shared/shared-forms/select/select-options.component.ts
client/src/app/shared/shared-main/angular/autofocus.directive.ts [new file with mode: 0644]
client/src/app/shared/shared-main/angular/index.ts
client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
client/src/app/shared/shared-main/shared-main.module.ts
client/src/app/shared/shared-main/users/user-notification.model.ts
client/src/app/shared/shared-main/users/user-notifications.component.html
client/src/app/shared/shared-main/users/user-notifications.component.ts
client/src/app/shared/shared-video-miniature/video-download.component.html
client/src/app/shared/shared-video-miniature/video-download.component.ts
client/src/assets/images/feather/cloud-download.svg
client/src/assets/images/feather/subscriptions.svg [deleted file]
client/src/assets/images/misc/language.svg
client/src/assets/images/misc/npm.svg
client/src/assets/images/misc/peertube-x.svg
client/src/assets/images/misc/playlist-add.svg
client/src/assets/images/misc/support.svg
client/src/assets/images/misc/video-lang.svg
client/src/sass/bootstrap.scss
client/src/sass/include/_mixins.scss
client/src/sass/player/peertube-skin.scss
client/src/standalone/videos/embed.ts
client/src/types/register-client-option.model.ts
client/yarn.lock
config/default.yaml
config/production.yaml.example
config/test.yaml
package.json
scripts/parse-log.ts
scripts/regenerate-thumbnails.ts [new file with mode: 0644]
server.ts
server/controllers/api/jobs.ts
server/controllers/api/plugins.ts
server/controllers/api/search.ts
server/controllers/api/users/index.ts
server/controllers/api/users/my-notifications.ts
server/controllers/api/users/token.ts
server/controllers/api/videos/index.ts
server/controllers/client.ts
server/controllers/download.ts
server/controllers/feeds.ts
server/controllers/plugins.ts
server/helpers/activitypub.ts
server/helpers/core-utils.ts
server/helpers/custom-validators/activitypub/activity.ts
server/helpers/custom-validators/activitypub/flag.ts [deleted file]
server/helpers/custom-validators/activitypub/rate.ts [deleted file]
server/helpers/custom-validators/activitypub/share.ts [deleted file]
server/helpers/custom-validators/activitypub/view.ts [deleted file]
server/helpers/custom-validators/user-notifications.ts
server/helpers/ffmpeg-utils.ts
server/helpers/logger.ts
server/helpers/peertube-crypto.ts
server/helpers/requests.ts
server/helpers/youtube-dl.ts
server/initializers/checker-after-init.ts
server/initializers/checker-before-init.ts
server/initializers/config.ts
server/initializers/constants.ts
server/initializers/database.ts
server/initializers/migrations/0610-views-index copy.ts [moved from server/initializers/migrations/0610-views-index.ts with 100% similarity]
server/initializers/migrations/0615-latest-versions-notification-settings.ts [new file with mode: 0644]
server/initializers/migrations/0620-latest-versions-application.ts [new file with mode: 0644]
server/initializers/migrations/0625-latest-versions-notification.ts [new file with mode: 0644]
server/lib/activitypub/actor.ts
server/lib/activitypub/crawl.ts
server/lib/activitypub/playlist.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/share.ts
server/lib/activitypub/video-comments.ts
server/lib/activitypub/video-rates.ts
server/lib/activitypub/videos.ts
server/lib/auth/external-auth.ts [moved from server/lib/auth.ts with 61% similarity]
server/lib/auth/oauth-model.ts [moved from server/lib/oauth-model.ts with 63% similarity]
server/lib/auth/oauth.ts [new file with mode: 0644]
server/lib/auth/tokens-cache.ts [new file with mode: 0644]
server/lib/emailer.ts
server/lib/emails/peertube-version-new/html.pug [new file with mode: 0644]
server/lib/emails/plugin-version-new/html.pug [new file with mode: 0644]
server/lib/files-cache/videos-caption-cache.ts
server/lib/files-cache/videos-preview-cache.ts
server/lib/files-cache/videos-torrent-cache.ts
server/lib/hls.ts
server/lib/job-queue/handlers/activitypub-cleaner.ts
server/lib/job-queue/handlers/activitypub-http-broadcast.ts
server/lib/job-queue/handlers/activitypub-http-unicast.ts
server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
server/lib/notifier.ts
server/lib/plugins/plugin-index.ts
server/lib/plugins/register-helpers.ts
server/lib/schedulers/auto-follow-index-instances.ts
server/lib/schedulers/peertube-version-check-scheduler.ts [new file with mode: 0644]
server/lib/schedulers/plugins-check-scheduler.ts
server/lib/user.ts
server/lib/video-blacklist.ts
server/middlewares/auth.ts [moved from server/middlewares/oauth.ts with 86% similarity]
server/middlewares/index.ts
server/middlewares/validators/activitypub/signature.ts
server/middlewares/validators/jobs.ts
server/middlewares/validators/pagination.ts
server/middlewares/validators/sort.ts
server/middlewares/validators/utils.ts
server/middlewares/validators/videos/video-comments.ts
server/middlewares/validators/videos/video-playlists.ts
server/middlewares/validators/videos/videos.ts
server/models/account/user-notification-setting.ts
server/models/account/user-notification.ts
server/models/account/user.ts
server/models/application/application.ts
server/models/oauth/oauth-token.ts
server/tests/api/activitypub/security.ts
server/tests/api/check-params/user-notifications.ts
server/tests/api/check-params/users.ts
server/tests/api/notifications/admin-notifications.ts [new file with mode: 0644]
server/tests/api/notifications/index.ts
server/tests/api/server/handle-down.ts
server/tests/api/users/users.ts
server/tests/cli/index.ts
server/tests/cli/regenerate-thumbnails.ts [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test/main.js
server/tests/helpers/request.ts
server/tests/plugins/external-auth.ts
server/tests/plugins/filter-hooks.ts
server/tools/peertube-import-videos.ts
server/types/models/application/application.ts [new file with mode: 0644]
server/types/models/application/index.ts [new file with mode: 0644]
server/types/models/index.ts
server/types/models/user/user-notification.ts
server/typings/express/index.d.ts
shared/extra-utils/index.ts
shared/extra-utils/miscs/sql.ts
shared/extra-utils/mock-servers/joinpeertube-versions.ts [new file with mode: 0644]
shared/extra-utils/mock-servers/mock-instances-index.ts [moved from shared/extra-utils/instances-index/mock-instances-index.ts with 100% similarity]
shared/extra-utils/requests/activitypub.ts
shared/extra-utils/server/servers.ts
shared/extra-utils/users/user-notifications.ts
shared/models/index.ts
shared/models/joinpeertube/index.ts [new file with mode: 0644]
shared/models/joinpeertube/versions.model.ts [new file with mode: 0644]
shared/models/plugins/client-hook.model.ts
shared/models/plugins/server-hook.model.ts
shared/models/server/emailer.model.ts
shared/models/server/job.model.ts
shared/models/users/user-notification-setting.model.ts
shared/models/users/user-notification.model.ts
support/doc/development/release.md
support/doc/plugins/guide.md
support/doc/tools.md
support/systemd/peertube.service
yarn.lock

index b06143c8f211a6737c5899cfa6ecfa1ce612109a..ac2e6ce92159743c0429e8365d8b7b048bdbd741 100644 (file)
@@ -1 +1 @@
-custom: ["https://joinpeertube.org/roadmap", "https://soutenir.framasoft.org/en/"]
+custom: ["https://soutenir.framasoft.org/en/"]
index b5fb6d2a6ed25b23d039dbdc151e371db9718a37..a2f0945b3d14a63c5ca80b168dd1b19393e47845 100644 (file)
@@ -5,6 +5,7 @@ on:
     branches:
     - develop
     - ci
+    - next
   pull_request:
     types: [synchronize, opened]
 
index f8706d4bef172f73c97c729a313209097b2b4c4e..442317ce2bfcf12f0320b35a112a9cd5781705f6 100644 (file)
@@ -6,6 +6,7 @@ on:
       - develop
       - master
       - ci
+      - next
   pull_request:
     types: [synchronize, opened]
   schedule:
index 2cf890acfbe00439e43abddc56c27e9ce9f5c86a..e9139b503678183fbb75574c2fde49d30776fe8a 100644 (file)
@@ -1,7 +1,7 @@
 <div class="row">
   <h1 class="sr-only" i18n>Follows</h1>
   <div class="col-xl-6 col-md-12">
-    <h2 i18n class="subtitle">Followers instances ({{ followersPagination.totalItems }})</h2>
+    <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2>
 
     <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div>
 
index d8794d602d5d11e5f1c4018d781fe71cca2a71d8..1f372090e283ca882a9d756325e09d748e225aaa 100644 (file)
@@ -83,7 +83,7 @@
           fragment="business-model"
           #anchorLink
           (click)="onClickCopyLink(anchorLink)">
-          <h3 i18n class="section-title">How we will pay for this instance</h3>
+          <h3 i18n class="section-title">How we will pay for keeping our instance running</h3>
         </a>
 
       <div [innerHTML]="html.businessModel"></div>
index 5bd7b0824d6af10711b5a33dcc23f30f70b3d1dc..1903bb36f10a0ce65145406b33a92f194975c7ff 100644 (file)
@@ -12,7 +12,7 @@
             <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
                     class="btn btn-outline-secondary btn-sm copy-button"
             >
-              <span class="glyphicon glyphicon-copy"></span>
+              <span class="glyphicon glyphicon-duplicate"></span>
             </button>
           </div>
           <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
index 1b5fe45c6c5dd7b4285207c72e6c788dac9edd62..8edf03a89946083e5da6a4ec30f9c8c66f9cac72 100644 (file)
@@ -3,7 +3,7 @@
 </div>
 
 <div class="search-bar">
-  <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..."/>
+  <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..." autofocus />
 </div>
 
 <div class="alert alert-info" i18n *ngIf="pluginInstalled">
@@ -20,8 +20,8 @@
     <my-global-icon iconName="search"></my-global-icon>
 
     <ng-container i18n>
-      {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}"
-      </ng-container>
+      {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}"
+    </ng-container>
   </ng-container>
 </div>
 
index 3171e5b0f27a0e43818dc7cd386cca19a5a403f9..0167066a0f1ce718ae2c40138db766635b8a67fd 100644 (file)
@@ -21,7 +21,7 @@
               <label i18n for="username">User</label>
               <input
                 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
-                formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #usernameInput
+                formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" autofocus
               >
             </div>
 
index af747b7fa080efc6e1296b56de44b3130ad03324..d8ad49081caec79b8fc99db4648a4ab1611b695b 100644 (file)
@@ -3,9 +3,9 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula
 import { ActivatedRoute } from '@angular/router'
 import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
 import { HooksService } from '@app/core/plugins/hooks.service'
-import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
 import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
 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 { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
 
@@ -16,7 +16,6 @@ import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
 })
 
 export class LoginComponent extends FormReactive implements OnInit, AfterViewInit {
-  @ViewChild('usernameInput', { static: false }) usernameInput: ElementRef
   @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef
 
   accordion: NgbAccordion
@@ -91,10 +90,6 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
   }
 
   ngAfterViewInit () {
-    if (this.usernameInput) {
-      this.usernameInput.nativeElement.focus()
-    }
-
     this.hooks.runAction('action:login.init', 'login')
   }
 
index ad7497f45f644fe3ffdce6ae94600149ceddf436..c7e17303812825e4086fca013ca24baec3dff476 100644 (file)
@@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
       newInstanceFollower: $localize`Your instance has a new follower`,
       autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
       abuseNewMessage: $localize`An abuse report received a new message`,
-      abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`
+      abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
+      newPeerTubeVersion: $localize`A new PeerTube version is available`,
+      newPluginVersion: $localize`One of your plugin/theme has a new available version`
     }
     this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
 
@@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
       videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
       newUserRegistration: UserRight.MANAGE_USERS,
       newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
-      autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION
+      autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION,
+      newPeerTubeVersion: UserRight.MANAGE_DEBUG,
+      newPluginVersion: UserRight.MANAGE_DEBUG
     }
   }
 
index 84be4fb1405676ed2e9ef1e8b1c28a2afa27df39..74c6839e1d6e54c6e1c248c9e5113e2ffc040178 100644 (file)
@@ -2,14 +2,12 @@
   <div class="results-header">
     <div class="first-line">
       <div class="results-counter" *ngIf="pagination.totalItems">
-        <span i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} </span>
+        <span class="mr-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span>
 
-        <span i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span>
-        <span i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span>
+        <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span>
+        <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span>
 
-        <span *ngIf="currentSearch" i18n>
-          for <span class="search-value">{{ currentSearch }}</span>
-        </span>
+        <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span>
       </div>
 
       <div
index 03770ceece041e8b4dc53975e548934adc699ae8..594935afdcadefa59fcdb53e10ee95d6a293899e 100644 (file)
@@ -1,6 +1,6 @@
 <div class="margin-content">
   <div i18n class="title-page title-page-single">
-    Created {{ pagination.totalItems }} playlists
+    Created {pagination.totalItems, plural, =1 {1 playlist} other {{{ pagination.totalItems }} playlists}}
   </div>
 
   <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div>
index 4b0d12b6e79e8fe3ffe36951853b1f47326e139d..b3ea19768c97ee49eed454da7a8599aca3094f87 100644 (file)
@@ -12,7 +12,7 @@
             <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
                     class="btn btn-outline-secondary btn-sm copy-button"
             >
-              <span class="glyphicon glyphicon-copy"></span>
+              <span class="glyphicon glyphicon-duplicate"></span>
             </button>
           </div>
         </div>
index 8780ca5678e641095fa4e5f6668fc59606877b79..8e035b6bbba9c1141b4a57c7f6b8fbe2631a0540 100644 (file)
@@ -1,8 +1,8 @@
 
 import { forkJoin } from 'rxjs'
-import { Component, EventEmitter, OnInit, Output } from '@angular/core'
+import { AfterViewChecked, AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
 import { Router } from '@angular/router'
-import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
+import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
 import { scrollToTop } from '@app/helpers'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
@@ -19,7 +19,7 @@ import { VideoSend } from './video-send'
     './video-send.scss'
   ]
 })
-export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate {
+export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
   @Output() firstStepDone = new EventEmitter<string>()
   @Output() firstStepError = new EventEmitter<void>()
 
@@ -41,7 +41,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
     protected videoService: VideoService,
     protected videoCaptionService: VideoCaptionService,
     private liveVideoService: LiveVideoService,
-    private router: Router
+    private router: Router,
+    private hooks: HooksService
     ) {
     super()
   }
@@ -50,6 +51,10 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
     super.ngOnInit()
   }
 
+  ngAfterViewInit () {
+    this.hooks.runAction('action:go-live.init', 'video-edit')
+  }
+
   canDeactivate () {
     return { canDeactivate: true }
   }
index 01087e5251799d430aea64c32dc4152cfff91e0e..3aae24732e5d33063e0d621f690e735c7d6594eb 100644 (file)
@@ -1,6 +1,6 @@
-import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
+import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
 import { Router } from '@angular/router'
-import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
+import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
 import { scrollToTop } from '@app/helpers'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
     './video-send.scss'
   ]
 })
-export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate {
+export class VideoImportTorrentComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
   @Output() firstStepDone = new EventEmitter<string>()
   @Output() firstStepError = new EventEmitter<void>()
   @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
@@ -43,7 +43,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
     protected videoService: VideoService,
     protected videoCaptionService: VideoCaptionService,
     private router: Router,
-    private videoImportService: VideoImportService
+    private videoImportService: VideoImportService,
+    private hooks: HooksService
     ) {
     super()
   }
@@ -52,6 +53,10 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
     super.ngOnInit()
   }
 
+  ngAfterViewInit () {
+    this.hooks.runAction('action:video-torrent-import.init', 'video-edit')
+  }
+
   canDeactivate () {
     return { canDeactivate: true }
   }
index c447c179d35a575cba30a3ed61092832e96b72c8..7a9fe369f108e96e41adf35256a23477a9f83c50 100644 (file)
@@ -1,7 +1,7 @@
 import { map, switchMap } from 'rxjs/operators'
-import { Component, EventEmitter, OnInit, Output } from '@angular/core'
+import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
 import { Router } from '@angular/router'
-import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
+import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
 import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
     './video-send.scss'
   ]
 })
-export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate {
+export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
   @Output() firstStepDone = new EventEmitter<string>()
   @Output() firstStepError = new EventEmitter<void>()
 
@@ -42,8 +42,9 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
     protected videoService: VideoService,
     protected videoCaptionService: VideoCaptionService,
     private router: Router,
-    private videoImportService: VideoImportService
-    ) {
+    private videoImportService: VideoImportService,
+    private hooks: HooksService
+  ) {
     super()
   }
 
@@ -51,6 +52,10 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
     super.ngOnInit()
   }
 
+  ngAfterViewInit () {
+    this.hooks.runAction('action:video-url-import.init', 'video-edit')
+  }
+
   canDeactivate () {
     return { canDeactivate: true }
   }
index ca21b61cd918bb7c81432714f3875751642c452a..effb37077465c3d7b9c3b913b3c034c7255e4e41 100644 (file)
@@ -1,15 +1,15 @@
 import { Subscription } from 'rxjs'
 import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
-import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
+import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
 import { Router } from '@angular/router'
-import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core'
+import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
 import { scrollToTop, uploadErrorHandler } from '@app/helpers'
 import { FormValidatorService } from '@app/shared/shared-forms'
 import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
 import { LoadingBarService } from '@ngx-loading-bar/core'
+import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
 import { VideoPrivacy } from '@shared/models'
 import { VideoSend } from './video-send'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
 
 @Component({
   selector: 'my-video-upload',
@@ -20,7 +20,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
     './video-send.scss'
   ]
 })
-export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate {
+export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate {
   @Output() firstStepDone = new EventEmitter<string>()
   @Output() firstStepError = new EventEmitter<void>()
   @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
@@ -60,7 +60,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
     protected videoService: VideoService,
     protected videoCaptionService: VideoCaptionService,
     private userService: UserService,
-    private router: Router
+    private router: Router,
+    private hooks: HooksService
     ) {
     super()
   }
@@ -79,6 +80,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
         })
   }
 
+  ngAfterViewInit () {
+    this.hooks.runAction('action:video-upload.init', 'video-edit')
+  }
+
   ngOnDestroy () {
     if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
   }
index 4a6426d3041ea52ea3c26d624d30eeb83a13f911..9e6fde2e0498415a44696d8c3786bc4b2dd0822d 100644 (file)
@@ -1,12 +1,7 @@
 <div>
   <div class="title-block">
     <h2 class="title-page title-page-single">
-      <ng-container *ngIf="totalNotDeletedComments > 0; then hasComments; else noComments"></ng-container>
-      <ng-template #hasComments>
-        <ng-container i18n *ngIf="totalNotDeletedComments === 1; else manyComments">1 Comment</ng-container>
-        <ng-template i18n #manyComments>{{ totalNotDeletedComments }} Comments</ng-template>
-      </ng-template>
-      <ng-template i18n #noComments>Comments</ng-template>
+      {totalNotDeletedComments, plural, =0 {Comments} =1 {1 Comment} other {{{totalNotDeletedComments}} Comments}}
     </h2>
 
     <my-feed [syndicationItems]="syndicationItems"></my-feed>
             <span class="glyphicon glyphicon-menu-down"></span>
 
             <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container>
+
             <ng-template #hasAuthorComments>
               <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n>
-                View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} and others
+                View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }} and others
               </ng-container>
               <ng-template i18n #onlyAuthorComments>
-                View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }}
+                View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }}
               </ng-template>
             </ng-template>
-            <ng-template i18n #noAuthorComments>View {{ comment.totalReplies }} replies</ng-template>
+
+            <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template>
 
             <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader>
           </div>
index d36dd9e34c6ad1481f762338c874622734649155..210236b61133856fe23f56afdb8737f75a484b47 100644 (file)
@@ -5,7 +5,6 @@ import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifie
 import { HooksService } from '@app/core/plugins/hooks.service'
 import { Syndication, VideoDetails } from '@app/shared/shared-main'
 import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment'
-import { ThisReceiver } from '@angular/compiler'
 
 @Component({
   selector: 'my-video-comments',
index bc3f7b89374093c986f4c93314158f3b37466c0f..eab1c63f24f9b1654233355d9c6674de22cf62e6 100644 (file)
@@ -58,12 +58,11 @@ export class PeerTubeSocket {
       this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', {
         query: { accessToken: this.auth.getAccessToken() }
       })
-
-      this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
-        this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
-      })
     })
 
+    this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
+      this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
+    })
   }
 
   private async initLiveVideosSocket () {
index ec47aa48c562fa621857c3b608e07edea5481dd1..ddde198d2ffb0fe2c7ec7ab0fd7023259a6ba36c 100644 (file)
@@ -3,13 +3,29 @@ import { mergeMap, switchMap } from 'rxjs/operators'
 import { Injectable } from '@angular/core'
 import { PluginService } from '@app/core/plugins/plugin.service'
 import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models'
+import { AuthService, AuthStatus } from '../auth'
 
 type RawFunction<U, T> = (params: U) => T
 type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
 
 @Injectable()
 export class HooksService {
-  constructor (private pluginService: PluginService) { }
+  constructor (
+    private authService: AuthService,
+    private pluginService: PluginService
+  ) {
+    // Run auth hooks
+    this.authService.userInformationLoaded
+      .subscribe(() => this.runAction('action:auth-user.information-loaded', 'common', { user: this.authService.getUser() }))
+
+    this.authService.loginChangedSource.subscribe(obj => {
+      if (obj === AuthStatus.LoggedIn) {
+        this.runAction('action:auth-user.logged-in', 'common')
+      } else if (obj === AuthStatus.LoggedOut) {
+        this.runAction('action:auth-user.logged-out', 'common')
+      }
+    })
+  }
 
   wrapObsFun
     <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName>
index b755fda2c6e3344c963c860987804985cbcaab6a..54dba5e178b5ee8095bb57ec58cd74a65335f0fc 100644 (file)
@@ -235,6 +235,12 @@ export class PluginService implements ClientHook {
                    .toPromise()
       },
 
+      getServerConfig: () => {
+        return this.server.getConfig()
+          .pipe(catchError(res => this.restExtractor.handleError(res)))
+          .toPromise()
+      },
+
       isLoggedIn: () => {
         return this.authService.isLoggedIn()
       },
index 03e86b8e64c7b0d1ad90b91dfb4935772f029f98..f84086b4a6793f0cb681fa11ed5060e399e3ce40 100644 (file)
@@ -34,7 +34,8 @@
 
     <!-- search instructions, when search input is empty -->
     <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden">
-      <div class="d-flex justify-content-between">
+      <span class="text-muted" i18n>Your query will be matched against video names or descriptions, channel names.</span>
+      <div class="d-flex justify-content-between mt-3">
         <label class="small-title" i18n>ADVANCED SEARCH</label>
         <div class="advanced-search-status c-help">
           <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
@@ -55,7 +56,6 @@
           <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span>
         </li>
       </ul>
-      <span class="text-muted" i18n>Any other input will return matching video or channel names.</span>
     </div>
   </div>
 
index 2ea66e57d71eb2b916006e0c2bb157ce01853cd0..aa247d268fc4e5e78b0304fd1026e403f3c6877f 100644 (file)
@@ -3,6 +3,7 @@
 
 $menu-link-icon-size: 22px;
 $menu-link-icon-margin-right: 18px;
+$footer-links-base-opacity: .8;
 
 @mixin menu-link {
   display: flex;
@@ -91,168 +92,168 @@ menu {
     align-items: center;
     justify-content: left;
 
-    .logged-in-more {
-      $main-radius: 25px;
+    my-notification {
+      margin-left: auto;
+      margin-right: 15px;
+    }
+  }
+}
 
-      flex: 1;
-      margin-left: 13px;
-      border-radius: $main-radius;
-      transition: all .1s ease-in-out;
-      cursor: pointer;
+.logged-in-more {
+  $main-radius: 25px;
 
-      *, & {
-        line-height: 1;
-      }
+  flex: 1;
+  margin-left: 13px;
+  border-radius: $main-radius;
+  transition: all .1s ease-in-out;
+  cursor: pointer;
 
-      &.show {
-        background-color: rgba(255, 255, 255, 0.20);
-        box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
-      }
+  *, & {
+    line-height: 1;
+  }
 
-      @mixin display-hints($is-mobile: false) {
-        background-color: rgba(255, 255, 255, 0.15);
-
-        @if $is-mobile {
-          .dropdown-toggle-indicator {
-            display: inherit !important;
-          }
-          .dropdown-toggle:first-child {
-            padding-right: 30px !important;
-          }
-        }
-      }
+  &.show {
+    background-color: rgba(255, 255, 255, 0.20);
+    box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
+  }
+
+  @mixin display-hints($is-mobile: false) {
+    background-color: rgba(255, 255, 255, 0.15);
 
-      &:hover {
-        @include display-hints;
+    @if $is-mobile {
+      .dropdown-toggle-indicator {
+        display: inherit !important;
+      }
+      .dropdown-toggle:first-child {
+        padding-right: 30px !important;
       }
+    }
+  }
 
-      /* smartphones and touchscreens */
-      @media (hover: none) and (pointer: coarse) {
-        @include display-hints($is-mobile: true);
+  &:hover {
+    @include display-hints;
+  }
 
-        /* fill space when on mobile */
-        max-width: calc(100% - 80px);
-        .dropdown-toggle {
-          max-width: 100%;
-        }
-        .logged-in-info {
-          max-width: calc(100% - 45px) !important;
-        }
+  /* smartphones and touchscreens */
+  @media (hover: none) and (pointer: coarse) {
+    @include display-hints($is-mobile: true);
 
-      }
+    /* fill space when on mobile */
+    max-width: calc(100% - 80px);
+    .dropdown-toggle {
+      max-width: 100%;
+    }
+    .logged-in-info {
+      max-width: calc(100% - 45px) !important;
+    }
 
-      .dropdown-toggle-indicator {
-        position: relative;
-        width: 0;
-        display: none;
-
-        span {
-          position: absolute;
-          right: -35px;
-          top: -8px;
-          color: grey;
-          width: $main-radius;
-        }
-      }
+  }
 
-      .dropdown-toggle {
-        &::after {
-          border: none;
-        }
-      }
+  .dropdown-toggle-indicator {
+    position: relative;
+    width: 0;
+    display: none;
 
-      .dropdown-toggle:first-child {
-        display: flex;
-        align-items: center;
-        padding: 5px 7px;
-        border-radius: $main-radius;
-      }
+    span {
+      position: absolute;
+      right: -35px;
+      top: -8px;
+      color: grey;
+      width: $main-radius;
+    }
+  }
 
-      img {
-        @include avatar(34px);
+  .dropdown-toggle {
+    &::after {
+      border: none;
+    }
+  }
 
-        margin-right: 10px;
-      }
+  .dropdown-toggle:first-child {
+    display: flex;
+    align-items: center;
+    padding: 5px 7px;
+    border-radius: $main-radius;
+  }
+
+  img {
+    @include avatar(34px);
 
-      .logged-in-info {
-        max-width: 105px;
+    margin-right: 10px;
+  }
+}
 
-        flex-grow: 1;
+.logged-in-info {
+  max-width: 105px;
 
-        .logged-in-display-name,
-        .logged-in-username {
-          @include ellipsis;
-        }
+  flex-grow: 1;
 
-        .logged-in-display-name {
-          font-size: 16px;
-          font-weight: $font-semibold;
-          color: pvar(--menuForegroundColor);
+  .logged-in-display-name,
+  .logged-in-username {
+    @include ellipsis;
+  }
 
-          @include disable-default-a-behaviour;
-        }
+  .logged-in-display-name {
+    font-size: 16px;
+    font-weight: $font-semibold;
+    color: pvar(--menuForegroundColor);
 
-        .logged-in-username {
-          font-size: 13px;
-          color: #C6C6C6;
-          margin-top: 3px;
-        }
-      }
-    }
+    @include disable-default-a-behaviour;
+  }
 
-    my-notification {
-      margin-left: auto;
-      margin-right: 15px;
-    }
+  .logged-in-username {
+    font-size: 13px;
+    color: #C6C6C6;
+    margin-top: 3px;
   }
+}
 
-  .logged-in-menu {
-    display: flex;
-    flex-direction: column;
-    align-items: flex-start;
-    border-top: 1px solid var(--greyForegroundColor);
-    line-height: $line-height-normal;
+.logged-in-menu {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  border-top: 1px solid var(--greyForegroundColor);
+  line-height: $line-height-normal;
 
-    a {
-      @include menu-link;
-      @include disable-default-a-behaviour;
+  a {
+    @include menu-link;
+    @include disable-default-a-behaviour;
 
-      $icon-size: 13px;
-      $additional-margin: ($menu-link-icon-size - $icon-size) / 2;
+    $icon-size: 13px;
+    $additional-margin: ($menu-link-icon-size - $icon-size) / 2;
 
-      font-size: 14px;
-      width: 100%;
-      min-height: 35px;
+    font-size: 14px;
+    width: 100%;
+    min-height: 35px;
 
-      my-global-icon {
-        width: $icon-size;
-        height: $icon-size;
+    my-global-icon {
+      width: $icon-size;
+      height: $icon-size;
 
-        // Keep aligned with other icons
-        margin-left: $additional-margin;
+      // Keep aligned with other icons
+      margin-left: $additional-margin;
 
-        &[iconName="channel"] {
-          margin-top: -2px;
-        }
+      &[iconName="channel"] {
+        margin-top: -2px;
       }
+    }
 
-      &.active,
-      &:hover,
-      &:focus-visible {
-        my-global-icon {
-          @include apply-svg-color(var(--menuForegroundColor));
-        }
+    &.active,
+    &:hover,
+    &:focus-visible {
+      my-global-icon {
+        @include apply-svg-color(var(--menuForegroundColor));
       }
+    }
 
-      &.active {
-        $border-left-width: 4px;
+    &.active {
+      $border-left-width: 4px;
 
-        font-weight: $font-semibold;
-        border-left: $border-left-width solid var(--mainColor);
+      font-weight: $font-semibold;
+      border-left: $border-left-width solid var(--mainColor);
 
-        my-global-icon {
-          margin-left: $additional-margin - $border-left-width;
-        }
+      my-global-icon {
+        margin-left: $additional-margin - $border-left-width;
       }
     }
   }
@@ -333,50 +334,48 @@ menu {
     flex-direction: column;
     padding: 0 $menu-lateral-padding;
   }
+}
 
-  $footer-links-base-opacity: .8;
-
-  .footer-links {
-    &, > div {
-      display: flex;
-      flex-wrap: wrap;
-    }
+.footer-links {
+  &, > div {
+    display: flex;
+    flex-wrap: wrap;
+  }
 
-    a, span[role=button] {
-      display: inline-block;
-      text-decoration: none;
-      color: pvar(--menuForegroundColor);
-      opacity: $footer-links-base-opacity;
+  a, span[role=button] {
+    display: inline-block;
+    text-decoration: none;
+    color: pvar(--menuForegroundColor);
+    opacity: $footer-links-base-opacity;
+    white-space: nowrap;
+    font-size: 90%;
+    font-weight: 500;
+    line-height: 1.4rem;
+    margin-right: 8px;
+
+    &.inline-global-icon {
+      display: inline-flex;
+      align-items: center;
       white-space: nowrap;
-      font-size: 90%;
-      font-weight: 500;
-      line-height: 1.4rem;
-      margin-right: 8px;
-
-      &.inline-global-icon {
-        display: inline-flex;
-        align-items: center;
-        white-space: nowrap;
-        height: 1.4rem;
-
-        my-global-icon {
-          @include apply-svg-color(pvar(--menuForegroundColor));
-
-          display: flex;
-          width: auto;
-          height: 90%;
-          margin-right: .2rem;
-        }
+      height: 1.4rem;
+
+      my-global-icon {
+        @include apply-svg-color(pvar(--menuForegroundColor));
+
+        display: flex;
+        width: auto;
+        height: 90%;
+        margin-right: .2rem;
       }
     }
   }
+}
 
-  .footer-copyleft small a {
-    @include disable-default-a-behaviour;
+.footer-copyleft small a {
+  @include disable-default-a-behaviour;
 
-    color: pvar(--menuForegroundColor);
-    opacity: $footer-links-base-opacity - .2;
-  }
+  color: pvar(--menuForegroundColor);
+  opacity: $footer-links-base-opacity - .2;
 }
 
 .dropdown {
index 5a8adf7264f53132908fff1fa4034610f5f69c09..498adfeffd24c1d20697fe25bfab2d0814045f18 100644 (file)
@@ -15,7 +15,7 @@
 
       <li i18n *ngIf="!about.instance.administrator">Who you are</li>
       <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li>
-      <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li>
+      <li i18n *ngIf="!about.instance.businessModel">How you plan to pay for keeping your instance running</li>
 
       <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li>
       <li i18n *ngIf="!about.instance.terms">Instance terms</li>
index e7441e4c19ee2dc0f71aa3acc8caa0c4bfcd32c1..9f252f299556f13538de7661456ab763d07434a5 100644 (file)
 
     <button
       *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
-      class="btn btn-outline-secondary" i18n-title title="Copy"
+      class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
     >
-      <span class="glyphicon glyphicon-copy"></span>
+      <span class="glyphicon glyphicon-duplicate"></span>
+      Copy
     </button>
   </div>
 </div>
index 2890670e5cda5881109cb54a3f31487b47be147c..8482b9deab2ed81199ca459dedd3a00c7d88e703 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, forwardRef, Input } from '@angular/core'
+import { Component, forwardRef, HostListener, Input } from '@angular/core'
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
 import { SelectOptionsItem } from '../../../../types/select-options-item.model'
 
@@ -26,6 +26,13 @@ export class SelectOptionsComponent implements ControlValueAccessor {
 
   propagateChange = (_: any) => { /* empty */ }
 
+  // Allow plugins to update our value
+  @HostListener('change', [ '$event.target' ])
+  handleChange (event: any) {
+    this.writeValue(event.value)
+    this.onModelChange()
+  }
+
   writeValue (id: number | string) {
     this.selectedId = id
   }
diff --git a/client/src/app/shared/shared-main/angular/autofocus.directive.ts b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
new file mode 100644 (file)
index 0000000..5f087d7
--- /dev/null
@@ -0,0 +1,12 @@
+import { AfterViewInit, Directive, ElementRef } from '@angular/core'
+
+@Directive({
+  selector: '[autofocus]'
+})
+export class AutofocusDirective implements AfterViewInit {
+  constructor (private host: ElementRef) { }
+
+  ngAfterViewInit () {
+    this.host.nativeElement.focus()
+  }
+}
index 29f8b3650586c36170fa571b2f38b2f2ecd7871c..8ea47bb33fdb4eb00282507700aba360f7a7e70f 100644 (file)
@@ -1,3 +1,4 @@
+export * from './autofocus.directive'
 export * from './bytes.pipe'
 export * from './duration-formatter.pipe'
 export * from './from-now.pipe'
index 3ddaffbdf35f2cc8b51e70fa532309326332b71d..4fe3b964d64770442cc19027df9da73d474cff1e 100644 (file)
@@ -27,7 +27,9 @@ export class AuthInterceptor implements HttpInterceptor {
                  catchError((err: HttpErrorResponse) => {
                    if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') {
                      return this.handleTokenExpired(req, next)
-                   } else if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
+                   }
+
+                   if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
                      return this.handleNotAuthenticated(err)
                    }
 
index 9d550996d0d8fe3d81280a40a475ff8b42540c4b..3e21d491a21acd2843f7cad5264f69138466a5c4 100644 (file)
@@ -19,6 +19,7 @@ import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
 import { SharedGlobalIconModule } from '../shared-icons'
 import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account'
 import {
+  AutofocusDirective,
   BytesPipe,
   DurationFormatterPipe,
   FromNowPipe,
@@ -71,6 +72,7 @@ import { VideoChannelService } from './video-channel'
     NumberFormatterPipe,
     BytesPipe,
     DurationFormatterPipe,
+    AutofocusDirective,
 
     InfiniteScrollerDirective,
     PeerTubeTemplateDirective,
@@ -125,6 +127,7 @@ import { VideoChannelService } from './video-channel'
     BytesPipe,
     NumberFormatterPipe,
     DurationFormatterPipe,
+    AutofocusDirective,
 
     InfiniteScrollerDirective,
     PeerTubeTemplateDirective,
index 1211995fd3a89e98ce3e02b43d9e9362f55d4df9..88a4811da0681ec350e267bd1a789f3dcc3ccd1e 100644 (file)
@@ -6,6 +6,7 @@ import {
   AbuseState,
   ActorInfo,
   FollowState,
+  PluginType,
   UserNotification as UserNotificationServer,
   UserNotificationType,
   UserRight,
@@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer {
     }
   }
 
+  plugin?: {
+    name: string
+    type: PluginType
+    latestVersion: string
+  }
+
+  peertube?: {
+    latestVersion: string
+  }
+
   createdAt: string
   updatedAt: string
 
   // Additional fields
   videoUrl?: string
   commentUrl?: any[]
+
   abuseUrl?: string
   abuseQueryParams?: { [id: string]: string } = {}
+
   videoAutoBlacklistUrl?: string
+
   accountUrl?: string
+
   videoImportIdentifier?: string
   videoImportUrl?: string
+
   instanceFollowUrl?: string
 
+  peertubeVersionLink?: string
+
+  pluginUrl?: string
+  pluginQueryParams?: { [id: string]: string } = {}
+
   constructor (hash: UserNotificationServer, user: AuthUser) {
     this.id = hash.id
     this.type = hash.type
@@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer {
       this.actorFollow = hash.actorFollow
       if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
 
+      this.plugin = hash.plugin
+      this.peertube = hash.peertube
+
       this.createdAt = hash.createdAt
       this.updatedAt = hash.updatedAt
 
@@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer {
         case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
           this.instanceFollowUrl = '/admin/follows/following-list'
           break
+
+        case UserNotificationType.NEW_PEERTUBE_VERSION:
+          this.peertubeVersionLink = 'https://joinpeertube.org/news'
+          break
+
+        case UserNotificationType.NEW_PLUGIN_VERSION:
+          this.pluginUrl = `/admin/plugins/list-installed`
+          this.pluginQueryParams.pluginType = this.plugin.type + ''
+          break
       }
     } catch (err) {
       this.type = null
index 265af8d5502825d4f2d5e94013834577551390b1..325f0eaae1bdcce6aef9eb5f00b3ea64681e6909 100644 (file)
@@ -4,7 +4,7 @@
   <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
 
     <ng-container [ngSwitch]="notification.type">
-      <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
+      <ng-container *ngSwitchCase="1"> <!-- UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION -->
         <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container>
 
         <ng-template #hasVideo>
@@ -26,7 +26,7 @@
         </ng-template>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
+      <ng-container *ngSwitchCase="5"> <!-- UserNotificationType.UNBLACKLIST_ON_MY_VIDEO -->
         <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
@@ -34,7 +34,7 @@
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
+      <ng-container *ngSwitchCase="4"> <!-- UserNotificationType.BLACKLIST_ON_MY_VIDEO -->
         <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
@@ -42,7 +42,7 @@
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS">
+      <ng-container *ngSwitchCase="3"> <!-- UserNotificationType.NEW_ABUSE_FOR_MODERATORS -->
         <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
 
         <div class="message" *ngIf="notification.videoUrl" i18n>
@@ -63,7 +63,7 @@
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE">
+      <ng-container *ngSwitchCase="15"> <!-- UserNotificationType.ABUSE_STATE_CHANGE -->
         <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
@@ -73,7 +73,7 @@
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE">
+      <ng-container *ngSwitchCase="16"> <!-- UserNotificationType.ABUSE_NEW_MESSAGE -->
         <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
@@ -81,7 +81,7 @@
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS">
+      <ng-container *ngSwitchCase="12"> <!-- UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS -->
         <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
@@ -89,7 +89,7 @@
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
+      <ng-container *ngSwitchCase="2">
         <ng-container *ngIf="notification.comment">
           <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
             <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
         </ng-container>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED">
+      <ng-container *ngSwitchCase="6"> <!-- UserNotificationType.MY_VIDEO_PUBLISHED -->
         <my-global-icon iconName="film" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
+      <ng-container *ngSwitchCase="7"> <!-- UserNotificationType.MY_VIDEO_IMPORT_SUCCESS -->
         <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
+      <ng-container *ngSwitchCase="8"> <!-- UserNotificationType.MY_VIDEO_IMPORT_ERROR -->
         <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
+      <ng-container *ngSwitchCase="9"> <!-- UserNotificationType.NEW_USER_REGISTRATION -->
         <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
+      <ng-container *ngSwitchCase="10"> <!-- UserNotificationType.NEW_FOLLOW -->
         <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
           <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
         </a>
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
+      <ng-container *ngSwitchCase="11">
         <ng-container *ngIf="notification.comment">
           <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
             <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
         </ng-container>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.NEW_INSTANCE_FOLLOWER">
+      <ng-container *ngSwitchCase="13"> <!-- UserNotificationType.NEW_INSTANCE_FOLLOWER -->
         <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
         </div>
       </ng-container>
 
-      <ng-container *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING">
+      <ng-container *ngSwitchCase="14"> <!-- UserNotificationType.AUTO_INSTANCE_FOLLOWING -->
         <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
         </div>
       </ng-container>
 
+      <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
+        <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
+
+        <div class="message" i18n>
+          <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }}
+        </div>
+      </ng-container>
+
+      <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
+        <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
+
+        <div class="message" i18n>
+            <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
+        </div>
+      </ng-container>
+
       <ng-container *ngSwitchDefault>
         <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
 
index 387c49d94ee0c4bd2e0ecf458e691d496eed9b27..d7c72235528ddef83bea36ac3ac9b82de4b096d3 100644 (file)
@@ -21,9 +21,6 @@ export class UserNotificationsComponent implements OnInit {
   notifications: UserNotification[] = []
   sortField = 'createdAt'
 
-  // So we can access it in the template
-  UserNotificationType = UserNotificationType
-
   componentPagination: ComponentPagination
 
   onDataSubject = new Subject<any[]>()
@@ -48,7 +45,7 @@ export class UserNotificationsComponent implements OnInit {
   }
 
   loadNotifications (reset?: boolean) {
-    this.userNotificationService.listMyNotifications({
+    const options = {
       pagination: this.componentPagination,
       ignoreLoadingBar: this.ignoreLoadingBar,
       sort: {
@@ -56,7 +53,9 @@ export class UserNotificationsComponent implements OnInit {
         // if we order by creation date, we want DESC. all other fields are ASC (like unread).
         order: this.sortField === 'createdAt' ? -1 : 1
       }
-    })
+    }
+
+    this.userNotificationService.listMyNotifications(options)
         .subscribe(
           result => {
             this.notifications = reset ? result.data : this.notifications.concat(result.data)
index 4608e93e755552a40a46dc761a86eecf75849756..0e659fbe2c87d164559e64d9929e5e21b76ee65a 100644 (file)
@@ -36,7 +36,7 @@
         <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
         <div class="input-group-append" *ngIf="!isConfidentialVideo()">
           <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
-            <span class="glyphicon glyphicon-copy"></span>
+            <span class="glyphicon glyphicon-duplicate"></span>
           </button>
         </div>
       </div>
index 90f4daf7c9d80ab4986b52966eb5822c80292dcd..e0b7b51ff25a8b1b72c0171e1a09dca5890cdbe3 100644 (file)
@@ -1,7 +1,9 @@
 import { mapValues, pick } from 'lodash-es'
+import { pipe } from 'rxjs'
+import { tap } from 'rxjs/operators'
 import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
-import { AuthService, Notifier } from '@app/core'
-import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { AuthService, HooksService, Notifier } from '@app/core'
+import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
 import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
 import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main'
 
@@ -16,7 +18,7 @@ type FileMetadata = { [key: string]: { label: string, value: string }}
 export class VideoDownloadComponent {
   @ViewChild('modal', { static: true }) modal: ElementRef
 
-  downloadType: 'direct' | 'torrent' = 'torrent'
+  downloadType: 'direct' | 'torrent' = 'direct'
   resolutionId: number | string = -1
   subtitleLanguageId: string
 
@@ -26,7 +28,7 @@ export class VideoDownloadComponent {
   videoFileMetadataVideoStream: FileMetadata | undefined
   videoFileMetadataAudioStream: FileMetadata | undefined
   videoCaptions: VideoCaption[]
-  activeModal: NgbActiveModal
+  activeModal: NgbModalRef
 
   type: DownloadType = 'video'
 
@@ -38,7 +40,8 @@ export class VideoDownloadComponent {
     private notifier: Notifier,
     private modalService: NgbModal,
     private videoService: VideoService,
-    private auth: AuthService
+    private auth: AuthService,
+    private hooks: HooksService
   ) {
     this.bytesPipe = new BytesPipe()
     this.numbersPipe = new NumberFormatterPipe(this.localeId)
@@ -64,7 +67,12 @@ export class VideoDownloadComponent {
 
     this.resolutionId = this.getVideoFiles()[0].resolution.id
     this.onResolutionIdChange()
+
     if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
+
+    this.activeModal.shown.subscribe(() => {
+      this.hooks.runAction('action:modal.video-download.shown', 'common')
+    })
   }
 
   onClose () {
@@ -88,6 +96,7 @@ export class VideoDownloadComponent {
     if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
 
     await this.hydrateMetadataFromMetadataUrl(this.videoFile)
+    if (!this.videoFile.metadata) return
 
     this.videoFileMetadataFormat = this.videoFile
       ? this.getMetadataFormat(this.videoFile.metadata.format)
@@ -201,7 +210,7 @@ export class VideoDownloadComponent {
 
   private hydrateMetadataFromMetadataUrl (file: VideoFile) {
     const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
-    observable.subscribe(res => file.metadata = res)
+      .pipe(tap(res => file.metadata = res))
 
     return observable.toPromise()
   }
index 3a4e58df15f76fedb2f6840798802248bece1cf3..16526d338ee38f7d134ab58016d2969bb2a96dcd 100644 (file)
@@ -1,6 +1,6 @@
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
   <defs/>
-  <g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-width="2">
+  <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-width="2">
     <path stroke-linejoin="round" d="M8 17H5h0a4 4 0 111-7.9v-.6a5.5 5.5 0 0110.8-1.4A5 5 0 0123 12a5 5 0 01-5 5h-2"/>
     <path d="M12 13v8"/>
     <path stroke-linejoin="round" d="M15 20l-3 3-3-3"/>
diff --git a/client/src/assets/images/feather/subscriptions.svg b/client/src/assets/images/feather/subscriptions.svg
deleted file mode 100644 (file)
index c721635..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
-  <defs/>
-  <defs>
-    <linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="97.33%">
-      <stop stop-color="#000" offset="0%"/>
-      <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
-    </linearGradient>
-    <linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="97.86%">
-      <stop stop-color="#000" offset="0%"/>
-      <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
-    </linearGradient>
-  </defs>
-  <g fill="none" fill-rule="evenodd">
-    <circle cx="12" cy="10" r="3" fill="#000"/>
-    <path fill="url(#a)" fill-rule="nonzero" d="M16.39 13.85A5.68 5.68 0 0018 10c0-3.26-2.74-6-6-6s-6 2.74-6 6c0 1.42.58 2.7 1.62 3.85a.5.5 0 00.74-.67A4.7 4.7 0 017 10c0-2.7 2.3-5 5-5s5 2.3 5 5a4.7 4.7 0 01-1.36 3.18.5.5 0 10.75.67z"/>
-    <path fill="url(#b)" fill-rule="nonzero" d="M17.57 18.3A9.99 9.99 0 0012 0a10 10 0 00-5.56 18.31 1 1 0 101.11-1.66 7.99 7.99 0 118.9 0 1 1 0 101.12 1.66z"/>
-    <path fill="#000" d="M9.33 15.98A1.64 1.64 0 0111 14h2c1.1 0 1.85.88 1.67 1.98l-1 6.04c-.1.54-.61.98-1.17.98h-1c-.55 0-1.07-.43-1.16-.98l-1.01-6.04z"/>
-  </g>
-</svg>
index 8fd1d0ba83bae237973707fe0bc2736679534424..204136f0bd1f4903e848092611c79ecaaf8721bb 100644 (file)
@@ -1,7 +1,7 @@
 <svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.2)" viewBox="0 0 200 200">
   <defs/>
-  <path stroke="#000" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/>
-  <path stroke="#000" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/>
-  <path stroke="#000" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/>
-  <path stroke="#000" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/>
+  <path stroke="currentColor" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/>
+  <path stroke="currentColor" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/>
+  <path stroke="currentColor" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/>
+  <path stroke="currentColor" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/>
 </svg>
index 1d1d827845cbcd9c6408bb3ce600e0eaabe907b1..8a4869f128f07de457c79e297467760f4d7968b6 100644 (file)
@@ -1,5 +1,5 @@
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);">\r
-  <path fill="#000" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/>\r
+  <path fill="currentColor" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/>\r
   <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/>\r
   <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/>\r
   <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/>\r
index 0099e627d8e32868b6df97f43297e7bfd1457649..30ab665e74c827546436a0e2d07660dde46b49d5 100644 (file)
@@ -1,20 +1,17 @@
-<?xml version="1.0" encoding="utf-8"?>\r
-<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->\r
-<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\r
-        viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">\r
-<style type="text/css">\r
-       .st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}\r
-       .st1{fill:#211F20;}\r
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">\r
+       <style type="text/css">\r
+       .st0{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}\r
+       .st1{fill:currentColor;}\r
 </style>\r
-<line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5"/>\r
-<line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5"/>\r
-<g>\r
+       <line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5" />\r
+       <line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5" />\r
        <g>\r
                <g>\r
-                       <path class="st1" d="M2,2.6V12l6.9-4.3"/>\r
-                       <path class="st1" d="M2,12v9.4l6.9-5.2"/>\r
-                       <path class="st1" d="M8.9,7.7v8.6l6.9-4.3"/>\r
+                       <g>\r
+                               <path class="st1" d="M2,2.6V12l6.9-4.3" />\r
+                               <path class="st1" d="M2,12v9.4l6.9-5.2" />\r
+                               <path class="st1" d="M8.9,7.7v8.6l6.9-4.3" />\r
+                       </g>\r
                </g>\r
        </g>\r
-</g>\r
 </svg>\r
index 7ec77b8511b5814dfff3d1457b8a5f47418d038f..4be495e83764005081448cf21b4ef34650a0c5db 100644 (file)
@@ -1,5 +1,5 @@
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.7 426.7">
   <defs/>
-  <path fill="#000" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/>
-  <path fill="#000" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
+  <path fill="currentColor" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/>
+  <path fill="currentColor" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
 </svg>
index 66280e18d4ebe2a9d9fefc413bce1e2c1b4ecf86..be3f58c246bcd61585530280246ee593aa27b67a 100644 (file)
@@ -6,9 +6,9 @@
     <g transform="translate(2.669496,27.625894)">
         <g transform="matrix(0.1,0,0,-0.1,0,511)">
             <path d="m 3744.3542,4564.3712 c -217.4,-34.2 -520.3,-200.3 -693.7,-376.2 -263.8,-263.8 -388.4,-571.6 -388.4,-952.6 0,-256.5 44,-437.2 173.4,-684 75.7,-144.1 197.9,-280.9 747.5,-842.7 1106.5,-1133.40001 1138.2,-1165.20001 1253,-1194.50001 188.1,-51.3 214.9,-29.3 1162.7,938.00001 498.3,508.1 911.1,950.2 962.4,1030.8 263.8,415.3 283.3,964.9 48.8,1409.4 -180.8,342 -581.3,620.4 -972.2,676.6 -332.2,48.9 -671.7,-36.6 -967.3,-236.9 l -156.3,-109.9 -119.7,87.9 c -158.8,117.2 -351.8,202.7 -554.5,244.3 -183.1,39.1 -295.4,41.6 -495.7,9.8 z"
-                  fill="#000"/>
+                  fill="currentColor"/>
             <path d="m 7991.4051,47.633899 c -39.1,-19.5 -473.9,-437.299999 -964.9,-925.800029 l -891.6,-891.59997 h -830.5 c -757.2,0 -837.8,4.9 -913.6,44 -207.6,112.4 -227.2,415.2 -39.1,561.8 66,53.7 83,53.7 950.2,53.7 989.3,0 1008.8,2.5 1094.3,173.49997 56.2,105 56.2,317.50003 4.9,427.50003 -83.1,175.9 4.8,168.5 -1915.1,168.5 h -1722 l -173.4,-63.5 c -95.3,-34.2 -232.1,-102.6 -305.3,-151.5 -73.3,-48.9 -442.1,-400.60003 -823.2,-779.2 l -688.80006,-693.7 664.40006,-647.3 c 366.4,-354.2 779.2,-754.8 918.4,-889.1 l 251.6,-241.8 481.2,481.2 481.2,481.2 h 1487.6 c 1294.6,0 1494.9,4.9 1565.8,39.1 58.6,26.9 339.6,368.8 1028.4,1248.2 522.8,666.89997 964.9,1243.3 982,1284.9 41.5,92.8 2.5,212.499999 -95.3,297.999999 -66,53.7 -95.3,61.1 -273.6,61.1 -132,-0.1 -224.8,-12.3 -273.6,-39.2 z"
-                  fill="#000"/>
+                  fill="currentColor"/>
         </g>
     </g>
 </svg>
index 5ffed18da0dc0d78e8aec5f0df4b093cb6fa3449..6bcaeb9be380aa7b04b8b1e43cc80f626ddb2409 100644 (file)
@@ -1,7 +1,7 @@
 <svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.1)" viewBox="0 0 24 24">
   <defs/>
   <g class="layer">
-    <path fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/>
+    <path fill="#fff" fill-rule="evenodd" stroke="currentColor" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/>
     <path d="M8.7 14.7a.7.7 0 01-.5-1.2l2.9-3H8.7a.7.7 0 010-1.3h4a.7.7 0 01.5 1.2l-4 4a.7.7 0 01-.5.3zM11.7 8.6h-2a.7.7 0 110-1.4h2a.7.7 0 010 1.4z"/>
   </g>
 </svg>
index 7047f6e031c08a6156075f82d6232aeb21926229..75dc91d7a01610397c5380b264b40be465f5abf6 100644 (file)
@@ -9,6 +9,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
   animation: spin .7s infinite linear;
 }
 
+.glyphicon-duplicate {
+  font-size: 70%;
+}
+
 .flex-auto {
   flex: auto;
 }
index ca11488cb0c117c51046b4eb2424fb70fe273e30..cf5ac8fd880b5fdb85ce265fccc7871913569936 100644 (file)
@@ -41,9 +41,6 @@
   word-break: break-word;
   word-wrap: break-word;
   overflow-wrap: break-word;
-  -webkit-hyphens: auto;
-  -ms-hyphens: auto;
-  -moz-hyphens: auto;
   hyphens: auto;
 }
 
   ::ng-deep .material {
     color: $color;
   }
-
-  ::ng-deep svg {
-    path[fill="#000"],
-    g[fill="#000"],
-    rect[fill="#000"],
-    circle[fill="#000"],
-    polygon[fill="#000"] {
-      fill: $color;
-    }
-
-    path[stroke="#000"],
-    g[stroke="#000"],
-    rect[stroke="#000"],
-    circle[stroke="#000"],
-    polygon[stroke="#000"] {
-      stroke: $color;
-    }
-
-    stop[stop-color="#000"] {
-      stop-color: $color;
-    }
-  }
 }
 
 @mixin fill-svg-color ($color) {
index 0144e89fb4f2d6cc644c8b8adfc4324326a3a0c3..81aacf1d769b0980e6b7a198b7c95edf5646c11a 100644 (file)
@@ -43,10 +43,6 @@ body {
     }
   }
 
-  .vjs-button > .vjs-icon-placeholder::before {
-    line-height: $control-bar-height;
-  }
-
   .vjs-volume-level::before {
     content: ''; /* Remove Circle From Progress Bar */
   }
@@ -242,8 +238,19 @@ body {
       @include disable-outline;
 
       cursor: pointer;
-      font-size: $play-control-font-size;
       width: 2em;
+
+      .vjs-icon-placeholder {
+        line-height: $control-bar-height;
+        position: relative;
+        top: -1px;
+
+        &::before {
+          font-size: 28px;
+          line-height: unset;
+          position: relative;
+        }
+      }
     }
 
     .vjs-time-control {
@@ -375,7 +382,6 @@ body {
     .vjs-mute-control {
       @include disable-outline;
 
-      line-height: $control-bar-height;
       padding: 0;
       width: 30px;
 
index cf4bc6f03d103c34b455d115206c2535cc995bbc..c8727002731fbf0be6a1ebbafff9a5fc6aa5ae37 100644 (file)
@@ -783,6 +783,8 @@ export class PeerTubeEmbed {
 
       showModal: unimplemented,
 
+      getServerConfig: unimplemented,
+
       markdownRenderer: {
         textMarkdownToHTML: unimplemented,
         enhancedMarkdownToHTML: unimplemented
index e3c6d803d476d165f3d8464a00f6f9efa307c471..7e5356a2b4fe9f35d9c2908b4bd0eb40b1b71f0f 100644 (file)
@@ -1,5 +1,6 @@
 import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
 import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
+import { ServerConfig } from '@shared/models/server'
 
 export type RegisterClientOptions = {
   registerHook: (options: RegisterClientHookOptions) => void
@@ -16,6 +17,8 @@ export type RegisterClientHelpers = {
 
   getSettings: () => Promise<{ [ name: string ]: string }>
 
+  getServerConfig: () => Promise<ServerConfig>
+
   notifier: {
     info: (text: string, title?: string, timeout?: number) => void,
     error: (text: string, title?: string, timeout?: number) => void,
index 79ab1e2a8b0b468b86269112cb621a680b9b1e43..75548e83f904d31d1d63a4ad74d644f92fcd3bda 100644 (file)
@@ -2,23 +2,23 @@
 # yarn lockfile v1
 
 
-"@angular-devkit/architect@0.1102.2":
-  version "0.1102.2"
-  resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1102.2.tgz#3b3eb654ae7c8c204b248bba76982ce8de2f7b6c"
-  integrity sha512-FE7DeT13elqDlELF23QqvEFnT2BkxeC5t31/QW85IN/OR5Tf/q7XEpj7giJXyzKFQ60M3ZzbznZyRz0EqtfaBQ==
+"@angular-devkit/architect@0.1102.5":
+  version "0.1102.5"
+  resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1102.5.tgz#431df157af0c6477e5951f64ff12f3d5d5f075ee"
+  integrity sha512-lVc6NmEAZZPzvc18GzMFLoxqKKvPlNOg4vEtFsFldZmrydLJJGFi4KAs2WaJd8qVR1XuY4el841cjDQAJSq6sQ==
   dependencies:
-    "@angular-devkit/core" "11.2.2"
+    "@angular-devkit/core" "11.2.5"
     rxjs "6.6.3"
 
 "@angular-devkit/build-angular@^0.1102.2":
-  version "0.1102.2"
-  resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1102.2.tgz#c850818fd8bb4dd4fda6288390868475c4b3236e"
-  integrity sha512-AjnvHrzkYTzDGzp0r5RmGoP9fyZXtaVFo0598PRusi1oWp1sW6B5FKPWw896iREOlotRXw3dsjqrGwbMcz0qyg==
-  dependencies:
-    "@angular-devkit/architect" "0.1102.2"
-    "@angular-devkit/build-optimizer" "0.1102.2"
-    "@angular-devkit/build-webpack" "0.1102.2"
-    "@angular-devkit/core" "11.2.2"
+  version "0.1102.5"
+  resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1102.5.tgz#7db51dfc33a8683458fa714d434f8c09fdc1f648"
+  integrity sha512-iAq/KbRq6kuA17rQZ67/0zQHEzpC9RzvtMZQ3wiiFsOmW5AIV5scjP7e6dn+F6vXZA44X4gCH5AUUkOLXyEtfg==
+  dependencies:
+    "@angular-devkit/architect" "0.1102.5"
+    "@angular-devkit/build-optimizer" "0.1102.5"
+    "@angular-devkit/build-webpack" "0.1102.5"
+    "@angular-devkit/core" "11.2.5"
     "@babel/core" "7.12.10"
     "@babel/generator" "7.12.11"
     "@babel/plugin-transform-async-to-generator" "7.12.1"
@@ -26,8 +26,9 @@
     "@babel/preset-env" "7.12.11"
     "@babel/runtime" "7.12.5"
     "@babel/template" "7.12.7"
+    "@discoveryjs/json-ext" "0.5.2"
     "@jsdevtools/coverage-istanbul-loader" "3.0.5"
-    "@ngtools/webpack" "11.2.2"
+    "@ngtools/webpack" "11.2.5"
     ansi-colors "4.1.1"
     autoprefixer "10.2.4"
     babel-loader "8.2.2"
     webpack-subresource-integrity "1.5.2"
     worker-plugin "5.0.0"
 
-"@angular-devkit/build-optimizer@0.1102.2":
-  version "0.1102.2"
-  resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1102.2.tgz#a306fee0bc648983405320953f05ad1fc60b6b84"
-  integrity sha512-TCWWqAe+pWZzLp/g2gG8Z5NC8JSgDNfyEuMBWxEUfo1Sm3BluXoz0BbmnietuhXJZ+fPAp9rLLzEGZlHvOlmOA==
+"@angular-devkit/build-optimizer@0.1102.5":
+  version "0.1102.5"
+  resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1102.5.tgz#5c17d82a8c4f03ec0a14110838c2c3da6cb24dfd"
+  integrity sha512-ujTwrevgMRNyWir4IdnJEdDRkVSLqugRpL6cU9OeqGn6Bu+zEzZQokLkMZvbw00eEKlf5Siej4hEeF1Hnx+LUA==
   dependencies:
     loader-utils "2.0.0"
     source-map "0.7.3"
     tslib "2.1.0"
-    typescript "4.1.3"
+    typescript "4.1.5"
     webpack-sources "2.2.0"
 
-"@angular-devkit/build-webpack@0.1102.2":
-  version "0.1102.2"
-  resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1102.2.tgz#f48501426a5d01b0610dafce33b4eb84d07181e6"
-  integrity sha512-59CBbwbdN8lI5/whuNeAZHRJxPlOmDc5ux8aJJNwWI9w54fz0ut/MLT3iuPk+WZuKlGdpS1sGkObfZwWen5kIQ==
+"@angular-devkit/build-webpack@0.1102.5":
+  version "0.1102.5"
+  resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1102.5.tgz#e111acf7c0cbed761ae382089052a5c2dee71d96"
+  integrity sha512-VMsi+mFwgPUQi7eEc2oKcf7X0xD0R1xfoguLS/+HGy3sfh+b7oJy3BU4+TRzDPBtGj6vWvENK2rwHFN3cBWvxA==
   dependencies:
-    "@angular-devkit/architect" "0.1102.2"
-    "@angular-devkit/core" "11.2.2"
+    "@angular-devkit/architect" "0.1102.5"
+    "@angular-devkit/core" "11.2.5"
     rxjs "6.6.3"
 
-"@angular-devkit/core@11.2.2":
-  version "11.2.2"
-  resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.2.tgz#c6b40f941b24d2af447831fc958b744316cd7d87"
-  integrity sha512-LUDO1AdIjereiMh0j5p9xJcdr9ifhbWCPxlZqfu5wHzUfhCx9gO2Lvjp6rZXQ3OedXg5IZUnyxHlzkszQOsgiw==
+"@angular-devkit/core@11.2.5":
+  version "11.2.5"
+  resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.5.tgz#f9ba8288a6cc388808ee639c383dada50d64d06a"
+  integrity sha512-DRFvEHRKoC+hTwcOAJqLe6UQa+bpXc/1IGCMHWEbuply0KIFIGQOlmaYwFZKixz3HdFZlmoCMcAVkAXvyaWVsQ==
   dependencies:
     ajv "6.12.6"
     fast-json-stable-stringify "2.1.0"
     rxjs "6.6.3"
     source-map "0.7.3"
 
-"@angular-devkit/schematics@11.2.2":
-  version "11.2.2"
-  resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.2.tgz#0c8c4b98a30f00649dcbb7794d3783b9a067209f"
-  integrity sha512-6bIxMwafz/+lwdtcshwOuFfhxTMU4RLma1uxBS34DXupMauPGl0IIXAy5cK9dXPlHLxuGsjeBiOM6eq033RLgw==
+"@angular-devkit/schematics@11.2.5":
+  version "11.2.5"
+  resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.5.tgz#ddcb966f3f1dc910e55f03067036f1f6a01b8222"
+  integrity sha512-7RoWgpMvhljPhW9CMz1EtqkwNnGpnsPyy0N29ClHPUq+o8wLR0hvbLBDz1fKSF7j1AwRccaQSNTj8KWsjzQJLQ==
   dependencies:
-    "@angular-devkit/core" "11.2.2"
+    "@angular-devkit/core" "11.2.5"
     ora "5.3.0"
     rxjs "6.6.3"
 
 "@angular/animations@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.2.3.tgz#518183e5f7b8c3b304020ea86d12cc3216142cc9"
-  integrity sha512-Z6sHIeTeeZrRAW83NI7FO7THF50cPCFkkuvVah3qmCqopY6FuoHKUBEENyGzQGH69LbGFYhEppY8KM/6JtVF6Q==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.2.6.tgz#36935bc0fe33f1486ed889f8b5e12915858ccf5a"
+  integrity sha512-fci034QakkoIrFeY/uOmDvf6AupZ7ziU1FlBMs/wn4HOqwsPCofpawvFQnfj5nez1+KM5JOJ1VHmZKJupkWfgw==
   dependencies:
     tslib "^2.0.0"
 
 "@angular/cdk@^11.0.0":
-  version "11.2.2"
-  resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-11.2.2.tgz#f541069db3f5705d8c064138f6cd94568fe1b658"
-  integrity sha512-p3lRDPlnOuJtLWEd020QOyn0ERyc1LF7OLi90hTdzMMxe9fT3v6sQJVRs8jIY3NTmpIm/pNDGi77+1/vKerLPQ==
+  version "11.2.5"
+  resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-11.2.5.tgz#e0cce8b28ca635b6151b834c6e1c4bc0a8dd7c04"
+  integrity sha512-ugalSDLME5E9JlxcRR8RGlOYlaV6rIzxOVQrGRBzY2tdhMT4Ng+BFtCkq1K88AU1sTLHq54xg9Xkfn7b5W2kiA==
   dependencies:
     tslib "^2.0.0"
   optionalDependencies:
     parse5 "^5.0.0"
 
 "@angular/cli@^11.1.2":
-  version "11.2.2"
-  resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.2.2.tgz#ca56894f1a4d1f4e411408b8185b711614c3195a"
-  integrity sha512-rOVBzDzrMuOgJY43O46/7yYbncx0egGfr+DMJDQdazePGH1H3INN/eA9gkVcVK53ztCYb9X1sbZKOs9TUhF6nw==
-  dependencies:
-    "@angular-devkit/architect" "0.1102.2"
-    "@angular-devkit/core" "11.2.2"
-    "@angular-devkit/schematics" "11.2.2"
-    "@schematics/angular" "11.2.2"
-    "@schematics/update" "0.1102.2"
+  version "11.2.5"
+  resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.2.5.tgz#3cf3e6432db41cebb364da2dcf3d44588535a34a"
+  integrity sha512-GIwK8l6wtg/++8aDYW++LSf7v1uqDtB6so2rPjNlOm7oYk5iqM73KaorQb/1A52oxWE3IRSJLNQaSyUlWvHvSA==
+  dependencies:
+    "@angular-devkit/architect" "0.1102.5"
+    "@angular-devkit/core" "11.2.5"
+    "@angular-devkit/schematics" "11.2.5"
+    "@schematics/angular" "11.2.5"
+    "@schematics/update" "0.1102.5"
     "@yarnpkg/lockfile" "1.1.0"
     ansi-colors "4.1.1"
     debug "4.3.1"
     uuid "8.3.2"
 
 "@angular/common@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.3.tgz#e71d645fb6bdef9463f23a551cc072ef276c1d84"
-  integrity sha512-51gVmr942SZtAFmhVfp7/3fcTQ+Tia7UxWjv6iUtYF3oCvTWbo/J1zki2VNSfmMNKJV8MaMq6XUw8UWbHA0sgQ==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.6.tgz#9985b9f1b3d82588f85bb74b1967749b0134d017"
+  integrity sha512-q1yR6bktd5p987gLEKiFY4CrHcmBxks9R6GcdgzGneQsucDtGESzEKdcJ0uaMXE+9teS+fQy5GvXel6DlA/J+w==
   dependencies:
     tslib "^2.0.0"
 
 "@angular/compiler-cli@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.2.3.tgz#5307215b9aa6e32d772906fd3b2960ba03a7565d"
-  integrity sha512-ObQVI6q2c0VTWbsDnWJDdUZv2Jz/u1jiQNcrdtu/rjtJARaldEno9dMakN838Q6Nw4FzKUO6uYZXmnvKCUjfxQ==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.2.6.tgz#456844d71079df3ca3f025aaa9d9df9ed5a79006"
+  integrity sha512-1OC8UkySaLzaw3aSrm8A6SA88CxQAdA4ffaOhBLE/Ee6CxpneVxn3ORlnccqnS8zWyEpschbootPJV56U3Azeg==
   dependencies:
     "@babel/core" "^7.8.6"
     "@babel/types" "^7.8.6"
   integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==
 
 "@angular/compiler@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.2.3.tgz#72427d57b992bf6840fb7268357a466095caf8eb"
-  integrity sha512-De8BwtSwPVYGdvQa6CDq2C1SLmB78YjS0t/KNlvfp85cl4Gb3BdjTDsKMkJXkm/3ubnIXi1BaRIsFNVTCCF70Q==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.2.6.tgz#8b69cd2f2c3bb0fbc6f95ded1ccbe20e6858daed"
+  integrity sha512-3ijsCxnCLU1V1hy4UMf9qtMz5LR+wCdVFDqktEQccN9YEkN0cNtOc8Nu9EV9/mc2tqd1Q4xSBpb2o2mvpy7AhQ==
   dependencies:
     tslib "^2.0.0"
 
   integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==
 
 "@angular/core@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.3.tgz#7dd59f35e0b2410543a61be6048c474c18a43f40"
-  integrity sha512-+G7rZj21Mcmf6nWjQ79EwomwEOVQ1WLqw6YvCXWzgJ9ZlVjLi/Sti0/jIzUpgK0E0Fn86yuXw/vgYq5kjGeOcQ==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.6.tgz#c38ee7834519d3c94e51be62156784a984cd93d2"
+  integrity sha512-lS5JOQ/Y9gbk5WiMnCp5Zyz2pRIoZ+IWLOXHU5rkQeXy0zE3eMJhw0FfpEK+X5CeSNl2EPVSPLT0MtDtbNPodg==
   dependencies:
     tslib "^2.0.0"
 
 "@angular/forms@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.2.3.tgz#57460a110e6601b50362f878fc0f67701c76dc24"
-  integrity sha512-VfyKV8IxHTclcHQmt5gjGFmKC1kGz7sdNLYsEM+M0y88Bsufh3VIhK4kspfO4nhJxVfh6HFOt1JVQ5bvo6PDlQ==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.2.6.tgz#d82a1c655754d48ec861b9b3af370e6ee1e841cb"
+  integrity sha512-0xxayXCNc8lPQhDj5q/hAcG55cmDXPSBn2cxX4V+uDSGwKU1+h2CQID6gJdBJBh5wOaeMe6h8dK2s1pRgok66A==
   dependencies:
     tslib "^2.0.0"
 
 "@angular/localize@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-11.2.3.tgz#df2e605341be53c2d4cead2d8b274415af8b3136"
-  integrity sha512-SCpum70G+MuoRitbv+u92fjDlKEbYizTosukxryh56QNa47iO3/rkVp8P2R75FDYJVJrxqoTiMGl0Q9tKdrEGA==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-11.2.6.tgz#465f2541c5bcdc396725504becaec3b96c718ec8"
+  integrity sha512-8K+SdqKqIaRlNRegDBy//VAtf2rlwoZAmqoFfiM5ujuB4SFt32NAduxDUlFGWdZD5V3iPorFBrceq04bt695AA==
   dependencies:
     "@babel/core" "7.8.3"
     glob "7.1.2"
     yargs "^16.1.1"
 
 "@angular/platform-browser-dynamic@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.3.tgz#3d7eb15ba4bcc9e227f68f13bf20258fa16efad1"
-  integrity sha512-QUPCvack7De6u5AqWcW8O6FzczwqoL858R1NlnqojnNbcnN/dCtXtKvvETEEgp/9VMwLfcuLd1BWdBJSah7f6A==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.6.tgz#26acbe4de315019ebe1e925ee826eda20c95d881"
+  integrity sha512-B56b8yPW3vAmPe4VONiBYEMZ6B1i5CUkJvit8qWWK3y7t5XrYOihIiGC0UqEDaw/uAg72GXjixspcxZWan5e9w==
   dependencies:
     tslib "^2.0.0"
 
 "@angular/platform-browser@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.2.3.tgz#0c6b537500a1c6304829fab19cf8c12daa2b48b9"
-  integrity sha512-S0IP/kGinIH18+gfnX0gLFLbP0Euw1RBceDt/WipYhUeFZZryQHvot/6KFLFtO+8rVunfrg+UyBiaK65/TT9Og==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.2.6.tgz#d2af4323275f501e279ee2aa821ac5599c11feae"
+  integrity sha512-xnYpfoqWyQOUngfbHefsZMyelCSAaxpopu/WYP0gpbYh9qJiVhsN9s6zRMqOIPueq9lmvlEuGBMgaJjeD6Ei7Q==
   dependencies:
     tslib "^2.0.0"
 
 "@angular/router@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.3.tgz#407a0797845c1cac963663537b30872e39e4b229"
-  integrity sha512-lRuEIlNj2BcBZ17mt5SZY7v80PsvlS4J6EbKSOFeSYhALM/AQnaaCdrrMlQ1WyEa5bBUabxGT9/zvahBosy2yA==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.6.tgz#5845ef37e85400aeeaf0ffe670802a58569638cc"
+  integrity sha512-n/3Sp36slXzRXUcUO9nVs3CkgFxa6U9A8GENeyxq9XQtcE912jOP4dzjDi3hlaNKbX9ijOyEh505KpqmiSYATg==
   dependencies:
     tslib "^2.0.0"
 
 "@angular/service-worker@^11.1.1":
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-11.2.3.tgz#316bfc07ccebdc5af1a9cbc825082880c551c0b9"
-  integrity sha512-/JgA4rCH2SyIK/v0+sCqNgiBEV/pXQUcUoqfm//2zfc3VwerehvF3RtRBfabtLBpdwdO5a9DZ4nX+djvTJypvw==
+  version "11.2.6"
+  resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-11.2.6.tgz#65e895a7a1dc309c9365ea801806549f7572646c"
+  integrity sha512-nZGwVhHZ6eLptnPzIjiFiktnl4ImC+4kejR3AaElTX8PgS9TykhYhgENB+ILU49bZOGMe3RVnNthgx/JkIEgjQ==
   dependencies:
     tslib "^2.0.0"
 
   dependencies:
     "@babel/highlight" "^7.12.13"
 
-"@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.0":
-  version "7.13.6"
-  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.6.tgz#11972d07db4c2317afdbf41d6feb3a730301ef4e"
-  integrity sha512-VhgqKOWYVm7lQXlvbJnWOzwfAQATd2nV52koT0HZ/LdDH0m4DUDwkKYsH+IwpXb+bKPyBJzawA4I6nBKqZcpQw==
+"@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.8":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1"
+  integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==
 
 "@babel/core@7.12.10":
   version "7.12.10"
     source-map "^0.5.0"
 
 "@babel/core@^7.7.5", "@babel/core@^7.8.6":
-  version "7.13.1"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.1.tgz#7ddd027176debe40f13bb88bac0c21218c5b1ecf"
-  integrity sha512-FzeKfFBG2rmFtGiiMdXZPFt/5R5DXubVi82uYhjGX4Msf+pgYQMCFIqFXZWs5vbIYbf14VeBIgdGI03CDOOM1w==
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559"
+  integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==
   dependencies:
     "@babel/code-frame" "^7.12.13"
-    "@babel/generator" "^7.13.0"
-    "@babel/helper-compilation-targets" "^7.13.0"
+    "@babel/generator" "^7.13.9"
+    "@babel/helper-compilation-targets" "^7.13.10"
     "@babel/helper-module-transforms" "^7.13.0"
-    "@babel/helpers" "^7.13.0"
-    "@babel/parser" "^7.13.0"
+    "@babel/helpers" "^7.13.10"
+    "@babel/parser" "^7.13.10"
     "@babel/template" "^7.12.13"
     "@babel/traverse" "^7.13.0"
     "@babel/types" "^7.13.0"
     gensync "^1.0.0-beta.2"
     json5 "^2.1.2"
     lodash "^4.17.19"
-    semver "7.0.0"
+    semver "^6.3.0"
     source-map "^0.5.0"
 
 "@babel/generator@7.12.11":
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
-"@babel/generator@^7.12.10", "@babel/generator@^7.13.0", "@babel/generator@^7.8.3":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.0.tgz#bd00d4394ca22f220390c56a0b5b85568ec1ec0c"
-  integrity sha512-zBZfgvBB/ywjx0Rgc2+BwoH/3H+lDtlgD4hBOpEv5LxRnYsm/753iRuLepqnYlynpjC3AdQxtxsoeHJoEEwOAw==
+"@babel/generator@^7.12.10", "@babel/generator@^7.13.0", "@babel/generator@^7.13.9", "@babel/generator@^7.8.3":
+  version "7.13.9"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
+  integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==
   dependencies:
     "@babel/types" "^7.13.0"
     jsesc "^2.5.1"
     "@babel/helper-explode-assignable-expression" "^7.12.13"
     "@babel/types" "^7.12.13"
 
-"@babel/helper-compilation-targets@^7.12.5", "@babel/helper-compilation-targets@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.0.tgz#c9cf29b82a76fd637f0faa35544c4ace60a155a1"
-  integrity sha512-SOWD0JK9+MMIhTQiUVd4ng8f3NXhPVQvTv7D3UN4wbp/6cAHnB2EmMaU1zZA2Hh1gwme+THBrVSqTFxHczTh0Q==
+"@babel/helper-compilation-targets@^7.12.5", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8":
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c"
+  integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==
   dependencies:
-    "@babel/compat-data" "^7.13.0"
+    "@babel/compat-data" "^7.13.8"
     "@babel/helper-validator-option" "^7.12.17"
     browserslist "^4.14.5"
-    semver "7.0.0"
+    semver "^6.3.0"
 
 "@babel/helper-create-class-features-plugin@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.0.tgz#28d04ad9cfbd1ed1d8b988c9ea7b945263365846"
-  integrity sha512-twwzhthM4/+6o9766AW2ZBHpIHPSGrPGk1+WfHiu13u/lBnggXGNYCpeAyVfNwGDKfkhEDp+WOD/xafoJ2iLjA==
+  version "7.13.11"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6"
+  integrity sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==
   dependencies:
     "@babel/helper-function-name" "^7.12.13"
     "@babel/helper-member-expression-to-functions" "^7.13.0"
   dependencies:
     "@babel/types" "^7.12.13"
 
-"@babel/helper-hoist-variables@^7.12.13":
+"@babel/helper-hoist-variables@^7.13.0":
   version "7.13.0"
   resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz#5d5882e855b5c5eda91e0cadc26c6e7a2c8593d8"
   integrity sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g==
     "@babel/traverse" "^7.13.0"
     "@babel/types" "^7.13.0"
 
-"@babel/helper-member-expression-to-functions@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz#6aa4bb678e0f8c22f58cdb79451d30494461b091"
-  integrity sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ==
+"@babel/helper-member-expression-to-functions@^7.13.0", "@babel/helper-member-expression-to-functions@^7.13.12":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72"
+  integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==
   dependencies:
-    "@babel/types" "^7.13.0"
+    "@babel/types" "^7.13.12"
 
-"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.12.5":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0"
-  integrity sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==
+"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.12.5", "@babel/helper-module-imports@^7.13.12":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
+  integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
   dependencies:
-    "@babel/types" "^7.12.13"
+    "@babel/types" "^7.13.12"
 
-"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.12.13", "@babel/helper-module-transforms@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz#42eb4bd8eea68bab46751212c357bfed8b40f6f1"
-  integrity sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw==
+"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.13.0":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.12.tgz#600e58350490828d82282631a1422268e982ba96"
+  integrity sha512-7zVQqMO3V+K4JOOj40kxiCrMf6xlQAkewBB0eu2b03OO/Q21ZutOzjpfD79A5gtE/2OWi1nv625MrDlGlkbknQ==
   dependencies:
-    "@babel/helper-module-imports" "^7.12.13"
-    "@babel/helper-replace-supers" "^7.13.0"
-    "@babel/helper-simple-access" "^7.12.13"
+    "@babel/helper-module-imports" "^7.13.12"
+    "@babel/helper-replace-supers" "^7.13.12"
+    "@babel/helper-simple-access" "^7.13.12"
     "@babel/helper-split-export-declaration" "^7.12.13"
     "@babel/helper-validator-identifier" "^7.12.11"
     "@babel/template" "^7.12.13"
     "@babel/traverse" "^7.13.0"
-    "@babel/types" "^7.13.0"
-    lodash "^4.17.19"
+    "@babel/types" "^7.13.12"
 
 "@babel/helper-optimise-call-expression@^7.12.13":
   version "7.12.13"
     "@babel/helper-wrap-function" "^7.13.0"
     "@babel/types" "^7.13.0"
 
-"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz#6034b7b51943094cb41627848cb219cb02be1d24"
-  integrity sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw==
+"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0", "@babel/helper-replace-supers@^7.13.12":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804"
+  integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==
   dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.13.0"
+    "@babel/helper-member-expression-to-functions" "^7.13.12"
     "@babel/helper-optimise-call-expression" "^7.12.13"
     "@babel/traverse" "^7.13.0"
-    "@babel/types" "^7.13.0"
+    "@babel/types" "^7.13.12"
 
-"@babel/helper-simple-access@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4"
-  integrity sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==
+"@babel/helper-simple-access@^7.12.13", "@babel/helper-simple-access@^7.13.12":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6"
+  integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==
   dependencies:
-    "@babel/types" "^7.12.13"
+    "@babel/types" "^7.13.12"
 
 "@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
   version "7.12.1"
     "@babel/traverse" "^7.13.0"
     "@babel/types" "^7.13.0"
 
-"@babel/helpers@^7.12.5", "@babel/helpers@^7.13.0", "@babel/helpers@^7.8.3":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0"
-  integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ==
+"@babel/helpers@^7.12.5", "@babel/helpers@^7.13.10", "@babel/helpers@^7.8.3":
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8"
+  integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==
   dependencies:
     "@babel/template" "^7.12.13"
     "@babel/traverse" "^7.13.0"
     "@babel/types" "^7.13.0"
 
 "@babel/highlight@^7.12.13":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c"
-  integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
+  integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
   dependencies:
     "@babel/helper-validator-identifier" "^7.12.11"
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.12.10", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.13.0", "@babel/parser@^7.8.3":
-  version "7.13.4"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.4.tgz#340211b0da94a351a6f10e63671fa727333d13ab"
-  integrity sha512-uvoOulWHhI+0+1f9L4BoozY7U5cIkZ9PgJqvb041d6vypgUmtVPG4vmGm4pSggjl8BELzvHyUeJSUyEMY6b+qA==
+"@babel/parser@^7.12.10", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10", "@babel/parser@^7.8.3":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.12.tgz#ba320059420774394d3b0c0233ba40e4250b81d1"
+  integrity sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==
 
 "@babel/plugin-proposal-async-generator-functions@^7.12.1":
-  version "7.13.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.5.tgz#69e3fbb9958949b09036e27b26eba1aafa1ba3db"
-  integrity sha512-8cErJEDzhZgNKzYyjCKsHuyPqtWxG8gc9h4OFSUDJu0vCAOsObPU2LcECnW0kJwh/b+uUz46lObVzIXw0fzAbA==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz#87aacb574b3bc4b5603f6fe41458d72a5a2ec4b1"
+  integrity sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.13.0"
     "@babel/helper-remap-async-to-generator" "^7.13.0"
-    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-async-generators" "^7.8.4"
 
 "@babel/plugin-proposal-class-properties@^7.12.1":
   version "7.13.0"
     "@babel/helper-plugin-utils" "^7.13.0"
 
 "@babel/plugin-proposal-dynamic-import@^7.12.1":
-  version "7.12.17"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.17.tgz#e0ebd8db65acc37eac518fa17bead2174e224512"
-  integrity sha512-ZNGoFZqrnuy9H2izB2jLlnNDAfVPlGl5NhFEiFe4D84ix9GQGygF+CWMGHKuE+bpyS/AOuDQCnkiRNqW2IzS1Q==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz#876a1f6966e1dec332e8c9451afda3bebcdf2e1d"
+  integrity sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/helper-plugin-utils" "^7.13.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
 
 "@babel/plugin-proposal-export-namespace-from@^7.12.1":
   version "7.12.13"
     "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
 
 "@babel/plugin-proposal-json-strings@^7.12.1":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.13.tgz#ced7888a2db92a3d520a2e35eb421fdb7fcc9b5d"
-  integrity sha512-v9eEi4GiORDg8x+Dmi5r8ibOe0VXoKDeNPYcTTxdGN4eOWikrJfDJCJrr1l5gKGvsNyGJbrfMftC2dTL6oz7pg==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz#bf1fb362547075afda3634ed31571c5901afef7b"
+  integrity sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/helper-plugin-utils" "^7.13.0"
+    "@babel/plugin-syntax-json-strings" "^7.8.3"
 
 "@babel/plugin-proposal-logical-assignment-operators@^7.12.1":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.13.tgz#575b5d9a08d8299eeb4db6430da6e16e5cf14350"
-  integrity sha512-fqmiD3Lz7jVdK6kabeSr1PZlWSUVqSitmHEe3Z00dtGTKieWnX9beafvavc32kjORa5Bai4QNHgFDwWJP+WtSQ==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz#93fa78d63857c40ce3c8c3315220fd00bfbb4e1a"
+  integrity sha512-aul6znYB4N4HGweImqKn59Su9RS8lbUIqxtXTOcAGtNIDczoEFv+l1EhmX8rUBp3G1jMjKJm8m0jXVp63ZpS4A==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
+    "@babel/helper-plugin-utils" "^7.13.0"
     "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
 
 "@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.0.tgz#1a96fdf2c43109cfe5568513c5379015a23f5380"
-  integrity sha512-UkAvFA/9+lBBL015gjA68NvKiCReNxqFLm3SdNKaM3XXoDisA7tMAIX4PmIwatFoFqMxxT3WyG9sK3MO0Kting==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz#3730a31dafd3c10d8ccd10648ed80a2ac5472ef3"
+  integrity sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.13.0"
-    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
 
 "@babel/plugin-proposal-numeric-separator@^7.12.7":
   version "7.12.13"
     "@babel/plugin-syntax-numeric-separator" "^7.10.4"
 
 "@babel/plugin-proposal-object-rest-spread@^7.12.1":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.0.tgz#8f19ad247bb96bd5ad2d4107e6eddfe0a789937b"
-  integrity sha512-B4qphdSTp0nLsWcuei07JPKeZej4+Hd22MdnulJXQa1nCcGSBlk8FiqenGERaPZ+PuYhz4Li2Wjc8yfJvHgUMw==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz#5d210a4d727d6ce3b18f9de82cc99a3964eed60a"
+  integrity sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g==
   dependencies:
+    "@babel/compat-data" "^7.13.8"
+    "@babel/helper-compilation-targets" "^7.13.8"
     "@babel/helper-plugin-utils" "^7.13.0"
-    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
     "@babel/plugin-transform-parameters" "^7.13.0"
 
 "@babel/plugin-proposal-optional-catch-binding@^7.12.1":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.13.tgz#4640520afe57728af14b4d1574ba844f263bcae5"
-  integrity sha512-9+MIm6msl9sHWg58NvqpNpLtuFbmpFYk37x8kgnGzAHvX35E1FyAwSUt5hIkSoWJFSAH+iwU8bJ4fcD1zKXOzg==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz#3ad6bd5901506ea996fc31bdcf3ccfa2bed71107"
+  integrity sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.12.13"
-    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/helper-plugin-utils" "^7.13.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
 
 "@babel/plugin-proposal-optional-chaining@^7.12.7":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.0.tgz#75b41ce0d883d19e8fe635fc3f846be3b1664f4d"
-  integrity sha512-OVRQOZEBP2luZrvEbNSX5FfWDousthhdEoAOpej+Tpe58HFLvqRClT89RauIvBuCDFEip7GW1eT86/5lMy2RNA==
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz#ba9feb601d422e0adea6760c2bd6bbb7bfec4866"
+  integrity sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.13.0"
     "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
-    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
 
 "@babel/plugin-proposal-private-methods@^7.12.1":
   version "7.13.0"
     "@babel/helper-create-regexp-features-plugin" "^7.12.13"
     "@babel/helper-plugin-utils" "^7.12.13"
 
-"@babel/plugin-syntax-async-generators@^7.8.0":
+"@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
   integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.12.13"
 
-"@babel/plugin-syntax-dynamic-import@^7.8.0":
+"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
   integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-syntax-json-strings@^7.8.0":
+"@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
   integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0":
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
   integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.10.4"
 
-"@babel/plugin-syntax-object-rest-spread@^7.8.0":
+"@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
   integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
   integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-optional-chaining@^7.8.0":
+"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
   integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
     babel-plugin-dynamic-import-node "^2.3.3"
 
 "@babel/plugin-transform-modules-commonjs@^7.12.1":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.0.tgz#276932693a20d12c9776093fdc99c0d9995e34c6"
-  integrity sha512-j7397PkIB4lcn25U2dClK6VLC6pr2s3q+wbE8R3vJvY6U1UTBBj0n6F+5v6+Fd/UwfDPAorMOs2TV+T4M+owpQ==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz#7b01ad7c2dcf2275b06fa1781e00d13d420b3e1b"
+  integrity sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw==
   dependencies:
     "@babel/helper-module-transforms" "^7.13.0"
     "@babel/helper-plugin-utils" "^7.13.0"
     babel-plugin-dynamic-import-node "^2.3.3"
 
 "@babel/plugin-transform-modules-systemjs@^7.12.1":
-  version "7.12.13"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.13.tgz#351937f392c7f07493fc79b2118201d50404a3c5"
-  integrity sha512-aHfVjhZ8QekaNF/5aNdStCGzwTbU7SI5hUybBKlMzqIMC7w7Ho8hx5a4R/DkTHfRfLwHGGxSpFt9BfxKCoXKoA==
+  version "7.13.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3"
+  integrity sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A==
   dependencies:
-    "@babel/helper-hoist-variables" "^7.12.13"
-    "@babel/helper-module-transforms" "^7.12.13"
-    "@babel/helper-plugin-utils" "^7.12.13"
+    "@babel/helper-hoist-variables" "^7.13.0"
+    "@babel/helper-module-transforms" "^7.13.0"
+    "@babel/helper-plugin-utils" "^7.13.0"
     "@babel/helper-validator-identifier" "^7.12.11"
     babel-plugin-dynamic-import-node "^2.3.3"
 
     regenerator-runtime "^0.13.4"
 
 "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
-  version "7.13.7"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a"
-  integrity sha512-h+ilqoX998mRVM5FtB5ijRuHUDVt5l3yfoOi2uh18Z/O3hvyaHQ39NpxVkCIG5yFs+mLq/ewFp8Bss6zmWv6ZA==
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
+  integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
   dependencies:
     regenerator-runtime "^0.13.4"
 
     globals "^11.1.0"
     lodash "^4.17.19"
 
-"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6":
-  version "7.13.0"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80"
-  integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==
+"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6":
+  version "7.13.12"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.12.tgz#edbf99208ef48852acdff1c8a681a1e4ade580cd"
+  integrity sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==
   dependencies:
     "@babel/helper-validator-identifier" "^7.12.11"
     lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
-"@discoveryjs/json-ext@^0.5.0":
+"@discoveryjs/json-ext@0.5.2", "@discoveryjs/json-ext@^0.5.0":
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
   integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==
   dependencies:
     tslib "^2.0.0"
 
-"@ngtools/webpack@11.2.2":
-  version "11.2.2"
-  resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.2.2.tgz#647862ed19761796c7f84d5fb3305661d2a3af67"
-  integrity sha512-X1M/Xs0kLi9FrOIU6yJ74q3pCzhgwPQowO1XjJ68KLOoMbj/DM6Qm0Hi9N0Ay8h0s7BIdjKEu/C3pCdGu1Q54w==
+"@ngtools/webpack@11.2.5":
+  version "11.2.5"
+  resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.2.5.tgz#3e2265145d19fcdda9ec2894ccded83658b1fa66"
+  integrity sha512-7fhg8hvqTiTS5ESiEN4xR2qRnOVX0rhVSckMXbAFvNYTwQOuS865RiBrYCJ4CsKhGJ9P7XS5i2EIwA3/aLSivg==
   dependencies:
-    "@angular-devkit/core" "11.2.2"
+    "@angular-devkit/core" "11.2.5"
     enhanced-resolve "5.7.0"
     webpack-sources "2.2.0"
 
     infer-owner "^1.0.4"
 
 "@npmcli/run-script@^1.3.0":
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.3.tgz#07f440ed492400bb1114369bc37315eeaaae2bb3"
-  integrity sha512-ELPGWAVU/xyU+A+H3pEPj0QOvYwLTX71RArXcClFzeiyJ/b/McsZ+d0QxpznvfFtZzxGN/gz/1cvlqICR4/suQ==
+  version "1.8.4"
+  resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.4.tgz#03ced92503a6fe948cbc0975ce39210bc5e824d6"
+  integrity sha512-Yd9HXTtF1JGDXZw0+SOn+mWLYS0e7bHBHVC/2C8yqs4wUrs/k8rwBSinD7rfk+3WG/MFGRZKxjyoD34Pch2E/A==
   dependencies:
     "@npmcli/node-gyp" "^1.0.2"
     "@npmcli/promise-spawn" "^1.3.2"
     infer-owner "^1.0.4"
     node-gyp "^7.1.0"
-    puka "^1.0.1"
     read-package-json-fast "^2.0.1"
 
 "@polka/url@^1.0.0-next.9":
   resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71"
   integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==
 
-"@schematics/angular@11.2.2":
-  version "11.2.2"
-  resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.2.2.tgz#ff69a66b6e1acf5aa36ed0795973f3f57d893d0b"
-  integrity sha512-TcxPy58adUnkirGXyZVVSMuKkA0eIz2PWSQWEgB9l7kO+5LvDOn+RMoc6AVx0s/bU9nH+eozBUJ1XAD/E8QnYQ==
+"@schematics/angular@11.2.5":
+  version "11.2.5"
+  resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.2.5.tgz#c984687c95be32d3fa6016faa8b5a61715a830f5"
+  integrity sha512-pjaK0gZyqhzgAVxMKElG6cDpAvNZ3adVCTA8dhEixpH+JaQdoczl59hMn7rH75yQW0PApe+8g7HMwVK6bLRmxQ==
   dependencies:
-    "@angular-devkit/core" "11.2.2"
-    "@angular-devkit/schematics" "11.2.2"
+    "@angular-devkit/core" "11.2.5"
+    "@angular-devkit/schematics" "11.2.5"
     jsonc-parser "3.0.0"
 
-"@schematics/update@0.1102.2":
-  version "0.1102.2"
-  resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1102.2.tgz#f8aed68bbcefdc8633c7804e47ff891ef06bd5ef"
-  integrity sha512-Nz8kjeixzDnOw00bnZznq3qrbIv8yWEWNb9eDkRBqgOUXQwlhKJY/sYBK58JF2D+conaRVuEqMsBlX08GlFtIA==
+"@schematics/update@0.1102.5":
+  version "0.1102.5"
+  resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1102.5.tgz#538493f0a7d06d794d521cca4f2ff588f05cc733"
+  integrity sha512-iz9pM8mabieqQnPZjrqP5jfRFvPm81/uIg46kY3KjtDtSBi4GAF2dnFyX1dC2mG1rq+e+8zeQLvOvhdLifYlEA==
   dependencies:
-    "@angular-devkit/core" "11.2.2"
-    "@angular-devkit/schematics" "11.2.2"
+    "@angular-devkit/core" "11.2.5"
+    "@angular-devkit/schematics" "11.2.5"
     "@yarnpkg/lockfile" "1.1.0"
     ini "2.0.0"
     npm-package-arg "^8.0.0"
     "@types/node" "*"
 
 "@types/chart.js@^2.9.16":
-  version "2.9.30"
-  resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.30.tgz#34b99897f4f5ef0f74c8fe4ced70ac52b4d752dd"
-  integrity sha512-EgjxUUZFvf6ls3kW2CwyrnSJhgyKxgwrlp/W5G9wqyPEO9iFatO63zAA7L24YqgMxiDjQ+tG7ODU+2yWH91lPg==
+  version "2.9.31"
+  resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.31.tgz#e8ebc7ed18eb0e5114c69bd46ef8e0037c89d39d"
+  integrity sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA==
   dependencies:
     moment "^2.10.2"
 
   integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==
 
 "@types/jasmine@*", "@types/jasmine@^3.3.15":
-  version "3.6.4"
-  resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.4.tgz#22ade1b692d5656f859ef9bc6c62d88632cc27e0"
-  integrity sha512-CTdMERA4iGNcxeqzD7pavb4WLIFq6bGnx6nIJD+1D4Knx24GE6QBPrWVhO8UlIy7gf7rbIt3ZD7iIzryRD2TgA==
+  version "3.6.7"
+  resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.7.tgz#e762d3ead78538efb7900ab932d7daf334acb0b4"
+  integrity sha512-8dtfiykrpe4Ysn6ONj0tOjmpDIh1vWxPk80eutSeWmyaJvAZXZ84219fS4gLrvz05eidhp7BP17WVQBaXHSyXQ==
 
 "@types/jasminewd2@^2.0.3":
   version "2.0.8"
   integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
 
 "@types/linkify-it@*":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.0.tgz#c0ca4c253664492dbf47a646f31cfd483a6bbc95"
-  integrity sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ==
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.1.tgz#4d26a9efe3aa2caf829234ec5a39580fc88b6001"
+  integrity sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ==
 
 "@types/linkifyjs@^2.1.2":
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d"
   integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew==
 
-"@types/node@*", "@types/node@^14.0.14", "@types/node@^14.14.10":
-  version "14.14.31"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
-  integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
+"@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.0.14":
+  version "14.14.35"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
+  integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
 
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
 
 "@types/react@*":
-  version "17.0.2"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8"
-  integrity sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==
+  version "17.0.3"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
+  integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
   dependencies:
     "@types/prop-types" "*"
+    "@types/scheduler" "*"
     csstype "^3.0.2"
 
 "@types/sanitize-html@1.27.1":
   dependencies:
     htmlparser2 "^4.1.0"
 
+"@types/scheduler@*":
+  version "0.16.1"
+  resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
+  integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
+
 "@types/selenium-webdriver@^3.0.0":
   version "3.0.17"
   resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b"
   integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
 
 "@types/uglify-js@*":
-  version "3.12.0"
-  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.12.0.tgz#2bb061c269441620d46b946350c8f16d52ef37c5"
-  integrity sha512-sYAF+CF9XZ5cvEBkI7RtrG9g2GtMBkviTnBxYYyq+8BWvO4QtXfwwR6a2LFwCi4evMKZfpv6U43ViYvv17Wz3Q==
+  version "3.13.0"
+  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
+  integrity sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q==
   dependencies:
     source-map "^0.6.1"
 
@@ -1925,9 +1932,9 @@ acorn@^6.4.1:
   integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
 
 acorn@^8.0.4:
-  version "8.0.5"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.5.tgz#a3bfb872a74a6a7f661bc81b9849d9cac12601b7"
-  integrity sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe"
+  integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==
 
 addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.1:
   version "1.5.1"
@@ -2793,7 +2800,7 @@ bytes@3.1.0:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
-cacache@15.0.5, cacache@^15.0.5:
+cacache@15.0.5:
   version "15.0.5"
   resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0"
   integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==
@@ -2837,6 +2844,29 @@ cacache@^12.0.2:
     unique-filename "^1.1.1"
     y18n "^4.0.0"
 
+cacache@^15.0.5:
+  version "15.0.6"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.6.tgz#65a8c580fda15b59150fb76bf3f3a8e45d583099"
+  integrity sha512-g1WYDMct/jzW+JdWEyjaX2zoBkZ6ZT9VpOyp2I/VMtDsNLffNat3kqPFfi1eDRSK9/SuKGyORDHcQMcPF8sQ/w==
+  dependencies:
+    "@npmcli/move-file" "^1.0.1"
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    glob "^7.1.4"
+    infer-owner "^1.0.4"
+    lru-cache "^6.0.0"
+    minipass "^3.1.1"
+    minipass-collect "^1.0.2"
+    minipass-flush "^1.0.5"
+    minipass-pipeline "^1.2.2"
+    mkdirp "^1.0.3"
+    p-map "^4.0.0"
+    promise-inflight "^1.0.1"
+    rimraf "^3.0.2"
+    ssri "^8.0.1"
+    tar "^6.0.2"
+    unique-filename "^1.1.1"
+
 cache-base@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -2937,9 +2967,9 @@ caniuse-api@^3.0.0:
     lodash.uniq "^4.5.0"
 
 caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001181:
-  version "1.0.30001192"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40"
-  integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw==
+  version "1.0.30001204"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz#256c85709a348ec4d175e847a3b515c66e79f2aa"
+  integrity sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==
 
 canonical-path@1.0.0:
   version "1.0.0"
@@ -2971,7 +3001,7 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
     escape-string-regexp "^1.0.5"
     supports-color "^5.3.0"
 
-chalk@^4.0.0, chalk@^4.1.0:
+chalk@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
   integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
@@ -3081,9 +3111,9 @@ chrome-trace-event@^1.0.2:
     tslib "^1.9.0"
 
 chunk-store-stream@^4.1.1:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.2.0.tgz#18f673c495946c4cdcf14124a3ebd5f31eb0ea35"
-  integrity sha512-90iueoPoqT2isnmy1fyqwzgFy5FokuaxQuijOQG1VgC/6DaXRfeYN0da8iWENkzqElWhqLxo8pWc7pH9dmxlcA==
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e"
+  integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw==
   dependencies:
     block-stream2 "^2.0.0"
     readable-stream "^3.6.0"
@@ -3143,9 +3173,9 @@ cli-cursor@^3.1.0:
     restore-cursor "^3.1.0"
 
 cli-spinners@^2.5.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047"
-  integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939"
+  integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==
 
 cli-width@^2.0.0:
   version "2.2.1"
@@ -3279,9 +3309,9 @@ color-name@^1.0.0, color-name@~1.1.4:
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 color-string@^1.5.4:
-  version "1.5.4"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6"
-  integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==
+  version "1.5.5"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
+  integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
@@ -3294,10 +3324,10 @@ color@^3.0.0:
     color-convert "^1.9.1"
     color-string "^1.5.4"
 
-colorette@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
-  integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
+colorette@^1.2.1, colorette@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
+  integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
 
 colors@1.4.0, colors@^1.4.0:
   version "1.4.0"
@@ -3327,9 +3357,9 @@ commander@^6.2.0:
   integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
 
 commander@^7.0.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff"
-  integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
 
 commondir@^1.0.1:
   version "1.0.1"
@@ -3501,9 +3531,9 @@ copy-webpack-plugin@6.3.2:
     webpack-sources "^1.4.3"
 
 core-js-compat@^3.8.0:
-  version "3.9.0"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.0.tgz#29da39385f16b71e1915565aa0385c4e0963ad56"
-  integrity sha512-YK6fwFjCOKWwGnjFUR3c544YsnA/7DoLL0ysncuOJ4pwbriAtOpvM2bygdlcXbvQCQZ7bBU9CL4t7tGl7ETRpQ==
+  version "3.9.1"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.1.tgz#4e572acfe90aff69d76d8c37759d21a5c59bb455"
+  integrity sha512-jXAirMQxrkbiiLsCx9bQPJFA6llDadKMpYrBJQJ3/c4/vsPP/fAf29h24tviRlvwUL6AmY5CHLu2GvjuYviQqA==
   dependencies:
     browserslist "^4.16.3"
     semver "7.0.0"
@@ -3514,9 +3544,9 @@ core-js@3.8.3:
   integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
 
 core-js@^3.1.4:
-  version "3.9.0"
-  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.0.tgz#790b1bb11553a2272b36e2625c7179db345492f8"
-  integrity sha512-PyFBJaLq93FlyYdsndE5VaueA9K5cNB7CGzeCj191YYLhkQM0gdZR2SKihM70oF0wdqKSKClv/tEBOpoRmdOVQ==
+  version "3.9.1"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae"
+  integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
 
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
@@ -3691,15 +3721,15 @@ css-loader@5.0.1:
     semver "^7.3.2"
 
 css-loader@^5.0.1:
-  version "5.0.2"
-  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.0.2.tgz#24f758dae349bad0a440c50d7e2067742e0899cb"
-  integrity sha512-gbkBigdcHbmNvZ1Cg6aV6qh6k9N6XOr8YWzISLQGrwk2mgOH8LLrizhkxbDhQtaLtktyKHD4970S0xwz5btfTA==
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1"
+  integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag==
   dependencies:
     camelcase "^6.2.0"
     cssesc "^3.0.0"
     icss-utils "^5.1.0"
     loader-utils "^2.0.0"
-    postcss "^8.2.4"
+    postcss "^8.2.8"
     postcss-modules-extract-imports "^3.0.0"
     postcss-modules-local-by-default "^4.0.0"
     postcss-modules-scope "^3.0.0"
@@ -4081,9 +4111,9 @@ destroy@~1.0.4:
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
 detect-node@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
-  integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.5.tgz#9d270aa7eaa5af0b72c4c9d9b814e7f4ce738b79"
+  integrity sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==
 
 dexie@^3.0.0:
   version "3.0.3"
@@ -4241,9 +4271,9 @@ domutils@^1.5.1, domutils@^1.7.0:
     domelementtype "1"
 
 domutils@^2.0.0, domutils@^2.4.4:
-  version "2.4.4"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3"
-  integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.0.tgz#42f49cffdabb92ad243278b331fd761c1c2d3039"
+  integrity sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==
   dependencies:
     dom-serializer "^1.0.1"
     domelementtype "^2.0.1"
@@ -4293,9 +4323,9 @@ ee-first@1.1.1:
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
 electron-to-chromium@^1.3.649:
-  version "1.3.673"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.673.tgz#b4f81c930b388f962b7eba20d0483299aaa40913"
-  integrity sha512-ms+QR2ckfrrpEAjXweLx6kNCbpAl66DcW//3BZD4BV5KhUgr0RZRce1ON/9J3QyA3JO28nzgb5Xv8DnPr05ILg==
+  version "1.3.695"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.695.tgz#955f419cf99137226180cc4cca2e59015a4e248d"
+  integrity sha512-lz66RliUqLHU1Ojxx1A4QUxKydjiQ79Y4dZyPobs2Dmxj5aVL2TM3KoQ2Gs7HS703Bfny+ukI3KOxwAB0xceHQ==
 
 elliptic@^6.5.3:
   version "6.5.4"
@@ -4357,9 +4387,9 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
     once "^1.4.0"
 
 engine.io-client@~4.1.0:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-4.1.1.tgz#109942705079f15a4fcf1090bc86d3a1341c0a61"
-  integrity sha512-iYasV/EttP/2pLrdowe9G3zwlNIFhwny8VSIh+vPlMnYZqSzLsTzSLa9hFy015OrH1s4fzoYxeHjVkO8hSFKwg==
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-4.1.2.tgz#823b4f005360321c41445fc23ce8ee028ef2e36b"
+  integrity sha512-1mwvwKYMa0AaCy+sPgvJ/SnKyO5MJZ1HEeXfA3Rm/KHkHGiYD5bQVq8QzvIrkI01FuVtOdZC5lWdRw1BGXB2NQ==
   dependencies:
     base64-arraybuffer "0.1.4"
     component-emitter "~1.3.0"
@@ -4437,9 +4467,9 @@ entities@~2.1.0:
   integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
 
 env-paths@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
-  integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
+  integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
 
 envinfo@^7.7.3:
   version "7.7.4"
@@ -4470,42 +4500,27 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.2:
-  version "1.17.7"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
-  integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
-  dependencies:
-    es-to-primitive "^1.2.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    has-symbols "^1.0.1"
-    is-callable "^1.2.2"
-    is-regex "^1.1.1"
-    object-inspect "^1.8.0"
-    object-keys "^1.1.1"
-    object.assign "^4.1.1"
-    string.prototype.trimend "^1.0.1"
-    string.prototype.trimstart "^1.0.1"
-
-es-abstract@^1.18.0-next.2:
-  version "1.18.0-next.2"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2"
-  integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==
+es-abstract@^1.17.2, es-abstract@^1.18.0-next.2:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4"
+  integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==
   dependencies:
     call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
-    get-intrinsic "^1.0.2"
+    get-intrinsic "^1.1.1"
     has "^1.0.3"
-    has-symbols "^1.0.1"
-    is-callable "^1.2.2"
+    has-symbols "^1.0.2"
+    is-callable "^1.2.3"
     is-negative-zero "^2.0.1"
-    is-regex "^1.1.1"
+    is-regex "^1.1.2"
+    is-string "^1.0.5"
     object-inspect "^1.9.0"
     object-keys "^1.1.1"
     object.assign "^4.1.2"
-    string.prototype.trimend "^1.0.3"
-    string.prototype.trimstart "^1.0.3"
+    string.prototype.trimend "^1.0.4"
+    string.prototype.trimstart "^1.0.4"
+    unbox-primitive "^1.0.0"
 
 es-to-primitive@^1.2.1:
   version "1.2.1"
@@ -4731,14 +4746,14 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.3:
   integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
 
 events@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
-  integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
 
 eventsource@^1.0.7:
-  version "1.0.7"
-  resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
-  integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf"
+  integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==
   dependencies:
     original "^1.0.0"
 
@@ -5109,9 +5124,9 @@ focus-visible@^5.0.2:
   integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
 
 follow-redirects@^1.0.0:
-  version "1.13.2"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147"
-  integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==
+  version "1.13.3"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
+  integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
 
 for-in@^1.0.2:
   version "1.0.2"
@@ -5301,7 +5316,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
 
-get-intrinsic@^1.0.2:
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
   integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
@@ -5353,9 +5368,9 @@ glob-parent@^3.1.0:
     path-dirname "^1.0.0"
 
 glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
-  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
   dependencies:
     is-glob "^4.0.1"
 
@@ -5410,9 +5425,9 @@ globals@^9.2.0:
   integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
 
 globby@^11.0.1:
-  version "11.0.2"
-  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83"
-  integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==
+  version "11.0.3"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb"
+  integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==
   dependencies:
     array-union "^2.1.0"
     dir-glob "^3.0.1"
@@ -5497,6 +5512,11 @@ has-ansi@^2.0.0:
   dependencies:
     ansi-regex "^2.0.0"
 
+has-bigints@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
+  integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
+
 has-cors@1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
@@ -5512,10 +5532,10 @@ has-flag@^4.0.0:
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
-has-symbols@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
-  integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+  integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
 
 has-unicode@^2.0.0:
   version "2.0.1"
@@ -5616,6 +5636,13 @@ hosted-git-info@^3.0.6:
   dependencies:
     lru-cache "^6.0.0"
 
+hosted-git-info@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.1.tgz#710ef5452ea429a844abc33c981056e7371edab7"
+  integrity sha512-eT7NrxAsppPRQEBSwKSosReE+v8OzABwEScQYk5d4uxaEPlzxTIku7LINXtBGalthkLhJnq5lBI89PfK43zAKg==
+  dependencies:
+    lru-cache "^6.0.0"
+
 hpack.js@^2.1.6:
   version "2.1.6"
   resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -5712,9 +5739,9 @@ htmlparser2@^4.1.0:
     entities "^2.0.0"
 
 htmlparser2@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01"
-  integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.1.tgz#422521231ef6d42e56bd411da8ba40aa36e91446"
+  integrity sha512-GDKPd+vk4jvSuvCbyuzx/unmXkk090Azec7LovXP8as1Hn8q9p3hbjmDGbUqqhknw0ajwit6LiiWqfiTUPMK7w==
   dependencies:
     domelementtype "^2.0.1"
     domhandler "^4.0.0"
@@ -6133,6 +6160,11 @@ is-ascii@^1.0.0:
   resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929"
   integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk=
 
+is-bigint@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2"
+  integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==
+
 is-binary-path@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
@@ -6147,12 +6179,19 @@ is-binary-path@~2.1.0:
   dependencies:
     binary-extensions "^2.0.0"
 
+is-boolean-object@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0"
+  integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==
+  dependencies:
+    call-bind "^1.0.0"
+
 is-buffer@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.4, is-callable@^1.2.2:
+is-callable@^1.1.4, is-callable@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
   integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
@@ -6312,6 +6351,11 @@ is-negative-zero@^2.0.1:
   resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
   integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
 
+is-number-object@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
+  integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
+
 is-number@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -6384,7 +6428,7 @@ is-property@^1.0.0, is-property@^1.0.2:
   resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
   integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
 
-is-regex@^1.0.4, is-regex@^1.1.1:
+is-regex@^1.0.4, is-regex@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
   integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
@@ -6407,6 +6451,11 @@ is-stream@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
   integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
 
+is-string@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
+  integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
+
 is-svg@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
@@ -6414,7 +6463,7 @@ is-svg@^3.0.0:
   dependencies:
     html-comment-regex "^1.1.0"
 
-is-symbol@^1.0.2:
+is-symbol@^1.0.2, is-symbol@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
   integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
@@ -6426,6 +6475,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
 
+is-unicode-supported@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
+  integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
+
 is-what@^3.12.0:
   version "3.14.1"
   resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
@@ -6538,16 +6592,21 @@ istanbul-reports@^3.0.2:
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
 
-jasmine-core@^3.6.0, jasmine-core@~3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20"
-  integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==
+jasmine-core@^3.6.0:
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561"
+  integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==
 
 jasmine-core@~2.8.0:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e"
   integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=
 
+jasmine-core@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20"
+  integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==
+
 jasmine-spec-reporter@~6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-6.0.0.tgz#3b9c85689676a351f343ba8dd6d3957f11a4bf1d"
@@ -6785,9 +6844,9 @@ karma-source-map-support@1.4.0:
     source-map-support "^0.5.5"
 
 karma@~6.1.0:
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/karma/-/karma-6.1.1.tgz#a7539618cca0f2cbb26d5497120ec31fe340c2a1"
-  integrity sha512-vVDFxFGAsclgmFjZA/qGw5xqWdZIWxVD7xLyCukYUYd5xs/uGzYbXGOT5zOruVBQleKEmXIr4H2hzGCTn+M9Cg==
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/karma/-/karma-6.1.2.tgz#9d7394559f5deb150b3021c1860960281c3a0e50"
+  integrity sha512-mKbxgsJrt3UHBPdKfCxC2eg3lpqyt6hQRFhNWJ2sk0wUnbnLPEiCpgIgiycuLSra0vC6TaK9OPJiMGATGzgH/A==
   dependencies:
     body-parser "^1.19.0"
     braces "^3.0.2"
@@ -7042,11 +7101,12 @@ lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.1
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
 log-symbols@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
-  integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
+  integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
   dependencies:
-    chalk "^4.0.0"
+    chalk "^4.1.0"
+    is-unicode-supported "^0.1.0"
 
 log4js@^6.2.1:
   version "6.3.0"
@@ -7110,9 +7170,9 @@ m3u8-parser@4.5.0:
     global "^4.3.2"
 
 m3u8-parser@^4.4.0:
-  version "4.5.2"
-  resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.5.2.tgz#f7d48a60112466e528324624c4e66d52ed341a75"
-  integrity sha512-sN/lu3TiRxmG2RFjZxo5c0/7Dr4RrEztl43jXrWwj5gFZ7vfa2iIxGfiPx485dm5QCazaIcKk+vNkUso8Aq0Ag==
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.6.0.tgz#a0e2f5dcf8391c9a6e59895a084fa38f27b52124"
+  integrity sha512-dKhhpMcPqDM/KzULVrNyDZ/z766peQjwUghDTcl6TE7DQKAt/vm74/IMUAxpO34f6LDpM+OH/dYGQwW1eM4yWw==
   dependencies:
     "@babel/runtime" "^7.12.5"
     "@videojs/vhs-utils" "^3.0.0"
@@ -7381,9 +7441,9 @@ mini-css-extract-plugin@1.3.5:
     webpack-sources "^1.1.0"
 
 mini-css-extract-plugin@^1.3.1:
-  version "1.3.8"
-  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.8.tgz#639047b78c2ee728704285aa468d2a5a8d91d566"
-  integrity sha512-u+2kVov/Gcs74iz+x3phEBWMAGw2djjnKfYez+Pl/b5dyXL7aM4Lp5QQtIq16CDwRHT/woUJki49gBNMhfm1eA==
+  version "1.3.9"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.9.tgz#47a32132b0fd97a119acd530e8421e8f6ab16d5e"
+  integrity sha512-Ac4s+xhVbqlyhXS5J/Vh/QXUz3ycXlCqoCPpg0vdfhsIBH9eg/It/9L1r1XhSCH737M1lqcWnMuWL13zcygn5A==
   dependencies:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
@@ -7630,9 +7690,9 @@ nan@^2.12.1:
   integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
 
 nanoid@^3.1.20:
-  version "3.1.20"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
-  integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
+  version "3.1.22"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
+  integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -7830,13 +7890,13 @@ npm-package-arg@8.1.0:
     semver "^7.0.0"
     validate-npm-package-name "^3.0.0"
 
-npm-package-arg@^8.0.0, npm-package-arg@^8.0.1:
-  version "8.1.1"
-  resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.1.tgz#00ebf16ac395c63318e67ce66780a06db6df1b04"
-  integrity sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg==
+npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.2:
+  version "8.1.2"
+  resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.2.tgz#b868016ae7de5619e729993fbd8d11dc3c52ab62"
+  integrity sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA==
   dependencies:
-    hosted-git-info "^3.0.6"
-    semver "^7.0.0"
+    hosted-git-info "^4.0.1"
+    semver "^7.3.4"
     validate-npm-package-name "^3.0.0"
 
 npm-packlist@^2.1.4:
@@ -7849,7 +7909,7 @@ npm-packlist@^2.1.4:
     npm-bundled "^1.1.1"
     npm-normalize-package-bin "^1.0.1"
 
-npm-pick-manifest@6.1.0, npm-pick-manifest@^6.0.0:
+npm-pick-manifest@6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a"
   integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw==
@@ -7858,6 +7918,16 @@ npm-pick-manifest@6.1.0, npm-pick-manifest@^6.0.0:
     npm-package-arg "^8.0.0"
     semver "^7.0.0"
 
+npm-pick-manifest@^6.0.0:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148"
+  integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==
+  dependencies:
+    npm-install-checks "^4.0.0"
+    npm-normalize-package-bin "^1.0.1"
+    npm-package-arg "^8.1.2"
+    semver "^7.3.4"
+
 npm-registry-fetch@^9.0.0:
   version "9.0.0"
   resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661"
@@ -7927,7 +7997,7 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.8.0, object-inspect@^1.9.0:
+object-inspect@^1.9.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
   integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
@@ -7952,7 +8022,7 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2:
+object.assign@^4.1.0, object.assign@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
   integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@@ -8896,12 +8966,12 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27:
     source-map "^0.6.1"
     supports-color "^6.1.0"
 
-postcss@^8.0.2, postcss@^8.1.4, postcss@^8.2.4:
-  version "8.2.6"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe"
-  integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==
+postcss@^8.0.2, postcss@^8.1.4, postcss@^8.2.8:
+  version "8.2.8"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
+  integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
   dependencies:
-    colorette "^1.2.1"
+    colorette "^1.2.2"
     nanoid "^3.1.20"
     source-map "^0.6.1"
 
@@ -8924,9 +8994,9 @@ pretty-error@^2.1.1:
     renderkid "^2.0.4"
 
 primeng@^11.0.0-rc.1:
-  version "11.2.3"
-  resolved "https://registry.yarnpkg.com/primeng/-/primeng-11.2.3.tgz#66e3d817fe27c9a7703726537c03ddcc1998bb44"
-  integrity sha512-8elRAGal8a+qXJ4egRKXU+bUvIyfCxsiCerXgOPbwbo/TU/DBK7WBXGGGi6KJOamFqClAqj/FO3WLAdofKQSRQ==
+  version "11.3.1"
+  resolved "https://registry.yarnpkg.com/primeng/-/primeng-11.3.1.tgz#644dd59d1f0808227a9529ea6ffaad31bdb5e5df"
+  integrity sha512-B86/su/3sNP2GfhyegvZh2MpHcUZHas+13bPL98QmZhoiPBQp2jz3H0iD716+piC00Wee6pi/PPm7e9y9qxGDg==
   dependencies:
     tslib "^2.0.0"
 
@@ -9027,11 +9097,6 @@ public-encrypt@^4.0.0:
     randombytes "^2.0.1"
     safe-buffer "^5.1.2"
 
-puka@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/puka/-/puka-1.0.1.tgz#a2df782b7eb4cf9564e4c93a5da422de0dfacc02"
-  integrity sha512-ssjRZxBd7BT3dte1RR3VoeT2cT/ODH8x+h0rUF1rMqB0srHYf48stSDWfiYakTp5UBZMxroZhB2+ExLDHm7W3g==
-
 pump@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@@ -9133,15 +9198,15 @@ querystringify@^2.1.1:
   resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
   integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
 
-queue-microtask@^1.1.2, queue-microtask@^1.2.0, queue-microtask@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3"
-  integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==
+queue-microtask@^1.2.0, queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
 random-access-file@^2.0.1:
-  version "2.1.5"
-  resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.1.5.tgz#27af6115b920a9adabb44559e29ea9944bb35bfe"
-  integrity sha512-lqmUGgF9X+LD0XSeWSHcs7U2nSLYp+RQvkDDqKWoxW8jcd13tZ00G6PHV32OZqDIHmS9ewoEUEa6jcvyB7UCvg==
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.2.0.tgz#b49b999efefb374afb7587f219071fec5ce66546"
+  integrity sha512-B744003Mj7v3EcuPl9hCiB2Ot4aZjgtU2mV6yFY1THiWU/XfGf1uSadR+SlQdJcwHgAWeG7Lbos0aUqjtj8FQg==
   dependencies:
     mkdirp-classic "^0.5.2"
     random-access-storage "^1.1.1"
@@ -9370,9 +9435,9 @@ regjsgen@^0.5.1:
   integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
 
 regjsparser@^0.6.4:
-  version "0.6.7"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.7.tgz#c00164e1e6713c2e3ee641f1701c4b7aa0a7f86c"
-  integrity sha512-ib77G0uxsA2ovgiYbCVGx4Pv3PSttAx2vIwidqQzbL2U5S4Q+j00HdSAneSBuyVcMvEnTXMjiGgB+DlXozVhpQ==
+  version "0.6.8"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.8.tgz#4532c3da36d75d56e3f394ce2ea6842bde7496bd"
+  integrity sha512-3weFrFQREJhJ2PW+iCGaG6TenyzNSZgsBKZ/oEf6Trme31COSeIWhHw9O6FPkuXktfx+b6Hf/5e6dKPHaROq2g==
   dependencies:
     jsesc "~0.5.0"
 
@@ -9593,9 +9658,9 @@ rework@1.0.1, rework@^1.0.1:
     css "^2.0.0"
 
 rfdc@^1.1.4:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.2.0.tgz#9e9894258f48f284b43c3143c68070a4f373b949"
-  integrity sha512-ijLyszTMmUrXvjSooucVQwimGUk84eRcmCuLV8Xghe3UO85mjUtRAHRyoMM6XtyqbECaXuBWx18La3523sXINA==
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
+  integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
 
 rgb-regex@^1.0.1:
   version "1.0.1"
@@ -9681,7 +9746,7 @@ run-series@^1.1.8, run-series@^1.1.9:
   resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a"
   integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==
 
-rusha@^0.8.1:
+rusha@^0.8.13:
   version "0.8.13"
   resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a"
   integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo=
@@ -9742,9 +9807,9 @@ safe-regex@^1.1.0:
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
 sanitize-html@^2.1.2:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.2.tgz#a1954aea877a096c408aca7b0c260bef6e4fc402"
-  integrity sha512-p7neuskvC8pSurUjdVmbWPXmc9A4+QpOXIL+4gwFC+av5h+lYCXFT8uEneqsFQg/wEA1IH+cKQA60AaQI6p3cg==
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353"
+  integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==
   dependencies:
     deepmerge "^4.2.2"
     escape-string-regexp "^4.0.0"
@@ -9894,7 +9959,7 @@ semver@7.0.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
   integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
 
-semver@7.3.4, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4:
+semver@7.3.4:
   version "7.3.4"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
   integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
@@ -9906,6 +9971,13 @@ semver@^6.0.0, semver@^6.3.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
+semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4:
+  version "7.3.5"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
 send@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -10061,9 +10133,9 @@ simple-get@^4.0.0:
     simple-concat "^1.0.0"
 
 simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3:
-  version "9.9.3"
-  resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.9.3.tgz#b52c39d1173620d06c8b29ada7ee2ad3384bb469"
-  integrity sha512-T3wuv0UqBpDTV0x0pJPPsz4thy0tC0fTOHE4g9+AF43RUxxT+MWeXVtdQcK5Xuzv/XTVrB2NrGzdfO1IFBqOkw==
+  version "9.10.0"
+  resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.10.0.tgz#f458444300f635e6fcc2f5a5166c45d71eafb57f"
+  integrity sha512-sKrKtca1UdmwdZIbvuT3iEL05tDGt/xdLP6+ej8rh1ADgtDk44yLaEZjIyPJ6c34zsSih46Ou7zUIT7e4hPK7g==
   dependencies:
     buffer "^6.0.2"
     debug "^4.2.0"
@@ -10074,12 +10146,12 @@ simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3:
     readable-stream "^3.6.0"
 
 simple-sha1@^3.0.0, simple-sha1@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.0.1.tgz#b34c3c978d74ac4baf99b6555c1e6736e0d6e700"
-  integrity sha512-q7ehqWfHc1VhOm7sW099YDZ4I0yYX7rqyhqqhHV1IYeUTjPOhHyD3mXvv8k2P+rO7+7c8R4/D+8ffzC9BE7Cqg==
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131"
+  integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg==
   dependencies:
-    queue-microtask "^1.1.2"
-    rusha "^0.8.1"
+    queue-microtask "^1.2.2"
+    rusha "^0.8.13"
 
 simple-swizzle@^0.2.2:
   version "0.2.2"
@@ -10159,9 +10231,9 @@ socket.io-adapter@~2.1.0:
   integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
 
 socket.io-client@^3.0.3:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.1.tgz#43dfc3feddbb675b274a724f685d6b6af319b3e3"
-  integrity sha512-BLgIuCjI7Sf3mDHunKddX9zKR/pbkP7IACM3sJS3jha+zJ6/pGKRV6Fz5XSBHCfUs9YzT8kYIqNwOOuFNLtnYA==
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.3.tgz#57ddcefea58cfab71f0e94c21124de8e3c5aa3e2"
+  integrity sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==
   dependencies:
     "@types/component-emitter" "^1.2.10"
     backo2 "~1.0.2"
@@ -10181,13 +10253,13 @@ socket.io-parser@~4.0.3, socket.io-parser@~4.0.4:
     debug "~4.3.1"
 
 socket.io@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.1.tgz#905e3d4a3b37d8e7970e67a4a6eb81110a5778ba"
-  integrity sha512-7cBWdsDC7bbyEF6WbBqffjizc/H4YF1wLdZoOzuYfo2uMNSFjJKuQ36t0H40o9B20DO6p+mSytEd92oP4S15bA==
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a"
+  integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==
   dependencies:
     "@types/cookie" "^0.4.0"
     "@types/cors" "^2.8.8"
-    "@types/node" "^14.14.10"
+    "@types/node" ">=10.0.0"
     accepts "~1.3.4"
     base64id "~2.0.0"
     debug "~4.3.1"
@@ -10226,9 +10298,9 @@ socks-proxy-agent@^5.0.0:
     socks "^2.3.3"
 
 socks@^2.3.3:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f"
-  integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ==
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2"
+  integrity sha512-mNmr9owlinMplev0Wd7UHFlqI4ofnBnNzFuzrm63PPaHgbkqCFe4T5LzwKmtQ/f2tX0NTpcdVLyD/FHxFBstYw==
   dependencies:
     ip "^1.1.5"
     smart-buffer "^4.1.0"
@@ -10416,7 +10488,7 @@ ssri@^6.0.1:
   dependencies:
     figgy-pudding "^3.5.1"
 
-ssri@^8.0.0:
+ssri@^8.0.0, ssri@^8.0.1:
   version "8.0.1"
   resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
   integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
@@ -10546,15 +10618,15 @@ string-width@^3.0.0, string-width@^3.1.0:
     strip-ansi "^5.1.0"
 
 string-width@^4.1.0, string-width@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
-  integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
+  integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
   dependencies:
     emoji-regex "^8.0.0"
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3:
+string.prototype.trimend@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
   integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
@@ -10562,7 +10634,7 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
 
-string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3:
+string.prototype.trimstart@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
   integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
@@ -10815,9 +10887,9 @@ terser@^4.1.2, terser@^4.6.3:
     source-map-support "~0.5.12"
 
 terser@^5.3.4:
-  version "5.6.0"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.0.tgz#138cdf21c5e3100b1b3ddfddf720962f88badcd2"
-  integrity sha512-vyqLMoqadC1uR0vywqOZzriDYzgEkNJFK4q9GeyOBHIbiECHiWLKcWfbQWAUaPfxkjDhapSlZB9f7fkMrvkVjA==
+  version "5.6.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c"
+  integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==
   dependencies:
     commander "^2.20.0"
     source-map "~0.7.2"
@@ -10976,9 +11048,9 @@ tree-kill@1.2.2:
   integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
 
 ts-loader@^8.0.14:
-  version "8.0.17"
-  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.17.tgz#98f2ccff9130074f4079fd89b946b4c637b1f2fc"
-  integrity sha512-OeVfSshx6ot/TCxRwpBHQ/4lRzfgyTkvi7ghDVrLXOHzTbSK413ROgu/xNqM72i3AFeAIJgQy78FwSMKmOW68w==
+  version "8.0.18"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.18.tgz#b2385cbe81c34ad9f997915129cdde3ad92a61ea"
+  integrity sha512-hRZzkydPX30XkLaQwJTDcWDoxZHK6IrEMDQpNd7tgcakFruFkeUp/aY+9hBb7BUGb+ZWKI0jiOGMo0MckwzdDQ==
   dependencies:
     chalk "^4.1.0"
     enhanced-resolve "^4.0.0"
@@ -11054,9 +11126,9 @@ tsutils@^2.29.0:
     tslib "^1.8.1"
 
 tsutils@^3.0.0:
-  version "3.20.0"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698"
-  integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
   dependencies:
     tslib "^1.8.1"
 
@@ -11103,9 +11175,9 @@ type@^1.0.1:
   integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
 
 type@^2.0.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/type/-/type-2.3.0.tgz#ada7c045f07ead08abf9e2edd29be1a0c0661132"
-  integrity sha512-rgPIqOdfK/4J9FhiVrZ3cveAjRRo5rsQBAIhnylX874y1DX/kEKSVdLsnuHB6l1KTjHyU01VjiMBHgU2adejyg==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
+  integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
 
 typedarray-to-buffer@^3.0.0:
   version "3.1.5"
@@ -11119,12 +11191,7 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typescript@4.1.3:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
-  integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
-
-typescript@~4.1.3:
+typescript@4.1.5, typescript@~4.1.3:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
   integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
@@ -11140,9 +11207,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
   integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
 
 uglify-js@^3.0.6:
-  version "3.12.8"
-  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.8.tgz#a82e6e53c9be14f7382de3d068ef1e26e7d4aaf8"
-  integrity sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w==
+  version "3.13.2"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.2.tgz#fe10319861bccc8682bfe2e8151fbdd8aa921c44"
+  integrity sha512-SbMu4D2Vo95LMC/MetNaso1194M1htEA+JrqE9Hk+G2DhI+itfS9TRu9ZKeCahLDNa/J3n4MqUJ/fOHMzQpRWw==
 
 uint64be@^2.0.2:
   version "2.0.2"
@@ -11151,6 +11218,16 @@ uint64be@^2.0.2:
   dependencies:
     buffer-alloc "^1.1.0"
 
+unbox-primitive@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f"
+  integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA==
+  dependencies:
+    function-bind "^1.1.1"
+    has-bigints "^1.0.0"
+    has-symbols "^1.0.0"
+    which-boxed-primitive "^1.0.1"
+
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -11402,9 +11479,9 @@ uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0:
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
 v8-compile-cache@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
-  integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
 
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
@@ -11801,15 +11878,26 @@ webtorrent@^0.112.3:
     utp-native "^2.3.0"
 
 whatwg-fetch@^3.0.0:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.1.tgz#93bc4005af6c2cc30ba3e42ec3125947c8f54ed3"
-  integrity sha512-IEmN/ZfmMw6G1hgZpVd0LuZXOQDisrMOZrzYd5x3RAK4bMPlJohKUZWZ9t/QsTvH0dV9TbPDcc2OSuIDcihnHA==
+  version "3.6.2"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
+  integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
 
 whatwg-mimetype@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
   integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
 
+which-boxed-primitive@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
+  integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
+  dependencies:
+    is-bigint "^1.0.1"
+    is-boolean-object "^1.1.0"
+    is-number-object "^1.0.4"
+    is-string "^1.0.5"
+    is-symbol "^1.0.3"
+
 which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
@@ -11915,9 +12003,9 @@ ws@^6.2.1:
     async-limiter "~1.0.0"
 
 ws@^7.3.0, ws@^7.3.1, ws@^7.4.2, ws@~7.4.2:
-  version "7.4.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd"
-  integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==
+  version "7.4.4"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
+  integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
 
 xml2js@^0.4.17:
   version "0.4.23"
@@ -11978,9 +12066,9 @@ yallist@^4.0.0:
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
 yaml@^1.10.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
-  integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
 
 yargs-parser@^13.1.2:
   version "13.1.2"
@@ -11999,9 +12087,9 @@ yargs-parser@^18.1.2:
     decamelize "^1.2.0"
 
 yargs-parser@^20.2.2:
-  version "20.2.6"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20"
-  integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA==
+  version "20.2.7"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
+  integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
 
 yargs-parser@^7.0.0:
   version "7.0.0"
index a09d20b9d6d9e61a3188b2057e79db2659893877..d400e10670b5f2448fe0255694d400a31ec1bb3b 100644 (file)
@@ -198,6 +198,13 @@ federation:
     # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
     cleanup_remote_interactions: false
 
+peertube:
+  check_latest_version:
+    # Check and notify admins of new PeerTube versions
+    enabled: true
+    # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
+    url: 'https://joinpeertube.org/api/v1/versions.json'
+
 cache:
   previews:
     size: 500 # Max number of previews you want to cache
index 31c0e6b9618f4262d4d29d912eb2740f05b57b89..895931e7cb62ef4271a975a651ffd62dd61e04e7 100644 (file)
@@ -196,6 +196,12 @@ federation:
     # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
     cleanup_remote_interactions: false
 
+peertube:
+  check_latest_version:
+    # Check and notify admins of new PeerTube versions
+    enabled: true
+    # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
+    url: 'https://joinpeertube.org/api/v1/versions.json'
 
 ###############################################################################
 #
index 33c11afc30b78099e0921eea63e966300df80790..4f0a7e5d9cd8028a63c7c5f2225e04dae1a40a78 100644 (file)
@@ -38,6 +38,10 @@ log:
 contact_form:
   enabled: true
 
+peertube:
+  check_latest_version:
+    enabled: false
+
 redundancy:
   videos:
     check_interval: '1 minute'
index 67a54a57f3ed3e7b664735063e9873001e5cf143..72b95e8426b651a53f9ecb832fda9759c300ef20 100644 (file)
@@ -53,6 +53,7 @@
     "start:server": "node dist/server --no-client",
     "update-host": "node ./dist/scripts/update-host.js",
     "create-transcoding-job": "node ./dist/scripts/create-transcoding-job.js",
+    "regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js",
     "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js",
     "print-transcode-command": "node ./dist/scripts/print-transcode-command.js",
     "test": "scripty",
     "swagger-cli": "swagger-cli",
     "sass-lint": "sass-lint"
   },
-  "resolutions": {
-    "oauth2-server": "3.1.0-beta.1",
-    "http-signature": "1.3.5"
-  },
   "dependencies": {
     "apicache": "1.6.2",
     "async": "^3.0.1",
     "deep-object-diff": "^1.1.0",
     "email-templates": "^8.0.3",
     "express": "^4.12.4",
-    "express-oauth-server": "^2.0.0",
     "express-rate-limit": "^5.0.0",
     "express-validator": "^6.4.0",
     "flat": "^5.0.0",
     "fluent-ffmpeg": "^2.1.0",
     "fs-extra": "^9.0.0",
+    "got": "^11.8.2",
     "helmet": "^4.1.0",
     "http-signature": "1.3.5",
     "ip-anonymize": "^0.1.0",
     "multer": "^1.1.0",
     "node-media-server": "^2.1.4",
     "nodemailer": "^6.0.0",
-    "oauth2-server": "3.1.0-beta.1",
+    "oauth2-server": "3.1.1",
     "parse-torrent": "^9.1.0",
     "password-generator": "^2.0.2",
     "pem": "^1.12.3",
     "pug": "^3.0.0",
     "redis": "^3.0.2",
     "reflect-metadata": "^0.1.12",
-    "request": "^2.81.0",
     "sanitize-html": "2.x",
     "scripty": "^2.0.0",
     "sequelize": "6.5.0",
index 3679dab747c02c632718da931f77507be4beafd2..5f4480c8827c608e0101e56940a66d5673f6e71a 100755 (executable)
@@ -15,6 +15,8 @@ import { format as sqlFormat } from 'sql-formatter'
 program
   .option('-l, --level [level]', 'Level log (debug/info/warn/error)')
   .option('-f, --files [file...]', 'Files to parse. If not provided, the script will parse the latest log file from config)')
+  .option('-t, --tags [tags...]', 'Display only lines with these tags')
+  .option('-nt, --not-tags [tags...]', 'Donrt display lines containing these tags')
   .parse(process.argv)
 
 const options = program.opts()
@@ -24,6 +26,7 @@ const excludedKeys = {
   message: true,
   splat: true,
   timestamp: true,
+  tags: true,
   label: true,
   sql: true
 }
@@ -93,6 +96,14 @@ function run () {
       rl.on('line', line => {
         try {
           const log = JSON.parse(line)
+          if (options.tags && !containsTags(log.tags, options.tags)) {
+            return
+          }
+
+          if (options.notTags && containsTags(log.tags, options.notTags)) {
+            return
+          }
+
           // Don't know why but loggerFormat does not remove splat key
           Object.assign(log, { splat: undefined })
 
@@ -131,3 +142,15 @@ function toTimeFormat (time: string) {
 
   return new Date(timestamp).toISOString()
 }
+
+function containsTags (loggerTags: string[], optionsTags: string[]) {
+  if (!loggerTags) return false
+
+  for (const lt of loggerTags) {
+    for (const ot of optionsTags) {
+      if (lt === ot) return true
+    }
+  }
+
+  return false
+}
diff --git a/scripts/regenerate-thumbnails.ts b/scripts/regenerate-thumbnails.ts
new file mode 100644 (file)
index 0000000..b0071ef
--- /dev/null
@@ -0,0 +1,54 @@
+import { registerTSPaths } from '../server/helpers/register-ts-paths'
+registerTSPaths()
+
+import * as Bluebird from 'bluebird'
+import * as program from 'commander'
+import { pathExists } from 'fs-extra'
+import { processImage } from '@server/helpers/image-utils'
+import { THUMBNAILS_SIZE } from '@server/initializers/constants'
+import { VideoModel } from '@server/models/video/video'
+import { MVideo } from '@server/types/models'
+import { initDatabaseModels } from '@server/initializers/database'
+
+program
+  .description('Regenerate local thumbnails using preview files')
+  .parse(process.argv)
+
+run()
+  .then(() => process.exit(0))
+  .catch(err => console.error(err))
+
+async function run () {
+  await initDatabaseModels(true)
+
+  const videos = await VideoModel.listLocal()
+
+  await Bluebird.map(videos, v => {
+    return processVideo(v)
+      .catch(err => console.error('Cannot process video %s.', v.url, err))
+  }, { concurrency: 20 })
+}
+
+async function processVideo (videoArg: MVideo) {
+  const video = await VideoModel.loadWithFiles(videoArg.id)
+
+  const thumbnail = video.getMiniature()
+  const preview = video.getPreview()
+
+  const thumbnailPath = thumbnail.getPath()
+  const previewPath = preview.getPath()
+
+  if (!await pathExists(thumbnailPath)) {
+    throw new Error(`Thumbnail ${thumbnailPath} does not exist on disk`)
+  }
+
+  if (!await pathExists(previewPath)) {
+    throw new Error(`Preview ${previewPath} does not exist on disk`)
+  }
+
+  const size = {
+    width: THUMBNAILS_SIZE.width,
+    height: THUMBNAILS_SIZE.height
+  }
+  await processImage(previewPath, thumbnailPath, size, true)
+}
index 00cd87e20f525cbec2f3d5390631c3e57bd30328..f44202c9af982bb294dd5563049c40046aa50809 100644 (file)
--- a/server.ts
+++ b/server.ts
@@ -44,7 +44,7 @@ checkFFmpeg(CONFIG)
 
 checkNodeVersion()
 
-import { checkConfig, checkActivityPubUrls } from './server/initializers/checker-after-init'
+import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './server/initializers/checker-after-init'
 
 const errorMessage = checkConfig()
 if (errorMessage !== null) {
@@ -120,6 +120,7 @@ import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
 import { PeerTubeSocket } from './server/lib/peertube-socket'
 import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
 import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler'
+import { PeerTubeVersionCheckScheduler } from './server/lib/schedulers/peertube-version-check-scheduler'
 import { Hooks } from './server/lib/plugins/hooks'
 import { PluginManager } from './server/lib/plugins/plugin-manager'
 import { LiveManager } from './server/lib/live-manager'
@@ -160,7 +161,9 @@ morgan.token('user-agent', (req: express.Request) => {
   return req.get('user-agent')
 })
 app.use(morgan('combined', {
-  stream: { write: logger.info.bind(logger) },
+  stream: {
+    write: (str: string) => logger.info(str, { tags: [ 'http' ] })
+  },
   skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping'
 }))
 
@@ -250,6 +253,9 @@ async function startApplication () {
       process.exit(-1)
     })
 
+  checkFFmpegVersion()
+    .catch(err => logger.error('Cannot check ffmpeg version', { err }))
+
   // Email initialization
   Emailer.Instance.init()
 
@@ -272,6 +278,7 @@ async function startApplication () {
   RemoveOldHistoryScheduler.Instance.enable()
   RemoveOldViewsScheduler.Instance.enable()
   PluginsCheckScheduler.Instance.enable()
+  PeerTubeVersionCheckScheduler.Instance.enable()
   AutoFollowIndexInstances.Instance.enable()
 
   // Redis initialization
index 861cc22b94978050531ca735c099f6b0feb53788..d7cee16054e47de7fa6f6a938bf96da36cdc34ce 100644 (file)
@@ -9,10 +9,10 @@ import {
   authenticate,
   ensureUserHasRight,
   jobsSortValidator,
+  paginationValidatorBuilder,
   setDefaultPagination,
   setDefaultSort
 } from '../../middlewares'
-import { paginationValidator } from '../../middlewares/validators'
 import { listJobsValidator } from '../../middlewares/validators/jobs'
 
 const jobsRouter = express.Router()
@@ -20,7 +20,7 @@ const jobsRouter = express.Router()
 jobsRouter.get('/:state?',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_JOBS),
-  paginationValidator,
+  paginationValidatorBuilder([ 'jobs' ]),
   jobsSortValidator,
   setDefaultSort,
   setDefaultPagination,
index 1c0b5edb1fdd02df278e33b696ca353a6298630e..bb69f25a1eafbdae809a46f618eb05b0105a88be 100644 (file)
@@ -205,7 +205,6 @@ async function listAvailablePlugins (req: express.Request, res: express.Response
   if (!resultList) {
     return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503)
       .json({ error: 'Plugin index unavailable. Please retry later' })
-      .end()
   }
 
   return res.json(resultList)
index 7e1b7b230b9ec9381a9917e967de982350a53301..f0cdf3a8904205c9215f60131d8dee4854c966e7 100644 (file)
@@ -1,8 +1,9 @@
 import * as express from 'express'
 import { sanitizeUrl } from '@server/helpers/core-utils'
-import { doRequest } from '@server/helpers/requests'
+import { doJSONRequest } from '@server/helpers/requests'
 import { CONFIG } from '@server/initializers/config'
 import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos'
+import { Hooks } from '@server/lib/plugins/hooks'
 import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
 import { getServerActor } from '@server/models/application/application'
 import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
@@ -22,8 +23,8 @@ import {
   paginationValidator,
   setDefaultPagination,
   setDefaultSearchSort,
-  videoChannelsSearchSortValidator,
   videoChannelsListSearchValidator,
+  videoChannelsSearchSortValidator,
   videosSearchSortValidator,
   videosSearchValidator
 } from '../../middlewares'
@@ -87,16 +88,17 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
 async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
   const result = await buildMutedForSearchIndex(res)
 
-  const body = Object.assign(query, result)
+  const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
 
   const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
 
   try {
     logger.debug('Doing video channels search index request on %s.', url, { body })
 
-    const searchIndexResult = await doRequest<ResultList<VideoChannel>>({ uri: url, body, json: true })
+    const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
+    const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
 
-    return res.json(searchIndexResult.body)
+    return res.json(jsonResult)
   } catch (err) {
     logger.warn('Cannot use search index to make video channels search.', { err })
 
@@ -107,14 +109,19 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
 async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
   const serverActor = await getServerActor()
 
-  const options = {
+  const apiOptions = await Hooks.wrapObject({
     actorId: serverActor.id,
     search: query.search,
     start: query.start,
     count: query.count,
     sort: query.sort
-  }
-  const resultList = await VideoChannelModel.searchForApi(options)
+  }, 'filter:api.search.video-channels.local.list.params')
+
+  const resultList = await Hooks.wrapPromiseFun(
+    VideoChannelModel.searchForApi,
+    apiOptions,
+    'filter:api.search.video-channels.local.list.result'
+  )
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
@@ -168,7 +175,7 @@ function searchVideos (req: express.Request, res: express.Response) {
 async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
   const result = await buildMutedForSearchIndex(res)
 
-  const body: VideosSearchQuery = Object.assign(query, result)
+  let body: VideosSearchQuery = Object.assign(query, result)
 
   // Use the default instance NSFW policy if not specified
   if (!body.nsfw) {
@@ -181,14 +188,17 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
       : 'both'
   }
 
+  body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
+
   const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
 
   try {
     logger.debug('Doing videos search index request on %s.', url, { body })
 
-    const searchIndexResult = await doRequest<ResultList<Video>>({ uri: url, body, json: true })
+    const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
+    const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
 
-    return res.json(searchIndexResult.body)
+    return res.json(jsonResult)
   } catch (err) {
     logger.warn('Cannot use search index to make video search.', { err })
 
@@ -197,13 +207,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
 }
 
 async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
-  const options = Object.assign(query, {
+  const apiOptions = await Hooks.wrapObject(Object.assign(query, {
     includeLocalVideos: true,
     nsfw: buildNSFWFilter(res, query.nsfw),
     filter: query.filter,
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined
-  })
-  const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
+  }), 'filter:api.search.videos.local.list.params')
+
+  const resultList = await Hooks.wrapPromiseFun(
+    VideoModel.searchAndPopulateAccountAndServer,
+    apiOptions,
+    'filter:api.search.videos.local.list.result'
+  )
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
index 3be1d55ae97122f649e575aaf58d16e89a8fd847..e2b1ea7cd8d8edb135ac18be1866e91fed16647a 100644 (file)
@@ -2,8 +2,10 @@ import * as express from 'express'
 import * as RateLimit from 'express-rate-limit'
 import { tokensRouter } from '@server/controllers/api/users/token'
 import { Hooks } from '@server/lib/plugins/hooks'
+import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
 import { MUser, MUserAccountDefault } from '@server/types/models'
 import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
 import { UserRegister } from '../../../../shared/models/users/user-register.model'
 import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { Emailer } from '../../../lib/emailer'
 import { Notifier } from '../../../lib/notifier'
-import { deleteUserToken } from '../../../lib/oauth-model'
 import { Redis } from '../../../lib/redis'
 import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
 import {
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history'
 import { myNotificationsRouter } from './my-notifications'
 import { mySubscriptionsRouter } from './my-subscriptions'
 import { myVideoPlaylistsRouter } from './my-video-playlists'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const auditLogger = auditLoggerFactory('users')
 
@@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) {
   const user = await userToUpdate.save()
 
   // Destroy user token to refresh rights
-  if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id)
+  if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
 
   auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
 
@@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault
   user.blockedReason = reason || null
 
   await sequelizeTypescript.transaction(async t => {
-    await deleteUserToken(user.id, t)
+    await OAuthTokenModel.deleteUserToken(user.id, t)
 
     await user.save({ transaction: t })
   })
index 5f5e4c5e6be8e1e91e075a2dfd99ba8338ddc539..0a9101a46df3340a995a7b8758f7bb3df61c4f78 100644 (file)
@@ -80,7 +80,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
     newInstanceFollower: body.newInstanceFollower,
     autoInstanceFollowing: body.autoInstanceFollowing,
     abuseNewMessage: body.abuseNewMessage,
-    abuseStateChange: body.abuseStateChange
+    abuseStateChange: body.abuseStateChange,
+    newPeerTubeVersion: body.newPeerTubeVersion,
+    newPluginVersion: body.newPluginVersion
   }
 
   await UserNotificationSettingModel.update(values, query)
index 82142935824375d42a6541e49d4e213bb01f386f..694bb0a9294130bfbc9905fb8b0f82c5b7c9c671 100644 (file)
@@ -1,11 +1,14 @@
-import { handleLogin, handleTokenRevocation } from '@server/lib/auth'
+import * as express from 'express'
 import * as RateLimit from 'express-rate-limit'
+import { v4 as uuidv4 } from 'uuid'
+import { logger } from '@server/helpers/logger'
 import { CONFIG } from '@server/initializers/config'
-import * as express from 'express'
+import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
+import { handleOAuthToken } from '@server/lib/auth/oauth'
+import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
 import { Hooks } from '@server/lib/plugins/hooks'
 import { asyncMiddleware, authenticate } from '@server/middlewares'
 import { ScopedToken } from '@shared/models/users/user-scoped-token'
-import { v4 as uuidv4 } from 'uuid'
 
 const tokensRouter = express.Router()
 
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({
 
 tokensRouter.post('/token',
   loginRateLimiter,
-  handleLogin,
-  tokenSuccess
+  asyncMiddleware(handleToken)
 )
 
 tokensRouter.post('/revoke-token',
@@ -42,10 +44,53 @@ export {
 }
 // ---------------------------------------------------------------------------
 
-function tokenSuccess (req: express.Request) {
-  const username = req.body.username
+async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const grantType = req.body.grant_type
+
+  try {
+    const bypassLogin = await buildByPassLogin(req, grantType)
+
+    const refreshTokenAuthName = grantType === 'refresh_token'
+      ? await getAuthNameFromRefreshGrant(req.body.refresh_token)
+      : undefined
+
+    const options = {
+      refreshTokenAuthName,
+      bypassLogin
+    }
+
+    const token = await handleOAuthToken(req, options)
+
+    res.set('Cache-Control', 'no-store')
+    res.set('Pragma', 'no-cache')
+
+    Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip })
+
+    return res.json({
+      token_type: 'Bearer',
 
-  Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
+      access_token: token.accessToken,
+      refresh_token: token.refreshToken,
+
+      expires_in: token.accessTokenExpiresIn,
+      refresh_token_expires_in: token.refreshTokenExpiresIn
+    })
+  } catch (err) {
+    logger.warn('Login error', { err })
+
+    return res.status(err.code || 400).json({
+      code: err.name,
+      error: err.message
+    })
+  }
+}
+
+async function handleTokenRevocation (req: express.Request, res: express.Response) {
+  const token = res.locals.oauth.token
+
+  const result = await revokeToken(token, { req, explicitLogout: true })
+
+  return res.json(result)
 }
 
 function getScopedTokens (req: express.Request, res: express.Response) {
@@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) {
     feedToken: user.feedToken
   } as ScopedToken)
 }
+
+async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
+  if (grantType !== 'password') return undefined
+
+  if (req.body.externalAuthToken) {
+    // Consistency with the getBypassFromPasswordGrant promise
+    return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
+  }
+
+  return getBypassFromPasswordGrant(req.body.username, req.body.password)
+}
index 2447c1288ad1f1514129c78966086df35d9a2c25..7fee278f28c528eb41db89d11c0425e30330f948 100644 (file)
@@ -17,7 +17,7 @@ import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../
 import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
 import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
 import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
-import { logger } from '../../../helpers/logger'
+import { logger, loggerTagsFactory } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { CONFIG } from '../../../initializers/config'
 import {
@@ -67,6 +67,7 @@ import { ownershipVideoRouter } from './ownership'
 import { rateVideoRouter } from './rate'
 import { watchingRouter } from './watching'
 
+const lTags = loggerTagsFactory('api', 'video')
 const auditLogger = auditLoggerFactory('videos')
 const videosRouter = express.Router()
 
@@ -257,14 +258,14 @@ async function addVideo (req: express.Request, res: express.Response) {
     })
 
     auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
-    logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
+    logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
 
     return { videoCreated }
   })
 
   // Create the torrent file in async way because it could be long
   createTorrentAndSetInfoHashAsync(video, videoFile)
-    .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err }))
+    .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
     .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
     .then(refreshedVideo => {
       if (!refreshedVideo) return
@@ -276,7 +277,7 @@ async function addVideo (req: express.Request, res: express.Response) {
         return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
       })
     })
-    .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err }))
+    .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
 
   if (video.state === VideoState.TO_TRANSCODE) {
     await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User)
@@ -389,7 +390,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
         new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
         oldVideoAuditView
       )
-      logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
+      logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
 
       return videoInstanceUpdated
     })
index 557cbfdfb278afde68b50b816138a2f6c4f58594..022a17ff47c8f7921bdbde3d11bf5f38ba43f040 100644 (file)
@@ -2,7 +2,9 @@ import * as express from 'express'
 import { constants, promises as fs } from 'fs'
 import { readFile } from 'fs-extra'
 import { join } from 'path'
+import { logger } from '@server/helpers/logger'
 import { CONFIG } from '@server/initializers/config'
+import { Hooks } from '@server/lib/plugins/hooks'
 import { HttpStatusCode } from '@shared/core-utils'
 import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
 import { root } from '../helpers/core-utils'
@@ -27,6 +29,7 @@ const embedMiddlewares = [
     ? embedCSP
     : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
 
+  // Set headers
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     res.removeHeader('X-Frame-Options')
 
@@ -105,6 +108,24 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
 }
 
 async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
+  const hookName = req.originalUrl.startsWith('/video-playlists/')
+    ? 'filter:html.embed.video-playlist.allowed.result'
+    : 'filter:html.embed.video.allowed.result'
+
+  const allowParameters = { req }
+
+  const allowedResult = await Hooks.wrapFun(
+    isEmbedAllowed,
+    allowParameters,
+    hookName
+  )
+
+  if (!allowedResult || allowedResult.allowed !== true) {
+    logger.info('Embed is not allowed.', { allowedResult })
+
+    return sendHTML(allowedResult?.html || '', res)
+  }
+
   const html = await ClientHtml.getEmbedHTML()
 
   return sendHTML(html, res)
@@ -158,3 +179,10 @@ function serveClientOverride (path: string) {
     }
   }
 }
+
+type AllowedResult = { allowed: boolean, html?: string }
+function isEmbedAllowed (_object: {
+  req: express.Request
+}): AllowedResult {
+  return { allowed: true }
+}
index 27caa15189a47685219df0fddf3bdd743e38fe5e..9a8194c5c4b3affe8083c64d326e68ab3f99e3f3 100644 (file)
@@ -1,8 +1,10 @@
 import * as cors from 'cors'
 import * as express from 'express'
+import { logger } from '@server/helpers/logger'
 import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
+import { Hooks } from '@server/lib/plugins/hooks'
 import { getVideoFilePath } from '@server/lib/video-paths'
-import { MVideoFile, MVideoFullLight } from '@server/types/models'
+import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
 import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
 import { VideoStreamingPlaylistType } from '@shared/models'
 import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
@@ -14,19 +16,19 @@ downloadRouter.use(cors())
 
 downloadRouter.use(
   STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
-  downloadTorrent
+  asyncMiddleware(downloadTorrent)
 )
 
 downloadRouter.use(
   STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
   asyncMiddleware(videosDownloadValidator),
-  downloadVideoFile
+  asyncMiddleware(downloadVideoFile)
 )
 
 downloadRouter.use(
   STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
   asyncMiddleware(videosDownloadValidator),
-  downloadHLSVideoFile
+  asyncMiddleware(downloadHLSVideoFile)
 )
 
 // ---------------------------------------------------------------------------
@@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
   const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
   if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
 
+  const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
+
+  const allowedResult = await Hooks.wrapFun(
+    isTorrentDownloadAllowed,
+    allowParameters,
+    'filter:api.download.torrent.allowed.result'
+  )
+
+  if (!checkAllowResult(res, allowParameters, allowedResult)) return
+
   return res.download(result.path, result.downloadName)
 }
 
-function downloadVideoFile (req: express.Request, res: express.Response) {
+async function downloadVideoFile (req: express.Request, res: express.Response) {
   const video = res.locals.videoAll
 
   const videoFile = getVideoFile(req, video.VideoFiles)
   if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
 
+  const allowParameters = { video, videoFile }
+
+  const allowedResult = await Hooks.wrapFun(
+    isVideoDownloadAllowed,
+    allowParameters,
+    'filter:api.download.video.allowed.result'
+  )
+
+  if (!checkAllowResult(res, allowParameters, allowedResult)) return
+
   return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
 }
 
-function downloadHLSVideoFile (req: express.Request, res: express.Response) {
+async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
   const video = res.locals.videoAll
-  const playlist = getHLSPlaylist(video)
-  if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end
+  const streamingPlaylist = getHLSPlaylist(video)
+  if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
 
-  const videoFile = getVideoFile(req, playlist.VideoFiles)
+  const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
   if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
 
-  const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
-  return res.download(getVideoFilePath(playlist, videoFile), filename)
+  const allowParameters = { video, streamingPlaylist, videoFile }
+
+  const allowedResult = await Hooks.wrapFun(
+    isVideoDownloadAllowed,
+    allowParameters,
+    'filter:api.download.video.allowed.result'
+  )
+
+  if (!checkAllowResult(res, allowParameters, allowedResult)) return
+
+  const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
+  return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename)
 }
 
 function getVideoFile (req: express.Request, files: MVideoFile[]) {
@@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) {
 
   return Object.assign(playlist, { Video: video })
 }
+
+type AllowedResult = {
+  allowed: boolean
+  errorMessage?: string
+}
+
+function isTorrentDownloadAllowed (_object: {
+  torrentPath: string
+}): AllowedResult {
+  return { allowed: true }
+}
+
+function isVideoDownloadAllowed (_object: {
+  video: MVideo
+  videoFile: MVideoFile
+  streamingPlaylist?: MStreamingPlaylist
+}): AllowedResult {
+  return { allowed: true }
+}
+
+function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
+  if (!result || result.allowed !== true) {
+    logger.info('Download is not allowed.', { result, allowParameters })
+    res.status(HttpStatusCode.FORBIDDEN_403)
+       .json({ error: result?.errorMessage || 'Refused download' })
+
+    return false
+  }
+
+  return true
+}
index e29a8fe1db9ee28efd7900935e6a7ac8d14c7943..921067e655623924b63b80439b9bceda281c879c 100644 (file)
@@ -1,8 +1,9 @@
 import * as express from 'express'
 import * as Feed from 'pfeed'
+import { VideoFilter } from '../../shared/models/videos/video-query.type'
 import { buildNSFWFilter } from '../helpers/express-utils'
 import { CONFIG } from '../initializers/config'
-import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants'
+import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
 import {
   asyncMiddleware,
   commonVideosFiltersValidator,
@@ -17,7 +18,6 @@ import {
 import { cacheRoute } from '../middlewares/cache'
 import { VideoModel } from '../models/video/video'
 import { VideoCommentModel } from '../models/video/video-comment'
-import { VideoFilter } from '../../shared/models/videos/video-query.type'
 
 const feedsRouter = express.Router()
 
@@ -318,9 +318,9 @@ function addVideosToFeed (feed, videos: VideoModel[]) {
       },
       thumbnail: [
         {
-          url: WEBSERVER.URL + video.getMiniatureStaticPath(),
-          height: THUMBNAILS_SIZE.height,
-          width: THUMBNAILS_SIZE.width
+          url: WEBSERVER.URL + video.getPreviewStaticPath(),
+          height: PREVIEWS_SIZE.height,
+          width: PREVIEWS_SIZE.width
         }
       ]
     })
index 6a1ccc0bf91c5439176eee6fab392e5371340a83..105f515184130196a093d5f2d842448e5e7542ca 100644 (file)
@@ -1,15 +1,15 @@
 import * as express from 'express'
-import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
 import { join } from 'path'
-import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
-import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins'
-import { serveThemeCSSValidator } from '../middlewares/validators/themes'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { logger } from '@server/helpers/logger'
+import { optionalAuthenticate } from '@server/middlewares/auth'
 import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n'
+import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
 import { PluginType } from '../../shared/models/plugins/plugin.type'
 import { isTestInstance } from '../helpers/core-utils'
-import { logger } from '@server/helpers/logger'
-import { optionalAuthenticate } from '@server/middlewares/oauth'
+import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
+import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
+import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
+import { serveThemeCSSValidator } from '../middlewares/validators/themes'
 
 const sendFileOptions = {
   maxAge: '30 days',
index 08aef29083dbb14a3753cf121b41f6d1c4dae275..e0754b501b2ce39729997c298942c20a81f9fb89 100644 (file)
@@ -3,7 +3,6 @@ import { URL } from 'url'
 import validator from 'validator'
 import { ContextType } from '@shared/models/activitypub/context'
 import { ResultList } from '../../shared/models'
-import { Activity } from '../../shared/models/activitypub'
 import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
 import { MActor, MVideoWithHost } from '../types/models'
 import { pageToStartAndCount } from './core-utils'
@@ -182,10 +181,10 @@ async function activityPubCollectionPagination (
 
 }
 
-function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) {
+function buildSignedActivity <T> (byActor: MActor, data: T, contextType?: ContextType) {
   const activity = activityPubContextify(data, contextType)
 
-  return signJsonLDObject(byActor, activity) as Promise<Activity>
+  return signJsonLDObject(byActor, activity)
 }
 
 function getAPId (activity: string | { id: string }) {
index 935fd22d9b4199d816176dca0481b61d12678466..0bd84ffaa88f2f6c59578210e7b654186673a673 100644 (file)
@@ -10,7 +10,9 @@ import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto'
 import { truncate } from 'lodash'
 import { basename, isAbsolute, join, resolve } from 'path'
 import * as pem from 'pem'
+import { pipeline } from 'stream'
 import { URL } from 'url'
+import { promisify } from 'util'
 
 const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
   if (!oldObject || typeof oldObject !== 'object') {
@@ -249,11 +251,23 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
   }
 }
 
+type SemVersion = { major: number, minor: number, patch: number }
+function parseSemVersion (s: string) {
+  const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
+
+  return {
+    major: parseInt(parsed[1]),
+    minor: parseInt(parsed[2]),
+    patch: parseInt(parsed[3])
+  } as SemVersion
+}
+
 const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
 const execPromise2 = promisify2<string, any, string>(exec)
 const execPromise = promisify1<string, string>(exec)
+const pipelinePromise = promisify(pipeline)
 
 // ---------------------------------------------------------------------------
 
@@ -284,5 +298,8 @@ export {
   createPrivateKey,
   getPublicKey,
   execPromise2,
-  execPromise
+  execPromise,
+  pipelinePromise,
+
+  parseSemVersion
 }
index da79b2782cf8d676e9e1c68be51c7ef2d6845c95..b5c96f6e764b05639ff4ca9e62a491445cb7bcc8 100644 (file)
@@ -1,16 +1,13 @@
 import validator from 'validator'
 import { Activity, ActivityType } from '../../../../shared/models/activitypub'
+import { isAbuseReasonValid } from '../abuses'
 import { exists } from '../misc'
 import { sanitizeAndCheckActorObject } from './actor'
 import { isCacheFileObjectValid } from './cache-file'
-import { isFlagActivityValid } from './flag'
 import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc'
 import { isPlaylistObjectValid } from './playlist'
-import { isDislikeActivityValid, isLikeActivityValid } from './rate'
-import { isShareActivityValid } from './share'
 import { sanitizeAndCheckVideoCommentObject } from './video-comments'
 import { sanitizeAndCheckVideoTorrentObject } from './videos'
-import { isViewActivityValid } from './view'
 
 function isRootActivityValid (activity: any) {
   return isCollection(activity) || isActivity(activity)
@@ -29,18 +26,18 @@ function isActivity (activity: any) {
 }
 
 const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
-  Create: checkCreateActivity,
-  Update: checkUpdateActivity,
-  Delete: checkDeleteActivity,
-  Follow: checkFollowActivity,
-  Accept: checkAcceptActivity,
-  Reject: checkRejectActivity,
-  Announce: checkAnnounceActivity,
-  Undo: checkUndoActivity,
-  Like: checkLikeActivity,
-  View: checkViewActivity,
-  Flag: checkFlagActivity,
-  Dislike: checkDislikeActivity
+  Create: isCreateActivityValid,
+  Update: isUpdateActivityValid,
+  Delete: isDeleteActivityValid,
+  Follow: isFollowActivityValid,
+  Accept: isAcceptActivityValid,
+  Reject: isRejectActivityValid,
+  Announce: isAnnounceActivityValid,
+  Undo: isUndoActivityValid,
+  Like: isLikeActivityValid,
+  View: isViewActivityValid,
+  Flag: isFlagActivityValid,
+  Dislike: isDislikeActivityValid
 }
 
 function isActivityValid (activity: any) {
@@ -51,34 +48,34 @@ function isActivityValid (activity: any) {
   return checker(activity)
 }
 
-// ---------------------------------------------------------------------------
-
-export {
-  isRootActivityValid,
-  isActivityValid
+function isFlagActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Flag') &&
+    isAbuseReasonValid(activity.content) &&
+    isActivityPubUrlValid(activity.object)
 }
 
-// ---------------------------------------------------------------------------
-
-function checkViewActivity (activity: any) {
-  return isBaseActivityValid(activity, 'View') &&
-    isViewActivityValid(activity)
+function isLikeActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Like') &&
+    isObjectValid(activity.object)
 }
 
-function checkFlagActivity (activity: any) {
-  return isBaseActivityValid(activity, 'Flag') &&
-    isFlagActivityValid(activity)
+function isDislikeActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Dislike') &&
+    isObjectValid(activity.object)
 }
 
-function checkDislikeActivity (activity: any) {
-  return isDislikeActivityValid(activity)
+function isAnnounceActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Announce') &&
+    isObjectValid(activity.object)
 }
 
-function checkLikeActivity (activity: any) {
-  return isLikeActivityValid(activity)
+function isViewActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'View') &&
+    isActivityPubUrlValid(activity.actor) &&
+    isActivityPubUrlValid(activity.object)
 }
 
-function checkCreateActivity (activity: any) {
+function isCreateActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Create') &&
     (
       isViewActivityValid(activity.object) ||
@@ -92,7 +89,7 @@ function checkCreateActivity (activity: any) {
     )
 }
 
-function checkUpdateActivity (activity: any) {
+function isUpdateActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Update') &&
     (
       isCacheFileObjectValid(activity.object) ||
@@ -102,36 +99,51 @@ function checkUpdateActivity (activity: any) {
     )
 }
 
-function checkDeleteActivity (activity: any) {
+function isDeleteActivityValid (activity: any) {
   // We don't really check objects
   return isBaseActivityValid(activity, 'Delete') &&
     isObjectValid(activity.object)
 }
 
-function checkFollowActivity (activity: any) {
+function isFollowActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Follow') &&
     isObjectValid(activity.object)
 }
 
-function checkAcceptActivity (activity: any) {
+function isAcceptActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Accept')
 }
 
-function checkRejectActivity (activity: any) {
+function isRejectActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Reject')
 }
 
-function checkAnnounceActivity (activity: any) {
-  return isShareActivityValid(activity)
-}
-
-function checkUndoActivity (activity: any) {
+function isUndoActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Undo') &&
     (
-      checkFollowActivity(activity.object) ||
-      checkLikeActivity(activity.object) ||
-      checkDislikeActivity(activity.object) ||
-      checkAnnounceActivity(activity.object) ||
-      checkCreateActivity(activity.object)
+      isFollowActivityValid(activity.object) ||
+      isLikeActivityValid(activity.object) ||
+      isDislikeActivityValid(activity.object) ||
+      isAnnounceActivityValid(activity.object) ||
+      isCreateActivityValid(activity.object)
     )
 }
+
+// ---------------------------------------------------------------------------
+
+export {
+  isRootActivityValid,
+  isActivityValid,
+  isFlagActivityValid,
+  isLikeActivityValid,
+  isDislikeActivityValid,
+  isAnnounceActivityValid,
+  isViewActivityValid,
+  isCreateActivityValid,
+  isUpdateActivityValid,
+  isDeleteActivityValid,
+  isFollowActivityValid,
+  isAcceptActivityValid,
+  isRejectActivityValid,
+  isUndoActivityValid
+}
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts
deleted file mode 100644 (file)
index dc90b36..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-import { isActivityPubUrlValid } from './misc'
-import { isAbuseReasonValid } from '../abuses'
-
-function isFlagActivityValid (activity: any) {
-  return activity.type === 'Flag' &&
-    isAbuseReasonValid(activity.content) &&
-    isActivityPubUrlValid(activity.object)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  isFlagActivityValid
-}
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts
deleted file mode 100644 (file)
index aafdda4..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import { isBaseActivityValid, isObjectValid } from './misc'
-
-function isLikeActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Like') &&
-    isObjectValid(activity.object)
-}
-
-function isDislikeActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Dislike') &&
-    isObjectValid(activity.object)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  isDislikeActivityValid,
-  isLikeActivityValid
-}
diff --git a/server/helpers/custom-validators/activitypub/share.ts b/server/helpers/custom-validators/activitypub/share.ts
deleted file mode 100644 (file)
index fb5e4c0..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { isBaseActivityValid, isObjectValid } from './misc'
-
-function isShareActivityValid (activity: any) {
-  return isBaseActivityValid(activity, 'Announce') &&
-    isObjectValid(activity.object)
-}
-// ---------------------------------------------------------------------------
-
-export {
-  isShareActivityValid
-}
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts
deleted file mode 100644 (file)
index 41d1646..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-import { isActivityPubUrlValid } from './misc'
-
-function isViewActivityValid (activity: any) {
-  return activity.type === 'View' &&
-    isActivityPubUrlValid(activity.actor) &&
-    isActivityPubUrlValid(activity.object)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  isViewActivityValid
-}
index 8a33b895bc0508ea3cb7953d38360e50f8981516..252c107db762c319cbbcdf057e447f51b30feb75 100644 (file)
@@ -1,10 +1,9 @@
-import { exists } from './misc'
 import validator from 'validator'
-import { UserNotificationType } from '../../../shared/models/users'
 import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
+import { exists } from './misc'
 
 function isUserNotificationTypeValid (value: any) {
-  return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined
+  return exists(value) && validator.isInt('' + value)
 }
 
 function isUserNotificationSettingValid (value: any) {
index 62002596664441c7f244311853119b2aee13f767..69cd397b91600aa44457f6b4739ddf0ac80139e9 100644 (file)
@@ -5,7 +5,7 @@ import { dirname, join } from 'path'
 import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
 import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
 import { CONFIG } from '../initializers/config'
-import { promisify0 } from './core-utils'
+import { execPromise, promisify0 } from './core-utils'
 import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
 import { processImage } from './image-utils'
 import { logger } from './logger'
@@ -649,6 +649,24 @@ function getFFmpeg (input: string, type: 'live' | 'vod') {
   return command
 }
 
+function getFFmpegVersion () {
+  return new Promise<string>((res, rej) => {
+    (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => {
+      if (err) return rej(err)
+      if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path'))
+
+      return execPromise(`${ffmpegPath} -version`)
+        .then(stdout => {
+          const parsed = stdout.match(/ffmpeg version .(\d+\.\d+\.\d+)/)
+          if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`))
+
+          return res(parsed[1])
+        })
+        .catch(err => rej(err))
+    })
+  })
+}
+
 async function runCommand (options: {
   command: ffmpeg.FfmpegCommand
   silent?: boolean // false
@@ -695,6 +713,7 @@ export {
   TranscodeOptionsType,
   transcode,
   runCommand,
+  getFFmpegVersion,
 
   resetSupportedEncoders,
 
index 6917a64d969b073777a2d18ec778c9530b11397a..a112fd30006b1d2f7706c5946d386261d6b23b61 100644 (file)
@@ -48,7 +48,7 @@ function getLoggerReplacer () {
 }
 
 const consoleLoggerFormat = winston.format.printf(info => {
-  const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql' ]
+  const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql', 'tags' ]
 
   const obj = omit(info, ...toOmit)
 
@@ -150,6 +150,13 @@ const bunyanLogger = {
   error: bunyanLogFactory('error'),
   fatal: bunyanLogFactory('error')
 }
+
+function loggerTagsFactory (...defaultTags: string[]) {
+  return (...tags: string[]) => {
+    return { tags: defaultTags.concat(tags) }
+  }
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -159,5 +166,6 @@ export {
   consoleLoggerFormat,
   jsonLoggerFormat,
   logger,
+  loggerTagsFactory,
   bunyanLogger
 }
index 994f725d88074dc891c50c801182d59a60a59ea3..bc6f1d0748cb0a78615ef396cbd9f95460593426 100644 (file)
@@ -84,7 +84,7 @@ async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any)
   return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
 }
 
-async function signJsonLDObject (byActor: MActor, data: any) {
+async function signJsonLDObject <T> (byActor: MActor, data: T) {
   const signature = {
     type: 'RsaSignature2017',
     creator: byActor.url,
index b556c392e262b321075afaa5c73d6dd79c9a7ea0..fd2a56f30c717e6ce892b1fd90deca6af228a327 100644 (file)
-import * as Bluebird from 'bluebird'
 import { createWriteStream, remove } from 'fs-extra'
-import * as request from 'request'
+import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got'
+import { join } from 'path'
+import { CONFIG } from '../initializers/config'
 import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants'
+import { pipelinePromise } from './core-utils'
 import { processImage } from './image-utils'
-import { join } from 'path'
 import { logger } from './logger'
-import { CONFIG } from '../initializers/config'
 
-function doRequest <T> (
-  requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean },
-  bodyKBLimit = 1000 // 1MB
-): Bluebird<{ response: request.RequestResponse, body: T }> {
-  if (!(requestOptions.headers)) requestOptions.headers = {}
-  requestOptions.headers['User-Agent'] = getUserAgent()
+export interface PeerTubeRequestError extends Error {
+  statusCode?: number
+  responseBody?: any
+}
 
-  if (requestOptions.activityPub === true) {
-    requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
+const httpSignature = require('http-signature')
+
+type PeerTubeRequestOptions = {
+  activityPub?: boolean
+  bodyKBLimit?: number // 1MB
+  httpSignature?: {
+    algorithm: string
+    authorizationHeaderName: string
+    keyId: string
+    key: string
+    headers: string[]
   }
+  jsonResponse?: boolean
+} & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'>
+
+const peertubeGot = got.extend({
+  headers: {
+    'user-agent': getUserAgent()
+  },
+
+  handlers: [
+    (options, next) => {
+      const promiseOrStream = next(options) as CancelableRequest<any>
+      const bodyKBLimit = options.context?.bodyKBLimit as number
+      if (!bodyKBLimit) throw new Error('No KB limit for this request')
+
+      const bodyLimit = bodyKBLimit * 1000
+
+      /* eslint-disable @typescript-eslint/no-floating-promises */
+      promiseOrStream.on('downloadProgress', progress => {
+        if (progress.transferred > bodyLimit && progress.percent !== 1) {
+          const message = `Exceeded the download limit of ${bodyLimit} B`
+          logger.warn(message)
+
+          // CancelableRequest
+          if (promiseOrStream.cancel) {
+            promiseOrStream.cancel()
+            return
+          }
+
+          // Stream
+          (promiseOrStream as any).destroy()
+        }
+      })
 
-  return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
-    request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
-      .on('data', onRequestDataLengthCheck(bodyKBLimit))
-  })
+      return promiseOrStream
+    }
+  ],
+
+  hooks: {
+    beforeRequest: [
+      options => {
+        const headers = options.headers || {}
+        headers['host'] = options.url.host
+      },
+
+      options => {
+        const httpSignatureOptions = options.context?.httpSignature
+
+        if (httpSignatureOptions) {
+          const method = options.method ?? 'GET'
+          const path = options.path ?? options.url.pathname
+
+          if (!method || !path) {
+            throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`)
+          }
+
+          httpSignature.signRequest({
+            getHeader: function (header) {
+              return options.headers[header]
+            },
+
+            setHeader: function (header, value) {
+              options.headers[header] = value
+            },
+
+            method,
+            path
+          }, httpSignatureOptions)
+        }
+      }
+    ]
+  }
+})
+
+function doRequest (url: string, options: PeerTubeRequestOptions = {}) {
+  const gotOptions = buildGotOptions(options)
+
+  return peertubeGot(url, gotOptions)
+    .catch(err => { throw buildRequestError(err) })
 }
 
-function doRequestAndSaveToFile (
-  requestOptions: request.CoreOptions & request.UriOptions,
+function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) {
+  const gotOptions = buildGotOptions(options)
+
+  return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' })
+    .catch(err => { throw buildRequestError(err) })
+}
+
+async function doRequestAndSaveToFile (
+  url: string,
   destPath: string,
-  bodyKBLimit = 10000 // 10MB
+  options: PeerTubeRequestOptions = {}
 ) {
-  if (!requestOptions.headers) requestOptions.headers = {}
-  requestOptions.headers['User-Agent'] = getUserAgent()
-
-  return new Bluebird<void>((res, rej) => {
-    const file = createWriteStream(destPath)
-    file.on('finish', () => res())
+  const gotOptions = buildGotOptions(options)
 
-    request(requestOptions)
-      .on('data', onRequestDataLengthCheck(bodyKBLimit))
-      .on('error', err => {
-        file.close()
+  const outFile = createWriteStream(destPath)
 
-        remove(destPath)
-          .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err }))
+  try {
+    await pipelinePromise(
+      peertubeGot.stream(url, gotOptions),
+      outFile
+    )
+  } catch (err) {
+    remove(destPath)
+      .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err }))
 
-        return rej(err)
-      })
-      .pipe(file)
-  })
+    throw buildRequestError(err)
+  }
 }
 
 async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
   const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
-  await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
+  await doRequestAndSaveToFile(url, tmpPath)
 
   const destPath = join(destDir, destName)
 
@@ -73,24 +156,46 @@ function getUserAgent () {
 
 export {
   doRequest,
+  doJSONRequest,
   doRequestAndSaveToFile,
   downloadImage
 }
 
 // ---------------------------------------------------------------------------
 
-// Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3
-function onRequestDataLengthCheck (bodyKBLimit: number) {
-  let bufferLength = 0
-  const bytesLimit = bodyKBLimit * 1000
+function buildGotOptions (options: PeerTubeRequestOptions) {
+  const { activityPub, bodyKBLimit = 1000 } = options
 
-  return function (chunk) {
-    bufferLength += chunk.length
-    if (bufferLength > bytesLimit) {
-      this.abort()
+  const context = { bodyKBLimit, httpSignature: options.httpSignature }
 
-      const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`)
-      this.emit('error', error)
-    }
+  let headers = options.headers || {}
+
+  if (!headers.date) {
+    headers = { ...headers, date: new Date().toUTCString() }
+  }
+
+  if (activityPub && !headers.accept) {
+    headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER }
   }
+
+  return {
+    method: options.method,
+    json: options.json,
+    searchParams: options.searchParams,
+    headers,
+    context
+  }
+}
+
+function buildRequestError (error: RequestError) {
+  const newError: PeerTubeRequestError = new Error(error.message)
+  newError.name = error.name
+  newError.stack = error.stack
+
+  if (error.response) {
+    newError.responseBody = error.response.body
+    newError.statusCode = error.response.statusCode
+  }
+
+  return newError
 }
index 8537a57722c57f9079ceb746dbc28f8ead0e7e4f..9d2e54fb53dd22a8379b9518c6b45d4a0cef9082 100644 (file)
@@ -1,13 +1,13 @@
 import { createWriteStream } from 'fs'
 import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra'
+import got from 'got'
 import { join } from 'path'
-import * as request from 'request'
 import { CONFIG } from '@server/initializers/config'
 import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
 import { VideoResolution } from '../../shared/models/videos'
 import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
 import { getEnabledResolutions } from '../lib/video-transcoding'
-import { peertubeTruncate, root } from './core-utils'
+import { peertubeTruncate, pipelinePromise, root } from './core-utils'
 import { isVideoFileExtnameValid } from './custom-validators/videos'
 import { logger } from './logger'
 import { generateVideoImportTmpPath } from './utils'
@@ -195,55 +195,32 @@ async function updateYoutubeDLBinary () {
 
   await ensureDir(binDirectory)
 
-  return new Promise<void>(res => {
-    request.get(url, { followRedirect: false }, (err, result) => {
-      if (err) {
-        logger.error('Cannot update youtube-dl.', { err })
-        return res()
-      }
-
-      if (result.statusCode !== HttpStatusCode.FOUND_302) {
-        logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
-        return res()
-      }
-
-      const url = result.headers.location
-      const downloadFile = request.get(url)
-      const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1]
-
-      downloadFile.on('response', result => {
-        if (result.statusCode !== HttpStatusCode.OK_200) {
-          logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', result.statusCode)
-          return res()
-        }
-
-        const writeStream = createWriteStream(bin, { mode: 493 }).on('error', err => {
-          logger.error('youtube-dl update error in write stream', { err })
-          return res()
-        })
+  try {
+    const result = await got(url, { followRedirect: false })
 
-        downloadFile.pipe(writeStream)
-      })
+    if (result.statusCode !== HttpStatusCode.FOUND_302) {
+      logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
+      return
+    }
 
-      downloadFile.on('error', err => {
-        logger.error('youtube-dl update error.', { err })
-        return res()
-      })
+    const newUrl = result.headers.location
+    const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
 
-      downloadFile.on('end', () => {
-        const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
-        writeFile(detailsPath, details, { encoding: 'utf8' }, err => {
-          if (err) {
-            logger.error('youtube-dl update error: cannot write details.', { err })
-            return res()
-          }
+    const downloadFileStream = got.stream(newUrl)
+    const writeStream = createWriteStream(bin, { mode: 493 })
 
-          logger.info('youtube-dl updated to version %s.', newVersion)
-          return res()
-        })
-      })
-    })
-  })
+    await pipelinePromise(
+      downloadFileStream,
+      writeStream
+    )
+
+    const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
+    await writeFile(detailsPath, details, { encoding: 'utf8' })
+
+    logger.info('youtube-dl updated to version %s.', newVersion)
+  } catch (err) {
+    logger.error('Cannot update youtube-dl.', { err })
+  }
 }
 
 async function safeGetYoutubeDL () {
index 2b00e2047e9770c57f8c896b59df94f0d6fe035b..a93c8b7fd3778204d0e4be383cb8bd107b902073 100644 (file)
@@ -1,16 +1,17 @@
 import * as config from 'config'
-import { isProdInstance, isTestInstance } from '../helpers/core-utils'
-import { UserModel } from '../models/account/user'
-import { getServerActor, ApplicationModel } from '../models/application/application'
-import { OAuthClientModel } from '../models/oauth/oauth-client'
+import { uniq } from 'lodash'
 import { URL } from 'url'
-import { CONFIG, isEmailEnabled } from './config'
-import { logger } from '../helpers/logger'
+import { getFFmpegVersion } from '@server/helpers/ffmpeg-utils'
+import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
 import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
+import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils'
 import { isArray } from '../helpers/custom-validators/misc'
-import { uniq } from 'lodash'
+import { logger } from '../helpers/logger'
+import { UserModel } from '../models/account/user'
+import { ApplicationModel, getServerActor } from '../models/application/application'
+import { OAuthClientModel } from '../models/oauth/oauth-client'
+import { CONFIG, isEmailEnabled } from './config'
 import { WEBSERVER } from './constants'
-import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
 
 async function checkActivityPubUrls () {
   const actor = await getServerActor()
@@ -176,11 +177,21 @@ async function applicationExist () {
   return totalApplication !== 0
 }
 
+async function checkFFmpegVersion () {
+  const version = await getFFmpegVersion()
+  const { major, minor } = parseSemVersion(version)
+
+  if (major < 4 || (major === 4 && minor < 1)) {
+    logger.warn('Your ffmpeg version (%s) is outdated. PeerTube supports ffmpeg >= 4.1. Please upgrade.', version)
+  }
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   checkConfig,
   clientsExist,
+  checkFFmpegVersion,
   usersExist,
   applicationExist,
   checkActivityPubUrls
index 565e0d1fa15bd1d4ac4fabb240d87a66b074a7a5..e92cc4d2cfd30e00ba01bec7686dcf23140fcf05 100644 (file)
@@ -1,5 +1,5 @@
 import * as config from 'config'
-import { promisify0 } from '../helpers/core-utils'
+import { parseSemVersion, promisify0 } from '../helpers/core-utils'
 import { logger } from '../helpers/logger'
 
 // ONLY USE CORE MODULES IN THIS FILE!
@@ -37,6 +37,7 @@ function checkMissedConfig () {
     'theme.default',
     'remote_redundancy.videos.accept_from',
     'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
+    'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
     'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
     'search.search_index.disable_local_search', 'search.search_index.is_default_search',
     'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
@@ -102,8 +103,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
 
 function checkNodeVersion () {
   const v = process.version
-  const majorString = v.split('.')[0].replace('v', '')
-  const major = parseInt(majorString, 10)
+  const { major } = parseSemVersion(v)
 
   logger.debug('Checking NodeJS version %s.', v)
 
index c16b63c33903ec1952a1aaa733a681dedc3d200b..48e7f7397be50e4d21f7e73a939b96e5b26bde9c 100644 (file)
@@ -163,6 +163,12 @@ const CONFIG = {
       CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
     }
   },
+  PEERTUBE: {
+    CHECK_LATEST_VERSION: {
+      ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'),
+      URL: config.get<string>('peertube.check_latest_version.url')
+    }
+  },
   ADMIN: {
     get EMAIL () { return config.get<string>('admin.email') }
   },
index 1623e6f42c2fa7ead4b2cb18e0687452a22fea50..b37aeb6223c8e3c09bbf8e6a041ee556735e6965 100644 (file)
@@ -24,12 +24,12 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 610
+const LAST_MIGRATION_VERSION = 625
 
 // ---------------------------------------------------------------------------
 
 const API_VERSION = 'v1'
-const PEERTUBE_VERSION = require(join(root(), 'package.json')).version
+const PEERTUBE_VERSION: string = require(join(root(), 'package.json')).version
 
 const PAGINATION = {
   GLOBAL: {
@@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = {
   updateVideos: 60000, // 1 minute
   youtubeDLUpdate: 60000 * 60 * 24, // 1 day
   checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
+  checkPeerTubeVersion: 60000 * 60 * 24, // 1 day
   autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
   removeOldViews: 60000 * 60 * 24, // 1 day
   removeOldHistory: 60000 * 60 * 24, // 1 day
@@ -763,6 +764,7 @@ if (isTestInstance() === true) {
   SCHEDULER_INTERVALS_MS.updateVideos = 5000
   SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
   SCHEDULER_INTERVALS_MS.updateInboxStats = 5000
+  SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000
   REPEAT_JOBS['videos-views'] = { every: 5000 }
   REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }
 
index 1f2b6d5211c18b335759536810ac2cf8729a089c..8378fa9827f366f02de83148e369711e246bcbd9 100644 (file)
@@ -76,7 +76,7 @@ const sequelizeTypescript = new SequelizeTypescript({
       newMessage += ' in ' + benchmark + 'ms'
     }
 
-    logger.debug(newMessage, { sql: message })
+    logger.debug(newMessage, { sql: message, tags: [ 'sql' ] })
   }
 })
 
diff --git a/server/initializers/migrations/0615-latest-versions-notification-settings.ts b/server/initializers/migrations/0615-latest-versions-notification-settings.ts
new file mode 100644 (file)
index 0000000..86bf560
--- /dev/null
@@ -0,0 +1,44 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+  db: any
+}): Promise<void> {
+  {
+    const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
+
+    for (const column of notificationSettingColumns) {
+      const data = {
+        type: Sequelize.INTEGER,
+        defaultValue: null,
+        allowNull: true
+      }
+      await utils.queryInterface.addColumn('userNotificationSetting', column, data)
+    }
+
+    {
+      const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
+      await utils.sequelize.query(query)
+    }
+
+    for (const column of notificationSettingColumns) {
+      const data = {
+        type: Sequelize.INTEGER,
+        defaultValue: null,
+        allowNull: false
+      }
+      await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
+    }
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts
new file mode 100644 (file)
index 0000000..a689b18
--- /dev/null
@@ -0,0 +1,27 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+  db: any
+}): Promise<void> {
+
+  {
+    const data = {
+      type: Sequelize.STRING,
+      defaultValue: null,
+      allowNull: true
+    }
+    await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts
new file mode 100644 (file)
index 0000000..77f395c
--- /dev/null
@@ -0,0 +1,26 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+  db: any
+}): Promise<void> {
+
+  {
+    await utils.sequelize.query(`
+      ALTER TABLE "userNotification"
+      ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
+      ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+    `)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index a726f9e209d0093f8510bec3cad4cd1ecbb1f2ae..3c9a7ba023a18b7f8b9e0e8afe72f81d30e178ca 100644 (file)
@@ -1,26 +1,28 @@
 import * as Bluebird from 'bluebird'
+import { extname } from 'path'
 import { Op, Transaction } from 'sequelize'
 import { URL } from 'url'
 import { v4 as uuidv4 } from 'uuid'
+import { getServerActor } from '@server/models/application/application'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
 import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
+import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
 import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
 import { logger } from '../../helpers/logger'
 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
-import { doRequest } from '../../helpers/requests'
+import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
 import { getUrlFromWebfinger } from '../../helpers/webfinger'
 import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
+import { sequelizeTypescript } from '../../initializers/database'
 import { AccountModel } from '../../models/account/account'
 import { ActorModel } from '../../models/activitypub/actor'
 import { AvatarModel } from '../../models/avatar/avatar'
 import { ServerModel } from '../../models/server/server'
 import { VideoChannelModel } from '../../models/video/video-channel'
-import { JobQueue } from '../job-queue'
-import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
-import { sequelizeTypescript } from '../../initializers/database'
 import {
   MAccount,
   MAccountDefault,
@@ -34,9 +36,7 @@ import {
   MActorId,
   MChannel
 } from '../../types/models'
-import { extname } from 'path'
-import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { JobQueue } from '../job-queue'
 
 // Set account keys, this could be long so process after the account creation and do not block the client
 async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
@@ -209,16 +209,10 @@ async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction)
 }
 
 async function fetchActorTotalItems (url: string) {
-  const options = {
-    uri: url,
-    method: 'GET',
-    json: true,
-    activityPub: true
-  }
-
   try {
-    const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options)
-    return body.totalItems ? body.totalItems : 0
+    const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
+
+    return body.totalItems || 0
   } catch (err) {
     logger.warn('Cannot fetch remote actor count %s.', url, { err })
     return 0
@@ -285,16 +279,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
       actorUrl = actor.url
     }
 
-    const { result, statusCode } = await fetchRemoteActor(actorUrl)
-
-    if (statusCode === HttpStatusCode.NOT_FOUND_404) {
-      logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
-      actor.Account
-        ? await actor.Account.destroy()
-        : await actor.VideoChannel.destroy()
-
-      return { actor: undefined, refreshed: false }
-    }
+    const { result } = await fetchRemoteActor(actorUrl)
 
     if (result === undefined) {
       logger.warn('Cannot fetch remote actor in refresh actor.')
@@ -334,6 +319,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
       return { refreshed: true, actor }
     })
   } catch (err) {
+    if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
+      logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
+      actor.Account
+        ? await actor.Account.destroy()
+        : await actor.VideoChannel.destroy()
+
+      return { actor: undefined, refreshed: false }
+    }
+
     logger.warn('Cannot refresh actor %s.', actor.url, { err })
     return { actor, refreshed: false }
   }
@@ -449,26 +443,19 @@ type FetchRemoteActorResult = {
   attributedTo: ActivityPubAttributedTo[]
 }
 async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
-  const options = {
-    uri: actorUrl,
-    method: 'GET',
-    json: true,
-    activityPub: true
-  }
-
   logger.info('Fetching remote actor %s.', actorUrl)
 
-  const requestResult = await doRequest<ActivityPubActor>(options)
+  const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
   const actorJSON = requestResult.body
 
   if (sanitizeAndCheckActorObject(actorJSON) === false) {
     logger.debug('Remote actor JSON is not valid.', { actorJSON })
-    return { result: undefined, statusCode: requestResult.response.statusCode }
+    return { result: undefined, statusCode: requestResult.statusCode }
   }
 
   if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
     logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
-    return { result: undefined, statusCode: requestResult.response.statusCode }
+    return { result: undefined, statusCode: requestResult.statusCode }
   }
 
   const followersCount = await fetchActorTotalItems(actorJSON.followers)
@@ -496,7 +483,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
 
   const name = actorJSON.name || actorJSON.preferredUsername
   return {
-    statusCode: requestResult.response.statusCode,
+    statusCode: requestResult.statusCode,
     result: {
       actor,
       name,
index 1ed105bbe78044c97c4c32eba04b3c6bf60644ed..278abf7de0b3a9637b380521dd9f78158535686f 100644 (file)
@@ -1,27 +1,26 @@
-import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
-import { doRequest } from '../../helpers/requests'
-import { logger } from '../../helpers/logger'
 import * as Bluebird from 'bluebird'
-import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
 import { URL } from 'url'
+import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
+import { logger } from '../../helpers/logger'
+import { doJSONRequest } from '../../helpers/requests'
+import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
 
 type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
 type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
 
-async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
-  logger.info('Crawling ActivityPub data on %s.', uri)
+async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
+  let url = argUrl
+
+  logger.info('Crawling ActivityPub data on %s.', url)
 
   const options = {
-    method: 'GET',
-    uri,
-    json: true,
     activityPub: true,
     timeout: REQUEST_TIMEOUT
   }
 
   const startDate = new Date()
 
-  const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
+  const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
   const firstBody = response.body
 
   const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
       const remoteHost = new URL(nextLink).host
       if (remoteHost === WEBSERVER.HOST) continue
 
-      options.uri = nextLink
+      url = nextLink
 
-      const res = await doRequest<ActivityPubOrderedCollection<T>>(options)
+      const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
       body = res.body
     } else {
       // nextLink is already the object we want
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
 
     if (Array.isArray(body.orderedItems)) {
       const items = body.orderedItems
-      logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri)
+      logger.info('Processing %i ActivityPub items for %s.', items.length, url)
 
       await handler(items)
     }
index d5a3ef7c8ba97069f768c1a7cb428a662956b674..7166c68a6529451dbf94ef9f38fa6cfb506505f2 100644 (file)
@@ -1,24 +1,24 @@
+import * as Bluebird from 'bluebird'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
 import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
-import { crawlCollectionPage } from './crawl'
-import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
+import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { checkUrlsSameHost } from '../../helpers/activitypub'
+import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
 import { isArray } from '../../helpers/custom-validators/misc'
-import { getOrCreateActorAndServerAndModel } from './actor'
 import { logger } from '../../helpers/logger'
+import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
+import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
+import { sequelizeTypescript } from '../../initializers/database'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
-import { doRequest } from '../../helpers/requests'
-import { checkUrlsSameHost } from '../../helpers/activitypub'
-import * as Bluebird from 'bluebird'
-import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
-import { getOrCreateVideoAndAccountAndChannel } from './videos'
-import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
 import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
-import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { sequelizeTypescript } from '../../initializers/database'
-import { createPlaylistMiniatureFromUrl } from '../thumbnail'
-import { FilteredModelAttributes } from '../../types/sequelize'
 import { MAccountDefault, MAccountId, MVideoId } from '../../types/models'
 import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { FilteredModelAttributes } from '../../types/sequelize'
+import { createPlaylistMiniatureFromUrl } from '../thumbnail'
+import { getOrCreateActorAndServerAndModel } from './actor'
+import { crawlCollectionPage } from './crawl'
+import { getOrCreateVideoAndAccountAndChannel } from './videos'
 
 function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
   const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount
       if (exists === true) return
 
       // Fetch url
-      const { body } = await doRequest<PlaylistObject>({
-        uri: playlistUrl,
-        json: true,
-        activityPub: true
-      })
+      const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true })
 
       if (!isPlaylistObjectValid(body)) {
         throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`)
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
   if (!videoPlaylist.isOutdated()) return videoPlaylist
 
   try {
-    const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
-    if (statusCode === HttpStatusCode.NOT_FOUND_404) {
-      logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
-
-      await videoPlaylist.destroy()
-      return undefined
-    }
+    const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
 
     if (playlistObject === undefined) {
       logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
 
     return videoPlaylist
   } catch (err) {
+    if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
+      logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
+
+      await videoPlaylist.destroy()
+      return undefined
+    }
+
     logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
 
     await videoPlaylist.setAsRefreshed()
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
 
   await Bluebird.map(elementUrls, async elementUrl => {
     try {
-      // Fetch url
-      const { body } = await doRequest<PlaylistElementObject>({
-        uri: elementUrl,
-        json: true,
-        activityPub: true
-      })
+      const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true })
 
       if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`)
 
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
 }
 
 async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
-  const options = {
-    uri: playlistUrl,
-    method: 'GET',
-    json: true,
-    activityPub: true
-  }
-
   logger.info('Fetching remote playlist %s.', playlistUrl)
 
-  const { response, body } = await doRequest<any>(options)
+  const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true })
 
   if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
     logger.debug('Remote video playlist JSON is not valid.', { body })
-    return { statusCode: response.statusCode, playlistObject: undefined }
+    return { statusCode, playlistObject: undefined }
   }
 
-  return { statusCode: response.statusCode, playlistObject: body }
+  return { statusCode, playlistObject: body }
 }
index 9fb2182249dd09127d3a67795b70a03bf0cf5299..baded642acb1d41ac1a4e73bf5c26e2dfa791670 100644 (file)
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
 import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
-import { logger } from '../../../helpers/logger'
+import { logger, loggerTagsFactory } from '../../../helpers/logger'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import {
   MActorLight,
@@ -18,10 +18,12 @@ import {
 import { getServerActor } from '@server/models/application/application'
 import { ContextType } from '@shared/models/activitypub/context'
 
+const lTags = loggerTagsFactory('ap', 'create')
+
 async function sendCreateVideo (video: MVideoAP, t: Transaction) {
   if (!video.hasPrivacyForFederation()) return undefined
 
-  logger.info('Creating job to send video creation of %s.', video.url)
+  logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
 
   const byActor = video.VideoChannel.Account.Actor
   const videoObject = video.toActivityPubObject()
@@ -37,7 +39,7 @@ async function sendCreateCacheFile (
   video: MVideoAccountLight,
   fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
 ) {
-  logger.info('Creating job to send file cache of %s.', fileRedundancy.url)
+  logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid))
 
   return sendVideoRelatedCreateActivity({
     byActor,
@@ -51,7 +53,7 @@ async function sendCreateCacheFile (
 async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
   if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
 
-  logger.info('Creating job to send create video playlist of %s.', playlist.url)
+  logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
 
   const byActor = playlist.OwnerAccount.Actor
   const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
index 1f8a8f3c48c872d0ac1568267e63d6883146defb..c22fa0893916625ba489d65c3388f38cb73f2d95 100644 (file)
@@ -1,15 +1,17 @@
+import * as Bluebird from 'bluebird'
 import { Transaction } from 'sequelize'
+import { getServerActor } from '@server/models/application/application'
+import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
+import { logger, loggerTagsFactory } from '../../helpers/logger'
+import { doJSONRequest } from '../../helpers/requests'
+import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
 import { VideoShareModel } from '../../models/video/video-share'
+import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
+import { getOrCreateActorAndServerAndModel } from './actor'
 import { sendUndoAnnounce, sendVideoAnnounce } from './send'
 import { getLocalVideoAnnounceActivityPubUrl } from './url'
-import * as Bluebird from 'bluebird'
-import { doRequest } from '../../helpers/requests'
-import { getOrCreateActorAndServerAndModel } from './actor'
-import { logger } from '../../helpers/logger'
-import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
-import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
-import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
-import { getServerActor } from '@server/models/application/application'
+
+const lTags = loggerTagsFactory('share')
 
 async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
   if (!video.hasPrivacyForFederation()) return undefined
@@ -25,7 +27,10 @@ async function changeVideoChannelShare (
   oldVideoChannel: MChannelActorLight,
   t: Transaction
 ) {
-  logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name)
+  logger.info(
+    'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name,
+    lTags(video.uuid)
+  )
 
   await undoShareByVideoChannel(video, oldVideoChannel, t)
 
@@ -35,12 +40,7 @@ async function changeVideoChannelShare (
 async function addVideoShares (shareUrls: string[], video: MVideoId) {
   await Bluebird.map(shareUrls, async shareUrl => {
     try {
-      // Fetch url
-      const { body } = await doRequest<any>({
-        uri: shareUrl,
-        json: true,
-        activityPub: true
-      })
+      const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true })
       if (!body || !body.actor) throw new Error('Body or body actor is invalid')
 
       const actorUrl = getAPId(body.actor)
index d025ed7f12eb348bec081647b68894a1fd164525..e23e0c0e71f051f34e18fbed284bc795f6b37489 100644 (file)
@@ -1,13 +1,13 @@
+import * as Bluebird from 'bluebird'
+import { checkUrlsSameHost } from '../../helpers/activitypub'
 import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
 import { logger } from '../../helpers/logger'
-import { doRequest } from '../../helpers/requests'
+import { doJSONRequest } from '../../helpers/requests'
 import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
 import { VideoCommentModel } from '../../models/video/video-comment'
+import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
 import { getOrCreateActorAndServerAndModel } from './actor'
 import { getOrCreateVideoAndAccountAndChannel } from './videos'
-import * as Bluebird from 'bluebird'
-import { checkUrlsSameHost } from '../../helpers/activitypub'
-import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
 
 type ResolveThreadParams = {
   url: string
@@ -18,8 +18,12 @@ type ResolveThreadParams = {
 type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
 
 async function addVideoComments (commentUrls: string[]) {
-  return Bluebird.map(commentUrls, commentUrl => {
-    return resolveThread({ url: commentUrl, isVideo: false })
+  return Bluebird.map(commentUrls, async commentUrl => {
+    try {
+      await resolveThread({ url: commentUrl, isVideo: false })
+    } catch (err) {
+      logger.warn('Cannot resolve thread %s.', commentUrl, { err })
+    }
   }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
 }
 
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
     throw new Error('Recursion limit reached when resolving a thread')
   }
 
-  const { body } = await doRequest<any>({
-    uri: url,
-    json: true,
-    activityPub: true
-  })
+  const { body } = await doJSONRequest<any>(url, { activityPub: true })
 
   if (sanitizeAndCheckVideoCommentObject(body) === false) {
     throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))
index e246b1313c85b039fb330719bf26cb62170f3fd7..f40c07fea368f90e092163baa545a7ce50c41773 100644 (file)
@@ -1,26 +1,22 @@
+import * as Bluebird from 'bluebird'
 import { Transaction } from 'sequelize'
-import { sendLike, sendUndoDislike, sendUndoLike } from './send'
+import { doJSONRequest } from '@server/helpers/requests'
 import { VideoRateType } from '../../../shared/models/videos'
-import * as Bluebird from 'bluebird'
-import { getOrCreateActorAndServerAndModel } from './actor'
-import { AccountVideoRateModel } from '../../models/account/account-video-rate'
+import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
 import { logger } from '../../helpers/logger'
 import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
-import { doRequest } from '../../helpers/requests'
-import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
-import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
-import { sendDislike } from './send/send-dislike'
+import { AccountVideoRateModel } from '../../models/account/account-video-rate'
 import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
+import { getOrCreateActorAndServerAndModel } from './actor'
+import { sendLike, sendUndoDislike, sendUndoLike } from './send'
+import { sendDislike } from './send/send-dislike'
+import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
 
 async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
   await Bluebird.map(ratesUrl, async rateUrl => {
     try {
       // Fetch url
-      const { body } = await doRequest<any>({
-        uri: rateUrl,
-        json: true,
-        activityPub: true
-      })
+      const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
       if (!body || !body.actor) throw new Error('Body or body actor is invalid')
 
       const actorUrl = getAPId(body.actor)
index c02578aadcbb4167ac975500b49510f51ef0153d..d484edd369dda3ca74fec2123e6119b22f9ddfaa 100644 (file)
@@ -2,7 +2,6 @@ import * as Bluebird from 'bluebird'
 import { maxBy, minBy } from 'lodash'
 import * as magnetUtil from 'magnet-uri'
 import { basename, join } from 'path'
-import * as request from 'request'
 import { Transaction } from 'sequelize/types'
 import { TrackerModel } from '@server/models/server/tracker'
 import { VideoLiveModel } from '@server/models/video/video-live'
@@ -31,7 +30,7 @@ import { isArray } from '../../helpers/custom-validators/misc'
 import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
 import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
 import { logger } from '../../helpers/logger'
-import { doRequest } from '../../helpers/requests'
+import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
 import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
 import {
   ACTIVITY_PUB,
@@ -115,36 +114,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
   }
 }
 
-async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> {
-  const options = {
-    uri: videoUrl,
-    method: 'GET',
-    json: true,
-    activityPub: true
-  }
-
+async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
   logger.info('Fetching remote video %s.', videoUrl)
 
-  const { response, body } = await doRequest<any>(options)
+  const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
 
   if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
     logger.debug('Remote video JSON is not valid.', { body })
-    return { response, videoObject: undefined }
+    return { statusCode, videoObject: undefined }
   }
 
-  return { response, videoObject: body }
+  return { statusCode, videoObject: body }
 }
 
 async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
   const host = video.VideoChannel.Account.Actor.Server.host
   const path = video.getDescriptionAPIPath()
-  const options = {
-    uri: REMOTE_SCHEME.HTTP + '://' + host + path,
-    json: true
-  }
+  const url = REMOTE_SCHEME.HTTP + '://' + host + path
 
-  const { body } = await doRequest<any>(options)
-  return body.description ? body.description : ''
+  const { body } = await doJSONRequest<any>(url)
+  return body.description || ''
 }
 
 function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) {
@@ -534,14 +523,7 @@ async function refreshVideoIfNeeded (options: {
     : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
 
   try {
-    const { response, videoObject } = await fetchRemoteVideo(video.url)
-    if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
-      logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
-
-      // Video does not exist anymore
-      await video.destroy()
-      return undefined
-    }
+    const { videoObject } = await fetchRemoteVideo(video.url)
 
     if (videoObject === undefined) {
       logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
@@ -565,6 +547,14 @@ async function refreshVideoIfNeeded (options: {
 
     return video
   } catch (err) {
+    if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
+      logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
+
+      // Video does not exist anymore
+      await video.destroy()
+      return undefined
+    }
+
     logger.warn('Cannot refresh video %s.', options.video.url, { err })
 
     ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
similarity index 61%
rename from server/lib/auth.ts
rename to server/lib/auth/external-auth.ts
index dbd421a7b3d6e0b33c1ecbf5dc2286475cc36452..80f5064b68cd3764b28868efb166553fec843655 100644 (file)
@@ -1,28 +1,16 @@
+
 import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
 import { logger } from '@server/helpers/logger'
 import { generateRandomString } from '@server/helpers/utils'
-import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
-import { revokeToken } from '@server/lib/oauth-model'
+import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
 import { PluginManager } from '@server/lib/plugins/plugin-manager'
 import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
-import { UserRole } from '@shared/models'
 import {
   RegisterServerAuthenticatedResult,
   RegisterServerAuthPassOptions,
   RegisterServerExternalAuthenticatedResult
 } from '@server/types/plugins/register-server-auth.model'
-import * as express from 'express'
-import * as OAuthServer from 'express-oauth-server'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-
-const oAuthServer = new OAuthServer({
-  useErrorHandler: true,
-  accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
-  refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
-  allowExtendedTokenAttributes: true,
-  continueMiddleware: true,
-  model: require('./oauth-model')
-})
+import { UserRole } from '@shared/models'
 
 // Token is the key, expiration date is the value
 const authBypassTokens = new Map<string, {
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, {
   npmName: string
 }>()
 
-async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const grantType = req.body.grant_type
-
-  if (grantType === 'password') {
-    if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
-    else await proxifyPasswordGrant(req, res)
-  } else if (grantType === 'refresh_token') {
-    await proxifyRefreshGrant(req, res)
-  }
-
-  return forwardTokenReq(req, res, next)
-}
-
-async function handleTokenRevocation (req: express.Request, res: express.Response) {
-  const token = res.locals.oauth.token
-
-  res.locals.explicitLogout = true
-  const result = await revokeToken(token)
-
-  // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
-  // oAuthServer.revoke(req, res, err => {
-  //   if (err) {
-  //     logger.warn('Error in revoke token handler.', { err })
-  //
-  //     return res.status(err.status)
-  //               .json({
-  //                 error: err.message,
-  //                 code: err.name
-  //               })
-  //               .end()
-  //   }
-  // })
-
-  return res.json(result)
-}
-
 async function onExternalUserAuthenticated (options: {
   npmName: string
   authName: string
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: {
     authName
   })
 
-  // Cleanup
+  // Cleanup expired tokens
   const now = new Date()
   for (const [ key, value ] of authBypassTokens) {
     if (value.expires.getTime() < now.getTime()) {
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: {
   res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
 }
 
-// ---------------------------------------------------------------------------
-
-export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
-
-// ---------------------------------------------------------------------------
-
-function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
-  return oAuthServer.token()(req, res, err => {
-    if (err) {
-      logger.warn('Login error.', { err })
-
-      return res.status(err.status)
-        .json({
-          error: err.message,
-          code: err.name
-        })
-    }
-
-    if (next) return next()
-  })
-}
-
-async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
-  const refreshToken = req.body.refresh_token
-  if (!refreshToken) return
+async function getAuthNameFromRefreshGrant (refreshToken?: string) {
+  if (!refreshToken) return undefined
 
   const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
-  if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName
+
+  return tokenModel?.authName
 }
 
-async function proxifyPasswordGrant (req: express.Request, res: express.Response) {
+async function getBypassFromPasswordGrant (username: string, password: string) {
   const plugins = PluginManager.Instance.getIdAndPassAuths()
   const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
 
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
   })
 
   const loginOptions = {
-    id: req.body.username,
-    password: req.body.password
+    id: username,
+    password
   }
 
   for (const pluginAuth of pluginAuths) {
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
         authName, npmName, loginOptions.id
       )
 
-      res.locals.bypassLogin = {
+      return {
         bypass: true,
         pluginName: pluginAuth.npmName,
         authName: authOptions.authName,
         user: buildUserResult(loginResult)
       }
-
-      return
     } catch (err) {
       logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
     }
   }
+
+  return undefined
 }
 
-function proxifyExternalAuthBypass (req: express.Request, res: express.Response) {
-  const obj = authBypassTokens.get(req.body.externalAuthToken)
-  if (!obj) {
-    logger.error('Cannot authenticate user with unknown bypass token')
-    return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
-  }
+function getBypassFromExternalAuth (username: string, externalAuthToken: string) {
+  const obj = authBypassTokens.get(externalAuthToken)
+  if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
 
   const { expires, user, authName, npmName } = obj
 
   const now = new Date()
   if (now.getTime() > expires.getTime()) {
-    logger.error('Cannot authenticate user with an expired external auth token')
-    return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
+    throw new Error('Cannot authenticate user with an expired external auth token')
   }
 
-  if (user.username !== req.body.username) {
-    logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username)
-    return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
+  if (user.username !== username) {
+    throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`)
   }
 
-  // Bypass oauth library validation
-  req.body.password = 'fake'
-
   logger.info(
     'Auth success with external auth method %s of plugin %s for %s.',
     authName, npmName, user.email
   )
 
-  res.locals.bypassLogin = {
+  return {
     bypass: true,
     pluginName: npmName,
     authName: authName,
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
     displayName: pluginResult.displayName || pluginResult.username
   }
 }
+
+// ---------------------------------------------------------------------------
+
+export {
+  onExternalUserAuthenticated,
+  getBypassFromExternalAuth,
+  getAuthNameFromRefreshGrant,
+  getBypassFromPasswordGrant
+}
similarity index 63%
rename from server/lib/oauth-model.ts
rename to server/lib/auth/oauth-model.ts
index a2c53a2c9afb6fed41f543097a9b39a13fb0b288..b9c69eb2db59f4b2fb7fd8dbc834ae2b8a889e34 100644 (file)
@@ -1,49 +1,36 @@
 import * as express from 'express'
-import * as LRUCache from 'lru-cache'
 import { AccessDeniedError } from 'oauth2-server'
-import { Transaction } from 'sequelize'
 import { PluginManager } from '@server/lib/plugins/plugin-manager'
 import { ActorModel } from '@server/models/activitypub/actor'
+import { MOAuthClient } from '@server/types/models'
 import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
 import { MUser } from '@server/types/models/user/user'
 import { UserAdminFlag } from '@shared/models/users/user-flag.model'
 import { UserRole } from '@shared/models/users/user-role'
-import { logger } from '../helpers/logger'
-import { CONFIG } from '../initializers/config'
-import { LRU_CACHE } from '../initializers/constants'
-import { UserModel } from '../models/account/user'
-import { OAuthClientModel } from '../models/oauth/oauth-client'
-import { OAuthTokenModel } from '../models/oauth/oauth-token'
-import { createUserAccountAndChannelAndPlaylist } from './user'
-
-type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
-
-const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
-const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
-
-// ---------------------------------------------------------------------------
-
-function deleteUserToken (userId: number, t?: Transaction) {
-  clearCacheByUserId(userId)
-
-  return OAuthTokenModel.deleteUserToken(userId, t)
+import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
+import { UserModel } from '../../models/account/user'
+import { OAuthClientModel } from '../../models/oauth/oauth-client'
+import { OAuthTokenModel } from '../../models/oauth/oauth-token'
+import { createUserAccountAndChannelAndPlaylist } from '../user'
+import { TokensCache } from './tokens-cache'
+
+type TokenInfo = {
+  accessToken: string
+  refreshToken: string
+  accessTokenExpiresAt: Date
+  refreshTokenExpiresAt: Date
 }
 
-function clearCacheByUserId (userId: number) {
-  const token = userHavingToken.get(userId)
-
-  if (token !== undefined) {
-    accessTokenCache.del(token)
-    userHavingToken.del(userId)
-  }
-}
-
-function clearCacheByToken (token: string) {
-  const tokenModel = accessTokenCache.get(token)
-
-  if (tokenModel !== undefined) {
-    userHavingToken.del(tokenModel.userId)
-    accessTokenCache.del(token)
+export type BypassLogin = {
+  bypass: boolean
+  pluginName: string
+  authName?: string
+  user: {
+    username: string
+    email: string
+    displayName: string
+    role: UserRole
   }
 }
 
@@ -54,15 +41,12 @@ async function getAccessToken (bearerToken: string) {
 
   let tokenModel: MOAuthTokenUser
 
-  if (accessTokenCache.has(bearerToken)) {
-    tokenModel = accessTokenCache.get(bearerToken)
+  if (TokensCache.Instance.hasToken(bearerToken)) {
+    tokenModel = TokensCache.Instance.getByToken(bearerToken)
   } else {
     tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
 
-    if (tokenModel) {
-      accessTokenCache.set(bearerToken, tokenModel)
-      userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
-    }
+    if (tokenModel) TokensCache.Instance.setToken(tokenModel)
   }
 
   if (!tokenModel) return undefined
@@ -99,16 +83,13 @@ async function getRefreshToken (refreshToken: string) {
   return tokenInfo
 }
 
-async function getUser (usernameOrEmail?: string, password?: string) {
-  const res: express.Response = this.request.res
-
+async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) {
   // Special treatment coming from a plugin
-  if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) {
-    const obj = res.locals.bypassLogin
-    logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
+  if (bypassLogin && bypassLogin.bypass === true) {
+    logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
 
-    let user = await UserModel.loadByEmail(obj.user.email)
-    if (!user) user = await createUserFromExternal(obj.pluginName, obj.user)
+    let user = await UserModel.loadByEmail(bypassLogin.user.email)
+    if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
 
     // Cannot create a user
     if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
@@ -117,7 +98,7 @@ async function getUser (usernameOrEmail?: string, password?: string) {
     // Then we just go through a regular login process
     if (user.pluginAuth !== null) {
       // This user does not belong to this plugin, skip it
-      if (user.pluginAuth !== obj.pluginName) return null
+      if (user.pluginAuth !== bypassLogin.pluginName) return null
 
       checkUserValidityOrThrow(user)
 
@@ -143,18 +124,25 @@ async function getUser (usernameOrEmail?: string, password?: string) {
   return user
 }
 
-async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> {
-  const res: express.Response = this.request.res
+async function revokeToken (
+  tokenInfo: { refreshToken: string },
+  options: {
+    req?: express.Request
+    explicitLogout?: boolean
+  } = {}
+): Promise<{ success: boolean, redirectUrl?: string }> {
+  const { req, explicitLogout } = options
+
   const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
 
   if (token) {
     let redirectUrl: string
 
-    if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) {
-      redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request)
+    if (explicitLogout === true && token.User.pluginAuth && token.authName) {
+      redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req)
     }
 
-    clearCacheByToken(token.accessToken)
+    TokensCache.Instance.clearCacheByToken(token.accessToken)
 
     token.destroy()
          .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
@@ -165,14 +153,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ
   return { success: false }
 }
 
-async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
-  const res: express.Response = this.request.res
-
+async function saveToken (
+  token: TokenInfo,
+  client: MOAuthClient,
+  user: MUser,
+  options: {
+    refreshTokenAuthName?: string
+    bypassLogin?: BypassLogin
+  } = {}
+) {
+  const { refreshTokenAuthName, bypassLogin } = options
   let authName: string = null
-  if (res.locals.bypassLogin?.bypass === true) {
-    authName = res.locals.bypassLogin.authName
-  } else if (res.locals.refreshTokenAuthName) {
-    authName = res.locals.refreshTokenAuthName
+
+  if (bypassLogin?.bypass === true) {
+    authName = bypassLogin.authName
+  } else if (refreshTokenAuthName) {
+    authName = refreshTokenAuthName
   }
 
   logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
@@ -199,17 +195,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
     refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt,
     client,
     user,
-    refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000)
+    accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt),
+    refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt)
   }
 }
 
-// ---------------------------------------------------------------------------
-
-// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
 export {
-  deleteUserToken,
-  clearCacheByUserId,
-  clearCacheByToken,
   getAccessToken,
   getClient,
   getRefreshToken,
@@ -218,6 +209,8 @@ export {
   saveToken
 }
 
+// ---------------------------------------------------------------------------
+
 async function createUserFromExternal (pluginAuth: string, options: {
   username: string
   email: string
@@ -252,3 +245,7 @@ async function createUserFromExternal (pluginAuth: string, options: {
 function checkUserValidityOrThrow (user: MUser) {
   if (user.blocked) throw new AccessDeniedError('User is blocked.')
 }
+
+function buildExpiresIn (expiresAt: Date) {
+  return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000)
+}
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts
new file mode 100644 (file)
index 0000000..5b6130d
--- /dev/null
@@ -0,0 +1,180 @@
+import * as express from 'express'
+import {
+  InvalidClientError,
+  InvalidGrantError,
+  InvalidRequestError,
+  Request,
+  Response,
+  UnauthorizedClientError,
+  UnsupportedGrantTypeError
+} from 'oauth2-server'
+import { randomBytesPromise, sha1 } from '@server/helpers/core-utils'
+import { MOAuthClient } from '@server/types/models'
+import { OAUTH_LIFETIME } from '../../initializers/constants'
+import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
+
+/**
+ *
+ * Reimplement some functions of OAuth2Server to inject external auth methods
+ *
+ */
+
+const oAuthServer = new (require('oauth2-server'))({
+  accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
+  refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
+
+  // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
+  model: require('./oauth-model')
+})
+
+// ---------------------------------------------------------------------------
+
+async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) {
+  const request = new Request(req)
+  const { refreshTokenAuthName, bypassLogin } = options
+
+  if (request.method !== 'POST') {
+    throw new InvalidRequestError('Invalid request: method must be POST')
+  }
+
+  if (!request.is([ 'application/x-www-form-urlencoded' ])) {
+    throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')
+  }
+
+  const clientId = request.body.client_id
+  const clientSecret = request.body.client_secret
+
+  if (!clientId || !clientSecret) {
+    throw new InvalidClientError('Invalid client: cannot retrieve client credentials')
+  }
+
+  const client = await getClient(clientId, clientSecret)
+  if (!client) {
+    throw new InvalidClientError('Invalid client: client is invalid')
+  }
+
+  const grantType = request.body.grant_type
+  if (!grantType) {
+    throw new InvalidRequestError('Missing parameter: `grant_type`')
+  }
+
+  if (![ 'password', 'refresh_token' ].includes(grantType)) {
+    throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid')
+  }
+
+  if (!client.grants.includes(grantType)) {
+    throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid')
+  }
+
+  if (grantType === 'password') {
+    return handlePasswordGrant({
+      request,
+      client,
+      bypassLogin
+    })
+  }
+
+  return handleRefreshGrant({
+    request,
+    client,
+    refreshTokenAuthName
+  })
+}
+
+async function handleOAuthAuthenticate (
+  req: express.Request,
+  res: express.Response,
+  authenticateInQuery = false
+) {
+  const options = authenticateInQuery
+    ? { allowBearerTokensInQueryString: true }
+    : {}
+
+  return oAuthServer.authenticate(new Request(req), new Response(res), options)
+}
+
+export {
+  handleOAuthToken,
+  handleOAuthAuthenticate
+}
+
+// ---------------------------------------------------------------------------
+
+async function handlePasswordGrant (options: {
+  request: Request
+  client: MOAuthClient
+  bypassLogin?: BypassLogin
+}) {
+  const { request, client, bypassLogin } = options
+
+  if (!request.body.username) {
+    throw new InvalidRequestError('Missing parameter: `username`')
+  }
+
+  if (!bypassLogin && !request.body.password) {
+    throw new InvalidRequestError('Missing parameter: `password`')
+  }
+
+  const user = await getUser(request.body.username, request.body.password, bypassLogin)
+  if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid')
+
+  const token = await buildToken()
+
+  return saveToken(token, client, user, { bypassLogin })
+}
+
+async function handleRefreshGrant (options: {
+  request: Request
+  client: MOAuthClient
+  refreshTokenAuthName: string
+}) {
+  const { request, client, refreshTokenAuthName } = options
+
+  if (!request.body.refresh_token) {
+    throw new InvalidRequestError('Missing parameter: `refresh_token`')
+  }
+
+  const refreshToken = await getRefreshToken(request.body.refresh_token)
+
+  if (!refreshToken) {
+    throw new InvalidGrantError('Invalid grant: refresh token is invalid')
+  }
+
+  if (refreshToken.client.id !== client.id) {
+    throw new InvalidGrantError('Invalid grant: refresh token is invalid')
+  }
+
+  if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) {
+    throw new InvalidGrantError('Invalid grant: refresh token has expired')
+  }
+
+  await revokeToken({ refreshToken: refreshToken.refreshToken })
+
+  const token = await buildToken()
+
+  return saveToken(token, client, refreshToken.user, { refreshTokenAuthName })
+}
+
+function generateRandomToken () {
+  return randomBytesPromise(256)
+    .then(buffer => sha1(buffer))
+}
+
+function getTokenExpiresAt (type: 'access' | 'refresh') {
+  const lifetime = type === 'access'
+    ? OAUTH_LIFETIME.ACCESS_TOKEN
+    : OAUTH_LIFETIME.REFRESH_TOKEN
+
+  return new Date(Date.now() + lifetime * 1000)
+}
+
+async function buildToken () {
+  const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ])
+
+  return {
+    accessToken,
+    refreshToken,
+    accessTokenExpiresAt: getTokenExpiresAt('access'),
+    refreshTokenExpiresAt: getTokenExpiresAt('refresh')
+  }
+}
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts
new file mode 100644 (file)
index 0000000..b027ce6
--- /dev/null
@@ -0,0 +1,52 @@
+import * as LRUCache from 'lru-cache'
+import { MOAuthTokenUser } from '@server/types/models'
+import { LRU_CACHE } from '../../initializers/constants'
+
+export class TokensCache {
+
+  private static instance: TokensCache
+
+  private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
+  private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
+
+  private constructor () { }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+
+  hasToken (token: string) {
+    return this.accessTokenCache.has(token)
+  }
+
+  getByToken (token: string) {
+    return this.accessTokenCache.get(token)
+  }
+
+  setToken (token: MOAuthTokenUser) {
+    this.accessTokenCache.set(token.accessToken, token)
+    this.userHavingToken.set(token.userId, token.accessToken)
+  }
+
+  deleteUserToken (userId: number) {
+    this.clearCacheByUserId(userId)
+  }
+
+  clearCacheByUserId (userId: number) {
+    const token = this.userHavingToken.get(userId)
+
+    if (token !== undefined) {
+      this.accessTokenCache.del(token)
+      this.userHavingToken.del(userId)
+    }
+  }
+
+  clearCacheByToken (token: string) {
+    const tokenModel = this.accessTokenCache.get(token)
+
+    if (tokenModel !== undefined) {
+      this.userHavingToken.del(tokenModel.userId)
+      this.accessTokenCache.del(token)
+    }
+  }
+}
index 969eae77b81ee948a10ea23043f4f7d58773d0b4..ce4134d5904ddd5a87d58f2758fd0152d0af17e7 100644 (file)
@@ -7,12 +7,12 @@ import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/m
 import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
 import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
 import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
-import { SendEmailOptions } from '../../shared/models/server/emailer.model'
+import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
 import { isTestInstance, root } from '../helpers/core-utils'
 import { bunyanLogger, logger } from '../helpers/logger'
 import { CONFIG, isEmailEnabled } from '../initializers/config'
 import { WEBSERVER } from '../initializers/constants'
-import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
+import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models'
 import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
 import { JobQueue } from './job-queue'
 
@@ -403,7 +403,7 @@ class Emailer {
   }
 
   async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
-    const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
+    const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
     const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
     const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
 
@@ -417,7 +417,7 @@ class Emailer {
         videoName: videoBlacklist.Video.name,
         action: {
           text: 'Review autoblacklist',
-          url: VIDEO_AUTO_BLACKLIST_URL
+          url: videoAutoBlacklistUrl
         }
       }
     }
@@ -472,6 +472,36 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
+  addNewPeerTubeVersionNotification (to: string[], latestVersion: string) {
+    const emailPayload: EmailPayload = {
+      to,
+      template: 'peertube-version-new',
+      subject: `A new PeerTube version is available: ${latestVersion}`,
+      locals: {
+        latestVersion
+      }
+    }
+
+    return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+  }
+
+  addNewPlugionVersionNotification (to: string[], plugin: MPlugin) {
+    const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type
+
+    const emailPayload: EmailPayload = {
+      to,
+      template: 'plugin-version-new',
+      subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`,
+      locals: {
+        pluginName: plugin.name,
+        latestVersion: plugin.latestVersion,
+        pluginUrl
+      }
+    }
+
+    return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+  }
+
   addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
     const emailPayload: EmailPayload = {
       template: 'password-reset',
@@ -569,26 +599,27 @@ class Emailer {
     })
 
     for (const to of options.to) {
-      await email
-        .send(merge(
-          {
-            template: 'common',
-            message: {
-              to,
-              from: options.from,
-              subject: options.subject,
-              replyTo: options.replyTo
-            },
-            locals: { // default variables available in all templates
-              WEBSERVER,
-              EMAIL: CONFIG.EMAIL,
-              instanceName: CONFIG.INSTANCE.NAME,
-              text: options.text,
-              subject: options.subject
-            }
-          },
-          options // overriden/new variables given for a specific template in the payload
-        ) as SendEmailOptions)
+      const baseOptions: SendEmailDefaultOptions = {
+        template: 'common',
+        message: {
+          to,
+          from: options.from,
+          subject: options.subject,
+          replyTo: options.replyTo
+        },
+        locals: { // default variables available in all templates
+          WEBSERVER,
+          EMAIL: CONFIG.EMAIL,
+          instanceName: CONFIG.INSTANCE.NAME,
+          text: options.text,
+          subject: options.subject
+        }
+      }
+
+      // overriden/new variables given for a specific template in the payload
+      const sendOptions = merge(baseOptions, options)
+
+      await email.send(sendOptions)
         .then(res => logger.debug('Sent email.', { res }))
         .catch(err => logger.error('Error in email sender.', { err }))
     }
diff --git a/server/lib/emails/peertube-version-new/html.pug b/server/lib/emails/peertube-version-new/html.pug
new file mode 100644 (file)
index 0000000..2f4d939
--- /dev/null
@@ -0,0 +1,9 @@
+extends ../common/greetings
+
+block title
+  | New PeerTube version available
+
+block content
+  p
+    | A new version of PeerTube is available: #{latestVersion}.
+    | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].
diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug
new file mode 100644 (file)
index 0000000..86d3d87
--- /dev/null
@@ -0,0 +1,9 @@
+extends ../common/greetings
+
+block title
+  | New plugin version available
+
+block content
+  p
+    | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
+    | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].
index ee0447010b6087c8d0d5f561ca93b085b2cd7ac0..58e2260b63bff999749ffe75b4fe6bc80a1a01e5 100644 (file)
@@ -41,7 +41,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> {
     const remoteUrl = videoCaption.getFileUrl(video)
     const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename)
 
-    await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
+    await doRequestAndSaveToFile(remoteUrl, destPath)
 
     return { isOwned: false, path: destPath }
   }
index ee72cd3f9b6afce9cf77c6b98f0e47e00921cc42..dd3a84aca60a7c8fe5d0dbf7c6a3676bea57d3aa 100644 (file)
@@ -39,7 +39,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
     const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
 
     const remoteUrl = preview.getFileUrl(video)
-    await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
+    await doRequestAndSaveToFile(remoteUrl, destPath)
 
     logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
 
index ca0e1770daf2e5f340379e97b6b73cc68cfa00b0..23217f1403119acfdb1f5b943c51f73260eb0fd4 100644 (file)
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config'
 import { FILES_CACHE } from '../../initializers/constants'
 import { VideoModel } from '../../models/video/video'
 import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
+import { MVideo, MVideoFile } from '@server/types/models'
 
 class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
 
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
     const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
     if (!file) return undefined
 
-    if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) }
+    if (file.getVideo().isOwned()) {
+      const downloadName = this.buildDownloadName(file.getVideo(), file)
+
+      return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
+    }
 
     return this.loadRemoteFile(filename)
   }
@@ -41,12 +46,16 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
     const remoteUrl = file.getRemoteTorrentUrl(video)
     const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename)
 
-    await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
+    await doRequestAndSaveToFile(remoteUrl, destPath)
 
-    const downloadName = `${video.name}-${file.resolution}p.torrent`
+    const downloadName = this.buildDownloadName(video, file)
 
     return { isOwned: false, path: destPath, downloadName }
   }
+
+  private buildDownloadName (video: MVideo, file: MVideoFile) {
+    return `${video.name}-${file.resolution}p.torrent`
+  }
 }
 
 export {
index 04187668c7afd4f2b24b8a50da8dad624c112edd..84539e2c1f5cd00fc7a9bf3e5649b153994e31f6 100644 (file)
@@ -135,7 +135,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
         const destPath = join(tmpDirectory, basename(fileUrl))
 
         const bodyKBLimit = 10 * 1000 * 1000 // 10GB
-        await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit)
+        await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit })
       }
 
       clearTimeout(timer)
@@ -156,7 +156,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
   }
 
   async function fetchUniqUrls (playlistUrl: string) {
-    const { body } = await doRequest<string>({ uri: playlistUrl })
+    const { body } = await doRequest(playlistUrl)
 
     if (!body) return []
 
index b58bbc98347124be85b7da90253a785730811623..1caca1dcc04bb386d9c4efa7751bea6f85b964f9 100644 (file)
@@ -1,10 +1,13 @@
 import * as Bluebird from 'bluebird'
 import * as Bull from 'bull'
 import { checkUrlsSameHost } from '@server/helpers/activitypub'
-import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate'
-import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share'
+import {
+  isAnnounceActivityValid,
+  isDislikeActivityValid,
+  isLikeActivityValid
+} from '@server/helpers/custom-validators/activitypub/activity'
 import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
-import { doRequest } from '@server/helpers/requests'
+import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
 import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
 import { VideoModel } from '@server/models/video/video'
 import { VideoCommentModel } from '@server/models/video/video-comment'
@@ -78,44 +81,44 @@ async function updateObjectIfNeeded <T> (
   updater: (url: string, newUrl: string) => Promise<T>,
   deleter: (url: string) => Promise<T>
 ): Promise<{ data: T, status: 'deleted' | 'updated' } | null> {
-  // Fetch url
-  const { response, body } = await doRequest<any>({
-    uri: url,
-    json: true,
-    activityPub: true
-  })
-
-  // Does not exist anymore, remove entry
-  if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
+  const on404OrTombstone = async () => {
     logger.info('Removing remote AP object %s.', url)
     const data = await deleter(url)
 
-    return { status: 'deleted', data }
+    return { status: 'deleted' as 'deleted', data }
   }
 
-  // If not same id, check same host and update
-  if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)
+  try {
+    const { body } = await doJSONRequest<any>(url, { activityPub: true })
 
-  if (body.type === 'Tombstone') {
-    logger.info('Removing remote AP object %s.', url)
-    const data = await deleter(url)
+    // If not same id, check same host and update
+    if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)
 
-    return { status: 'deleted', data }
-  }
+    if (body.type === 'Tombstone') {
+      return on404OrTombstone()
+    }
 
-  const newUrl = body.id
-  if (newUrl !== url) {
-    if (checkUrlsSameHost(newUrl, url) !== true) {
-      throw new Error(`New url ${newUrl} has not the same host than old url ${url}`)
+    const newUrl = body.id
+    if (newUrl !== url) {
+      if (checkUrlsSameHost(newUrl, url) !== true) {
+        throw new Error(`New url ${newUrl} has not the same host than old url ${url}`)
+      }
+
+      logger.info('Updating remote AP object %s.', url)
+      const data = await updater(url, newUrl)
+
+      return { status: 'updated', data }
     }
 
-    logger.info('Updating remote AP object %s.', url)
-    const data = await updater(url, newUrl)
+    return null
+  } catch (err) {
+    // Does not exist anymore, remove entry
+    if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
+      return on404OrTombstone()
+    }
 
-    return { status: 'updated', data }
+    throw err
   }
-
-  return null
 }
 
 function rateOptionsFactory () {
@@ -149,7 +152,7 @@ function rateOptionsFactory () {
 
 function shareOptionsFactory () {
   return {
-    bodyValidator: (body: any) => isShareActivityValid(body),
+    bodyValidator: (body: any) => isAnnounceActivityValid(body),
 
     updater: async (url: string, newUrl: string) => {
       const share = await VideoShareModel.loadByUrl(url, undefined)
index 7174786d6084271172ad9e496846b95233870d6c..c69ff9e83fd2dcf5a920e51c47ad55a7d15ae66e 100644 (file)
@@ -16,8 +16,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
   const httpSignatureOptions = await buildSignedRequestOptions(payload)
 
   const options = {
-    method: 'POST',
-    uri: '',
+    method: 'POST' as 'POST',
     json: body,
     httpSignature: httpSignatureOptions,
     timeout: REQUEST_TIMEOUT,
@@ -28,7 +27,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
   const goodUrls: string[] = []
 
   await Bluebird.map(payload.uris, uri => {
-    return doRequest(Object.assign({}, options, { uri }))
+    return doRequest(uri, options)
       .then(() => goodUrls.push(uri))
       .catch(() => badUrls.push(uri))
   }, { concurrency: BROADCAST_CONCURRENCY })
index 74989d62e5dd9088b9fa31250875bc254a6c6633..585dad671e04f7c5074646920b197a8ac6945836 100644 (file)
@@ -16,8 +16,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
   const httpSignatureOptions = await buildSignedRequestOptions(payload)
 
   const options = {
-    method: 'POST',
-    uri,
+    method: 'POST' as 'POST',
     json: body,
     httpSignature: httpSignatureOptions,
     timeout: REQUEST_TIMEOUT,
@@ -25,7 +24,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
   }
 
   try {
-    await doRequest(options)
+    await doRequest(uri, options)
     ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], [])
   } catch (err) {
     ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ])
index c030d31ef78cd063aecbb4daac6825c4f15a8900..e8a91450dec8dc89213d8972b82e49a4b4adf7e1 100644 (file)
@@ -6,21 +6,24 @@ import { getServerActor } from '@server/models/application/application'
 import { buildDigest } from '@server/helpers/peertube-crypto'
 import { ContextType } from '@shared/models/activitypub/context'
 
-type Payload = { body: any, contextType?: ContextType, signatureActorId?: number }
+type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number }
 
-async function computeBody (payload: Payload) {
+async function computeBody <T> (
+  payload: Payload<T>
+): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> {
   let body = payload.body
 
   if (payload.signatureActorId) {
     const actorSignature = await ActorModel.load(payload.signatureActorId)
     if (!actorSignature) throw new Error('Unknown signature actor id.')
+
     body = await buildSignedActivity(actorSignature, payload.body, payload.contextType)
   }
 
   return body
 }
 
-async function buildSignedRequestOptions (payload: Payload) {
+async function buildSignedRequestOptions (payload: Payload<any>) {
   let actor: MActor | null
 
   if (payload.signatureActorId) {
@@ -43,9 +46,9 @@ async function buildSignedRequestOptions (payload: Payload) {
 
 function buildGlobalHeaders (body: any) {
   return {
-    'Digest': buildDigest(body),
-    'Content-Type': 'application/activity+json',
-    'Accept': ACTIVITY_PUB.ACCEPT_HEADER
+    'digest': buildDigest(body),
+    'content-type': 'application/activity+json',
+    'accept': ACTIVITY_PUB.ACCEPT_HEADER
   }
 }
 
index 740c274d72116cd1b4edf0c310bffffbb03c8be6..da7f7cc0506dc094814fd4e378a20f238415717a 100644 (file)
@@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config'
 import { AccountBlocklistModel } from '../models/account/account-blocklist'
 import { UserModel } from '../models/account/user'
 import { UserNotificationModel } from '../models/account/user-notification'
-import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models'
+import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
 import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
 import { isBlockedByServerOrAccount } from './blocklist'
 import { Emailer } from './emailer'
@@ -144,6 +144,20 @@ class Notifier {
       })
   }
 
+  notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
+    this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion)
+      .catch(err => {
+        logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })
+      })
+  }
+
+  notifyOfNewPluginVersion (plugin: MPlugin) {
+    this.notifyAdminsOfNewPluginVersion(plugin)
+      .catch(err => {
+        logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })
+      })
+  }
+
   private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
     // List all followers that are users
     const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -667,6 +681,64 @@ class Notifier {
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
   }
 
+  private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
+    // Use the debug right to know who is an administrator
+    const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
+    if (admins.length === 0) return
+
+    logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion)
+
+    function settingGetter (user: MUserWithNotificationSetting) {
+      return user.NotificationSetting.newPeerTubeVersion
+    }
+
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
+        type: UserNotificationType.NEW_PEERTUBE_VERSION,
+        userId: user.id,
+        applicationId: application.id
+      })
+      notification.Application = application
+
+      return notification
+    }
+
+    function emailSender (emails: string[]) {
+      return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion)
+    }
+
+    return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
+  }
+
+  private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) {
+    // Use the debug right to know who is an administrator
+    const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
+    if (admins.length === 0) return
+
+    logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion)
+
+    function settingGetter (user: MUserWithNotificationSetting) {
+      return user.NotificationSetting.newPluginVersion
+    }
+
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
+        type: UserNotificationType.NEW_PLUGIN_VERSION,
+        userId: user.id,
+        pluginId: plugin.id
+      })
+      notification.Plugin = plugin
+
+      return notification
+    }
+
+    function emailSender (emails: string[]) {
+      return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin)
+    }
+
+    return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
+  }
+
   private async notify<T extends MUserWithNotificationSetting> (options: {
     users: T[]
     notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
index 7bcb6ed4c9a291c278b4555ed85be66d6971e16a..624f5da1df325fe7a317371b7463d293e3d763cd 100644 (file)
@@ -1,22 +1,22 @@
-import { doRequest } from '../../helpers/requests'
-import { CONFIG } from '../../initializers/config'
+import { sanitizeUrl } from '@server/helpers/core-utils'
+import { ResultList } from '../../../shared/models'
+import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
+import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
 import {
   PeertubePluginLatestVersionRequest,
   PeertubePluginLatestVersionResponse
 } from '../../../shared/models/plugins/peertube-plugin-latest-version.model'
-import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
-import { ResultList } from '../../../shared/models'
-import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
-import { PluginModel } from '../../models/server/plugin'
-import { PluginManager } from './plugin-manager'
 import { logger } from '../../helpers/logger'
+import { doJSONRequest } from '../../helpers/requests'
+import { CONFIG } from '../../initializers/config'
 import { PEERTUBE_VERSION } from '../../initializers/constants'
-import { sanitizeUrl } from '@server/helpers/core-utils'
+import { PluginModel } from '../../models/server/plugin'
+import { PluginManager } from './plugin-manager'
 
 async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) {
   const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options
 
-  const qs: PeertubePluginIndexList = {
+  const searchParams: PeertubePluginIndexList & Record<string, string | number> = {
     start,
     count,
     sort,
@@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
   const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins'
 
   try {
-    const { body } = await doRequest<any>({ uri, qs, json: true })
+    const { body } = await doJSONRequest<any>(uri, { searchParams })
 
     logger.debug('Got result from PeerTube index.', { body })
 
@@ -58,7 +58,11 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu
 
   const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version'
 
-  const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' })
+  const options = {
+    json: bodyRequest,
+    method: 'POST' as 'POST'
+  }
+  const { body } = await doJSONRequest<PeertubePluginLatestVersionResponse>(uri, options)
 
   return body
 }
index 1f2a88c274d54119e521bc80c8a22d64e4438d75..9b5e1a5462af714e322a1d9cb0968992fda08227 100644 (file)
@@ -7,7 +7,7 @@ import {
   VIDEO_PLAYLIST_PRIVACIES,
   VIDEO_PRIVACIES
 } from '@server/initializers/constants'
-import { onExternalUserAuthenticated } from '@server/lib/auth'
+import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
 import { PluginModel } from '@server/models/server/plugin'
 import {
   RegisterServerAuthExternalOptions,
index f62f52f9cdc237adf6dcea7b34bd3644ae4be30f..0b8cd13898118f815158a14b645508ecf0e6cf2a 100644 (file)
@@ -1,5 +1,5 @@
 import { chunk } from 'lodash'
-import { doRequest } from '@server/helpers/requests'
+import { doJSONRequest } from '@server/helpers/requests'
 import { JobQueue } from '@server/lib/job-queue'
 import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
 import { getServerActor } from '@server/models/application/application'
@@ -34,12 +34,12 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
     try {
       const serverActor = await getServerActor()
 
-      const qs = { count: 1000 }
-      if (this.lastCheck) Object.assign(qs, { since: this.lastCheck.toISOString() })
+      const searchParams = { count: 1000 }
+      if (this.lastCheck) Object.assign(searchParams, { since: this.lastCheck.toISOString() })
 
       this.lastCheck = new Date()
 
-      const { body } = await doRequest<any>({ uri: indexUrl, qs, json: true })
+      const { body } = await doJSONRequest<any>(indexUrl, { searchParams })
       if (!body.data || Array.isArray(body.data) === false) {
         logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body })
         return
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts
new file mode 100644 (file)
index 0000000..c896046
--- /dev/null
@@ -0,0 +1,55 @@
+
+import { doJSONRequest } from '@server/helpers/requests'
+import { ApplicationModel } from '@server/models/application/application'
+import { compareSemVer } from '@shared/core-utils'
+import { JoinPeerTubeVersions } from '@shared/models'
+import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
+import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
+import { Notifier } from '../notifier'
+import { AbstractScheduler } from './abstract-scheduler'
+
+export class PeerTubeVersionCheckScheduler extends AbstractScheduler {
+
+  private static instance: AbstractScheduler
+
+  protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion
+
+  private constructor () {
+    super()
+  }
+
+  protected async internalExecute () {
+    return this.checkLatestVersion()
+  }
+
+  private async checkLatestVersion () {
+    if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return
+
+    logger.info('Checking latest PeerTube version.')
+
+    const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL)
+
+    if (!body?.peertube?.latestVersion) {
+      logger.warn('Cannot check latest PeerTube version: body is invalid.', { body })
+      return
+    }
+
+    const latestVersion = body.peertube.latestVersion
+    const application = await ApplicationModel.load()
+
+    // Already checked this version
+    if (application.latestPeerTubeVersion === latestVersion) return
+
+    if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) {
+      application.latestPeerTubeVersion = latestVersion
+      await application.save()
+
+      Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion)
+    }
+  }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+}
index 014993e94cee04568588ca21445911f2db4c966b..9a1ae3ec50861dc9e26695dd6ca6680e7f9dc0b8 100644 (file)
@@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin'
 import { chunk } from 'lodash'
 import { getLatestPluginsVersion } from '../plugins/plugin-index'
 import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
+import { Notifier } from '../notifier'
 
 export class PluginsCheckScheduler extends AbstractScheduler {
 
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler {
             plugin.latestVersion = result.latestVersion
             await plugin.save()
 
+            // Notify if there is an higher plugin version available
+            if (compareSemVer(plugin.version, result.latestVersion) < 0) {
+              Notifier.Instance.notifyOfNewPluginVersion(plugin)
+            }
+
             logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion)
           }
         }
index e1892f22c89646b08a68f32ddb6b52985d7f1894..9b0a0a2f111cd46b64a834ac6bf8ba61c28f2e1b 100644 (file)
@@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
     newInstanceFollower: UserNotificationSettingValue.WEB,
     abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
     abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    autoInstanceFollowing: UserNotificationSettingValue.WEB
+    autoInstanceFollowing: UserNotificationSettingValue.WEB,
+    newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newPluginVersion: UserNotificationSettingValue.WEB
   }
 
   return UserNotificationSettingModel.create(values, { transaction: t })
index dbb37e0b28b70915c87615c4118066ba04770fbc..37c43c3b0ce91057b31ae8bb74ce8425ae351473 100644 (file)
@@ -11,7 +11,7 @@ import {
 } from '@server/types/models'
 import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models'
 import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
-import { logger } from '../helpers/logger'
+import { logger, loggerTagsFactory } from '../helpers/logger'
 import { CONFIG } from '../initializers/config'
 import { VideoBlacklistModel } from '../models/video/video-blacklist'
 import { sendDeleteVideo } from './activitypub/send'
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager'
 import { Notifier } from './notifier'
 import { Hooks } from './plugins/hooks'
 
+const lTags = loggerTagsFactory('blacklist')
+
 async function autoBlacklistVideoIfNeeded (parameters: {
   video: MVideoWithBlacklistLight
   user?: MUser
@@ -60,7 +62,7 @@ async function autoBlacklistVideoIfNeeded (parameters: {
     })
   }
 
-  logger.info('Video %s auto-blacklisted.', video.uuid)
+  logger.info('Video %s auto-blacklisted.', video.uuid, lTags(video.uuid))
 
   return true
 }
similarity index 86%
rename from server/middlewares/oauth.ts
rename to server/middlewares/auth.ts
index 280595acc3a08b27c8eab9fd3c0a2bd161e63e3a..f38373624c1cccf006c3be35de067cdf7c552516 100644 (file)
@@ -1,15 +1,19 @@
 import * as express from 'express'
 import { Socket } from 'socket.io'
-import { oAuthServer } from '@server/lib/auth'
-import { logger } from '../helpers/logger'
-import { getAccessToken } from '../lib/oauth-model'
+import { getAccessToken } from '@server/lib/auth/oauth-model'
 import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { logger } from '../helpers/logger'
+import { handleOAuthAuthenticate } from '../lib/auth/oauth'
 
 function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) {
-  const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {}
+  handleOAuthAuthenticate(req, res, authenticateInQuery)
+    .then((token: any) => {
+      res.locals.oauth = { token }
+      res.locals.authenticated = true
 
-  oAuthServer.authenticate(options)(req, res, err => {
-    if (err) {
+      return next()
+    })
+    .catch(err => {
       logger.warn('Cannot authenticate.', { err })
 
       return res.status(err.status)
@@ -17,13 +21,7 @@ function authenticate (req: express.Request, res: express.Response, next: expres
           error: 'Token is invalid.',
           code: err.name
         })
-        .end()
-    }
-
-    res.locals.authenticated = true
-
-    return next()
-  })
+    })
 }
 
 function authenticateSocket (socket: Socket, next: (err?: any) => void) {
index b758a8586b19fb89a3adf9697260ee76eb2c7048..3e280e16f590a182396934a8b070cea72ee28ee4 100644 (file)
@@ -1,7 +1,7 @@
 export * from './validators'
 export * from './activitypub'
 export * from './async'
-export * from './oauth'
+export * from './auth'
 export * from './pagination'
 export * from './servers'
 export * from './sort'
index 02b191480b022ba401663e9211b5c5a4f119da37..7c4e4946329d12e330bd070693bac2f4862e6e66 100644 (file)
@@ -23,7 +23,7 @@ const signatureValidator = [
     .custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } })
+    logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } })
 
     if (areValidationErrors(req, res)) return
 
index 99ef25e0a1a448b8af74cf6c7b0a1c88d44d166c..d87b28c06860e987df04e9f1dabe64f79478af3a 100644 (file)
@@ -1,9 +1,11 @@
 import * as express from 'express'
 import { param, query } from 'express-validator'
 import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs'
-import { logger } from '../../helpers/logger'
+import { logger, loggerTagsFactory } from '../../helpers/logger'
 import { areValidationErrors } from './utils'
 
+const lTags = loggerTagsFactory('validators', 'jobs')
+
 const listJobsValidator = [
   param('state')
   .optional()
@@ -14,7 +16,7 @@ const listJobsValidator = [
     .custom(isValidJobType).withMessage('Should have a valid job state'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking listJobsValidator parameters.', { parameters: req.params })
+    logger.debug('Checking listJobsValidator parameters.', { parameters: req.params, ...lTags() })
 
     if (areValidationErrors(req, res)) return
 
index 1cae7848c400fe14f40392f5152c7e96a2097933..6b0a83d802ac2cd6e8c211bff7b02a4dd4759767 100644 (file)
@@ -4,25 +4,30 @@ import { logger } from '../../helpers/logger'
 import { areValidationErrors } from './utils'
 import { PAGINATION } from '@server/initializers/constants'
 
-const paginationValidator = [
-  query('start')
-    .optional()
-    .isInt({ min: 0 }).withMessage('Should have a number start'),
-  query('count')
-    .optional()
-    .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
+const paginationValidator = paginationValidatorBuilder()
 
-  (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking pagination parameters', { parameters: req.query })
+function paginationValidatorBuilder (tags: string[] = []) {
+  return [
+    query('start')
+      .optional()
+      .isInt({ min: 0 }).withMessage('Should have a number start'),
+    query('count')
+      .optional()
+      .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
 
-    if (areValidationErrors(req, res)) return
+    (req: express.Request, res: express.Response, next: express.NextFunction) => {
+      logger.debug('Checking pagination parameters', { parameters: req.query, tags })
 
-    return next()
-  }
-]
+      if (areValidationErrors(req, res)) return
+
+      return next()
+    }
+  ]
+}
 
 // ---------------------------------------------------------------------------
 
 export {
-  paginationValidator
+  paginationValidator,
+  paginationValidatorBuilder
 }
index e93ceb20036304efbcfe6cee8825f200dad6c1db..beecc155bcbe5b92bc6acd8f310da467639f073f 100644 (file)
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM
 
 const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
 const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
-const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
+const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS, [ 'jobs' ])
 const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
 const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
 const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
index 2899bed6f64de9f507c4404047d3b766181899f8..4167f6d437f7ca9273abe998d2165ac74966e168 100644 (file)
@@ -17,12 +17,12 @@ function areValidationErrors (req: express.Request, res: express.Response) {
   return false
 }
 
-function checkSort (sortableColumns: string[]) {
+function checkSort (sortableColumns: string[], tags: string[] = []) {
   return [
     query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'),
 
     (req: express.Request, res: express.Response, next: express.NextFunction) => {
-      logger.debug('Checking sort parameters', { parameters: req.query })
+      logger.debug('Checking sort parameters', { parameters: req.query, tags })
 
       if (areValidationErrors(req, res)) return
 
index 226c9d43683a2857ea833d592b233a99d09d4906..1afacfed878e508391749815cfc2237c0e43bf89 100644 (file)
@@ -216,7 +216,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
   if (!acceptedResult || acceptedResult.accepted !== true) {
     logger.info('Refused local comment.', { acceptedResult, acceptParameters })
     res.status(HttpStatusCode.FORBIDDEN_403)
-       .json({ error: acceptedResult.errorMessage || 'Refused local comment' })
+       .json({ error: acceptedResult?.errorMessage || 'Refused local comment' })
 
     return false
   }
index 0fba4f5fdfc0d66a1d83e21696b3316133b79073..c872d045e0a150655d3a4b8e65e7bf287e066334 100644 (file)
@@ -29,7 +29,7 @@ import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoP
 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
 import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
 import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
-import { authenticatePromiseIfNeeded } from '../../oauth'
+import { authenticatePromiseIfNeeded } from '../../auth'
 import { areValidationErrors } from '../utils'
 
 const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
index 37cc07b94477c17be57608473330da88b725a469..4d31d3dcb076e6cd4a1644fe2b9719ca519e1b38 100644 (file)
@@ -54,7 +54,7 @@ import { isLocalVideoAccepted } from '../../../lib/moderation'
 import { Hooks } from '../../../lib/plugins/hooks'
 import { AccountModel } from '../../../models/account/account'
 import { VideoModel } from '../../../models/video/video'
-import { authenticatePromiseIfNeeded } from '../../oauth'
+import { authenticatePromiseIfNeeded } from '../../auth'
 import { areValidationErrors } from '../utils'
 
 const videosAddValidator = getCommonVideoEditAttributes().concat([
index ebab8b6d2c58c2fcae57c340934796c8d7967309..138051528d6f493da896a79773d37294f56c2093 100644 (file)
@@ -12,10 +12,10 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { TokensCache } from '@server/lib/auth/tokens-cache'
 import { MNotificationSettingFormattable } from '@server/types/models'
 import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
 import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
-import { clearCacheByUserId } from '../../lib/oauth-model'
 import { throwIfNotValid } from '../utils'
 import { UserModel } from './user'
 
@@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model {
   @Column
   abuseNewMessage: UserNotificationSettingValue
 
+  @AllowNull(false)
+  @Default(null)
+  @Is(
+    'UserNotificationSettingNewPeerTubeVersion',
+    value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
+  )
+  @Column
+  newPeerTubeVersion: UserNotificationSettingValue
+
+  @AllowNull(false)
+  @Default(null)
+  @Is(
+    'UserNotificationSettingNewPeerPluginVersion',
+    value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
+  )
+  @Column
+  newPluginVersion: UserNotificationSettingValue
+
   @ForeignKey(() => UserModel)
   @Column
   userId: number
@@ -177,7 +195,7 @@ export class UserNotificationSettingModel extends Model {
   @AfterUpdate
   @AfterDestroy
   static removeTokenCache (instance: UserNotificationSettingModel) {
-    return clearCacheByUserId(instance.userId)
+    return TokensCache.Instance.clearCacheByUserId(instance.userId)
   }
 
   toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model {
       newInstanceFollower: this.newInstanceFollower,
       autoInstanceFollowing: this.autoInstanceFollowing,
       abuseNewMessage: this.abuseNewMessage,
-      abuseStateChange: this.abuseStateChange
+      abuseStateChange: this.abuseStateChange,
+      newPeerTubeVersion: this.newPeerTubeVersion,
+      newPluginVersion: this.newPluginVersion
     }
   }
 }
index add129644d577292b98dc72ce515e093f04a7073..25c5232032132ea977217697b4553456b0c49d3a 100644 (file)
@@ -9,7 +9,9 @@ import { VideoAbuseModel } from '../abuse/video-abuse'
 import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
 import { ActorModel } from '../activitypub/actor'
 import { ActorFollowModel } from '../activitypub/actor-follow'
+import { ApplicationModel } from '../application/application'
 import { AvatarModel } from '../avatar/avatar'
+import { PluginModel } from '../server/plugin'
 import { ServerModel } from '../server/server'
 import { getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from '../video/video'
@@ -96,7 +98,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
             attributes: [ 'id' ],
             model: VideoAbuseModel.unscoped(),
             required: false,
-            include: [ buildVideoInclude(true) ]
+            include: [ buildVideoInclude(false) ]
           },
           {
             attributes: [ 'id' ],
@@ -106,12 +108,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
               {
                 attributes: [ 'id', 'originCommentId' ],
                 model: VideoCommentModel.unscoped(),
-                required: true,
+                required: false,
                 include: [
                   {
                     attributes: [ 'id', 'name', 'uuid' ],
                     model: VideoModel.unscoped(),
-                    required: true
+                    required: false
                   }
                 ]
               }
@@ -120,7 +122,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
           {
             model: AccountModel,
             as: 'FlaggedAccount',
-            required: true,
+            required: false,
             include: [ buildActorWithAvatarInclude() ]
           }
         ]
@@ -140,6 +142,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
         include: [ buildVideoInclude(false) ]
       },
 
+      {
+        attributes: [ 'id', 'name', 'type', 'latestVersion' ],
+        model: PluginModel.unscoped(),
+        required: false
+      },
+
+      {
+        attributes: [ 'id', 'latestPeerTubeVersion' ],
+        model: ApplicationModel.unscoped(),
+        required: false
+      },
+
       {
         attributes: [ 'id', 'state' ],
         model: ActorFollowModel.unscoped(),
@@ -251,6 +265,22 @@ function buildAccountInclude (required: boolean, withActor = false) {
           [Op.ne]: null
         }
       }
+    },
+    {
+      fields: [ 'pluginId' ],
+      where: {
+        pluginId: {
+          [Op.ne]: null
+        }
+      }
+    },
+    {
+      fields: [ 'applicationId' ],
+      where: {
+        applicationId: {
+          [Op.ne]: null
+        }
+      }
     }
   ] as (ModelIndexesOptions & { where?: WhereOptions })[]
 })
@@ -370,6 +400,30 @@ export class UserNotificationModel extends Model {
   })
   ActorFollow: ActorFollowModel
 
+  @ForeignKey(() => PluginModel)
+  @Column
+  pluginId: number
+
+  @BelongsTo(() => PluginModel, {
+    foreignKey: {
+      allowNull: true
+    },
+    onDelete: 'cascade'
+  })
+  Plugin: PluginModel
+
+  @ForeignKey(() => ApplicationModel)
+  @Column
+  applicationId: number
+
+  @BelongsTo(() => ApplicationModel, {
+    foreignKey: {
+      allowNull: true
+    },
+    onDelete: 'cascade'
+  })
+  Application: ApplicationModel
+
   static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
     const where = { userId }
 
@@ -524,6 +578,18 @@ export class UserNotificationModel extends Model {
       }
       : undefined
 
+    const plugin = this.Plugin
+      ? {
+        name: this.Plugin.name,
+        type: this.Plugin.type,
+        latestVersion: this.Plugin.latestVersion
+      }
+      : undefined
+
+    const peertube = this.Application
+      ? { latestVersion: this.Application.latestPeerTubeVersion }
+      : undefined
+
     return {
       id: this.id,
       type: this.type,
@@ -535,6 +601,8 @@ export class UserNotificationModel extends Model {
       videoBlacklist,
       account,
       actorFollow,
+      plugin,
+      peertube,
       createdAt: this.createdAt.toISOString(),
       updatedAt: this.updatedAt.toISOString()
     }
@@ -553,17 +621,19 @@ export class UserNotificationModel extends Model {
       ? {
         threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
 
-        video: {
-          id: abuse.VideoCommentAbuse.VideoComment.Video.id,
-          name: abuse.VideoCommentAbuse.VideoComment.Video.name,
-          uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
-        }
+        video: abuse.VideoCommentAbuse.VideoComment.Video
+          ? {
+            id: abuse.VideoCommentAbuse.VideoComment.Video.id,
+            name: abuse.VideoCommentAbuse.VideoComment.Video.name,
+            uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
+          }
+          : undefined
       }
       : undefined
 
     const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
 
-    const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
+    const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
 
     return {
       id: abuse.id,
index c1f22b76aaad87c4195416c0096d6e1ffacd80fe..a7a65c48972f6023e2e369f39f355f97bc4e5440 100644 (file)
@@ -21,6 +21,7 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { TokensCache } from '@server/lib/auth/tokens-cache'
 import {
   MMyUserFormattable,
   MUser,
@@ -58,7 +59,6 @@ import {
 } from '../../helpers/custom-validators/users'
 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
 import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
-import { clearCacheByUserId } from '../../lib/oauth-model'
 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
 import { ActorModel } from '../activitypub/actor'
 import { ActorFollowModel } from '../activitypub/actor-follow'
@@ -411,7 +411,7 @@ export class UserModel extends Model {
   @AfterUpdate
   @AfterDestroy
   static removeTokenCache (instance: UserModel) {
-    return clearCacheByUserId(instance.id)
+    return TokensCache.Instance.clearCacheByUserId(instance.id)
   }
 
   static countTotal () {
index 909569de1117e157f5f97ad10cc81635c0f71596..21f8b1cbc70acc65c96e24c829ee65fdca1bffad 100644 (file)
@@ -32,6 +32,10 @@ export class ApplicationModel extends Model {
   @Column
   migrationVersion: number
 
+  @AllowNull(true)
+  @Column
+  latestPeerTubeVersion: string
+
   @HasOne(() => AccountModel, {
     foreignKey: {
       allowNull: true
index 6bc6cf27c6fcf6bd5ef0f44c7565456eaeb13f2c..27e643aa71cf0aba93e18fb8a15d4903d6c36c2a 100644 (file)
@@ -12,9 +12,10 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
+import { TokensCache } from '@server/lib/auth/tokens-cache'
+import { MUserAccountId } from '@server/types/models'
 import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
 import { logger } from '../../helpers/logger'
-import { clearCacheByToken } from '../../lib/oauth-model'
 import { AccountModel } from '../account/account'
 import { UserModel } from '../account/user'
 import { ActorModel } from '../activitypub/actor'
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = {
   client: {
     id: number
   }
-  user: {
-    id: number
-  }
+  user: MUserAccountId
   token: MOAuthTokenUser
 }
 
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model {
   @AfterUpdate
   @AfterDestroy
   static removeTokenCache (token: OAuthTokenModel) {
-    return clearCacheByToken(token.accessToken)
+    return TokensCache.Instance.clearCacheByToken(token.accessToken)
   }
 
   static loadByRefreshToken (refreshToken: string) {
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model {
   }
 
   static deleteUserToken (userId: number, t?: Transaction) {
+    TokensCache.Instance.deleteUserToken(userId)
+
     const query = {
       where: {
         userId
index 8bde54a406f17e9db19ac5ebe3ed373bc376bf89..364b53e0f7602f4423dc0cbf24b3797ace241832 100644 (file)
@@ -8,6 +8,8 @@ import {
   cleanupTests,
   closeAllSequelize,
   flushAndRunMultipleServers,
+  killallServers,
+  reRunServer,
   ServerInfo,
   setActorField,
   wait
@@ -20,21 +22,32 @@ import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activi
 const expect = chai.expect
 
 function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) {
+  const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
+
   return Promise.all([
-    setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'publicKey', publicKey),
-    setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'privateKey', privateKey)
+    setActorField(onServer.internalServerNumber, url, 'publicKey', publicKey),
+    setActorField(onServer.internalServerNumber, url, 'privateKey', privateKey)
   ])
 }
 
-function getAnnounceWithoutContext (server2: ServerInfo) {
+function setUpdatedAtOfServer (onServer: ServerInfo, ofServer: ServerInfo, updatedAt: string) {
+  const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
+
+  return Promise.all([
+    setActorField(onServer.internalServerNumber, url, 'createdAt', updatedAt),
+    setActorField(onServer.internalServerNumber, url, 'updatedAt', updatedAt)
+  ])
+}
+
+function getAnnounceWithoutContext (server: ServerInfo) {
   const json = require('./json/peertube/announce-without-context.json')
   const result: typeof json = {}
 
   for (const key of Object.keys(json)) {
     if (Array.isArray(json[key])) {
-      result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`))
+      result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
     } else {
-      result[key] = json[key].replace(':9002', `:${server2.port}`)
+      result[key] = json[key].replace(':9002', `:${server.port}`)
     }
   }
 
@@ -64,7 +77,8 @@ describe('Test ActivityPub security', function () {
 
     url = servers[0].url + '/inbox'
 
-    await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
+    await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
+    await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
 
     const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
     const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
@@ -79,9 +93,12 @@ describe('Test ActivityPub security', function () {
         Digest: buildDigest({ hello: 'coucou' })
       }
 
-      const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      try {
+        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
+        expect(true, 'Did not throw').to.be.false
+      } catch (err) {
+        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      }
     })
 
     it('Should fail with an invalid date', async function () {
@@ -89,9 +106,12 @@ describe('Test ActivityPub security', function () {
       const headers = buildGlobalHeaders(body)
       headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
 
-      const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      try {
+        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
+        expect(true, 'Did not throw').to.be.false
+      } catch (err) {
+        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      }
     })
 
     it('Should fail with bad keys', async function () {
@@ -101,9 +121,12 @@ describe('Test ActivityPub security', function () {
       const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
       const headers = buildGlobalHeaders(body)
 
-      const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      try {
+        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
+        expect(true, 'Did not throw').to.be.false
+      } catch (err) {
+        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      }
     })
 
     it('Should reject requests without appropriate signed headers', async function () {
@@ -123,8 +146,12 @@ describe('Test ActivityPub security', function () {
       for (const badHeaders of badHeadersMatrix) {
         signatureOptions.headers = badHeaders
 
-        const { response } = await makePOSTAPRequest(url, body, signatureOptions, headers)
-        expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+        try {
+          await makePOSTAPRequest(url, body, signatureOptions, headers)
+          expect(true, 'Did not throw').to.be.false
+        } catch (err) {
+          expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+        }
       }
     })
 
@@ -132,27 +159,32 @@ describe('Test ActivityPub security', function () {
       const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
       const headers = buildGlobalHeaders(body)
 
-      const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
+      const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
+      expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
     })
 
     it('Should refresh the actor keys', async function () {
       this.timeout(20000)
 
-      // Wait refresh invalidation
-      await wait(10000)
-
       // Update keys of server 2 to invalid keys
       // Server 1 should refresh the actor and fail
       await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
+      await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
+
+      // Invalid peertube actor cache
+      killallServers([ servers[1] ])
+      await reRunServer(servers[1])
 
       const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
       const headers = buildGlobalHeaders(body)
 
-      const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      try {
+        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
+        expect(true, 'Did not throw').to.be.false
+      } catch (err) {
+        console.error(err)
+        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      }
     })
   })
 
@@ -183,9 +215,12 @@ describe('Test ActivityPub security', function () {
 
       const headers = buildGlobalHeaders(signedBody)
 
-      const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      try {
+        await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
+        expect(true, 'Did not throw').to.be.false
+      } catch (err) {
+        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      }
     })
 
     it('Should fail with an altered body', async function () {
@@ -204,9 +239,12 @@ describe('Test ActivityPub security', function () {
 
       const headers = buildGlobalHeaders(signedBody)
 
-      const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      try {
+        await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
+        expect(true, 'Did not throw').to.be.false
+      } catch (err) {
+        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      }
     })
 
     it('Should succeed with a valid signature', async function () {
@@ -220,9 +258,8 @@ describe('Test ActivityPub security', function () {
 
       const headers = buildGlobalHeaders(signedBody)
 
-      const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
+      const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
+      expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
     })
 
     it('Should refresh the actor keys', async function () {
@@ -243,9 +280,12 @@ describe('Test ActivityPub security', function () {
 
       const headers = buildGlobalHeaders(signedBody)
 
-      const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
-
-      expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      try {
+        await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
+        expect(true, 'Did not throw').to.be.false
+      } catch (err) {
+        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
+      }
     })
   })
 
index 05a78b0adc320d49940fe44788e7455ac42ce5fb..26d4423f9313d3a545e4a94f9b916a5d69212ca5 100644 (file)
@@ -176,7 +176,9 @@ describe('Test user notifications API validators', function () {
       newInstanceFollower: UserNotificationSettingValue.WEB,
       autoInstanceFollowing: UserNotificationSettingValue.WEB,
       abuseNewMessage: UserNotificationSettingValue.WEB,
-      abuseStateChange: UserNotificationSettingValue.WEB
+      abuseStateChange: UserNotificationSettingValue.WEB,
+      newPeerTubeVersion: UserNotificationSettingValue.WEB,
+      newPluginVersion: UserNotificationSettingValue.WEB
     }
 
     it('Should fail with missing fields', async function () {
index 0a13f5b678cefc1168b4b4dae95200c37a43c5f3..2b03fde2d05028adcbe92e91e95f1ee213b55d5e 100644 (file)
@@ -241,7 +241,7 @@ describe('Test users API validators', function () {
     })
 
     it('Should succeed with no password on a server with smtp enabled', async function () {
-      this.timeout(10000)
+      this.timeout(20000)
 
       killallServers([ server ])
 
diff --git a/server/tests/api/notifications/admin-notifications.ts b/server/tests/api/notifications/admin-notifications.ts
new file mode 100644 (file)
index 0000000..e07327d
--- /dev/null
@@ -0,0 +1,165 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import { expect } from 'chai'
+import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
+import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
+import { ServerInfo } from '../../../../shared/extra-utils/index'
+import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
+import {
+  CheckerBaseParams,
+  checkNewPeerTubeVersion,
+  checkNewPluginVersion,
+  prepareNotificationsTest
+} from '../../../../shared/extra-utils/users/user-notifications'
+import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
+import { PluginType } from '@shared/models'
+
+describe('Test admin notifications', function () {
+  let server: ServerInfo
+  let userNotifications: UserNotification[] = []
+  let adminNotifications: UserNotification[] = []
+  let emails: object[] = []
+  let baseParams: CheckerBaseParams
+  let joinPeerTubeServer: MockJoinPeerTubeVersions
+
+  before(async function () {
+    this.timeout(120000)
+
+    const config = {
+      peertube: {
+        check_latest_version: {
+          enabled: true,
+          url: 'http://localhost:42102/versions.json'
+        }
+      },
+      plugins: {
+        index: {
+          enabled: true,
+          check_latest_versions_interval: '5 seconds'
+        }
+      }
+    }
+
+    const res = await prepareNotificationsTest(1, config)
+    emails = res.emails
+    server = res.servers[0]
+
+    userNotifications = res.userNotifications
+    adminNotifications = res.adminNotifications
+
+    baseParams = {
+      server: server,
+      emails,
+      socketNotifications: adminNotifications,
+      token: server.accessToken
+    }
+
+    await installPlugin({
+      url: server.url,
+      accessToken: server.accessToken,
+      npmName: 'peertube-plugin-hello-world'
+    })
+
+    await installPlugin({
+      url: server.url,
+      accessToken: server.accessToken,
+      npmName: 'peertube-theme-background-red'
+    })
+
+    joinPeerTubeServer = new MockJoinPeerTubeVersions()
+    await joinPeerTubeServer.initialize()
+  })
+
+  describe('Latest PeerTube version notification', function () {
+
+    it('Should not send a notification to admins if there is not a new version', async function () {
+      this.timeout(30000)
+
+      joinPeerTubeServer.setLatestVersion('1.4.2')
+
+      await wait(3000)
+      await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence')
+    })
+
+    it('Should send a notification to admins on new plugin version', async function () {
+      this.timeout(30000)
+
+      joinPeerTubeServer.setLatestVersion('15.4.2')
+
+      await wait(3000)
+      await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence')
+    })
+
+    it('Should not send the same notification to admins', async function () {
+      this.timeout(30000)
+
+      await wait(3000)
+      expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
+    })
+
+    it('Should not have sent a notification to users', async function () {
+      this.timeout(30000)
+
+      expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
+    })
+
+    it('Should send a new notification after a new release', async function () {
+      this.timeout(30000)
+
+      joinPeerTubeServer.setLatestVersion('15.4.3')
+
+      await wait(3000)
+      await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence')
+      expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
+    })
+  })
+
+  describe('Latest plugin version notification', function () {
+
+    it('Should not send a notification to admins if there is no new plugin version', async function () {
+      this.timeout(30000)
+
+      await wait(6000)
+      await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence')
+    })
+
+    it('Should send a notification to admins on new plugin version', async function () {
+      this.timeout(30000)
+
+      await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+      await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+      await wait(6000)
+
+      await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence')
+    })
+
+    it('Should not send the same notification to admins', async function () {
+      this.timeout(30000)
+
+      await wait(6000)
+
+      expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
+    })
+
+    it('Should not have sent a notification to users', async function () {
+      expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
+    })
+
+    it('Should send a new notification after a new plugin release', async function () {
+      this.timeout(30000)
+
+      await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+      await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
+      await wait(6000)
+
+      expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
+    })
+  })
+
+  after(async function () {
+    MockSmtpServer.Instance.kill()
+
+    await cleanupTests([ server ])
+  })
+})
index bd07a339e0368b6661fd9078f0a38e903fe4dd9e..8caa30a3d2c9304603c46aa6fc0c937e6f4a12c0 100644 (file)
@@ -1,3 +1,4 @@
+import './admin-notifications'
 import './comments-notifications'
 import './moderation-notifications'
 import './notifications-api'
index 043754e706faa49fa1c86633d43de2a0fd6cbfce..f3ba11950018e0ed726cc668f10d546ad74d76ee 100644 (file)
@@ -348,8 +348,8 @@ describe('Test handle downs', function () {
 
     for (let i = 0; i < 3; i++) {
       await getVideo(servers[1].url, videoIdsServer1[i])
-      await wait(1000)
       await waitJobs([ servers[1] ])
+      await wait(1500)
     }
 
     for (const id of videoIdsServer1) {
index 62a59033fe10ed39a27c3ba6c0f0078c1d56ef8f..cea98aac7b9f9fc40d9434927f80a0f427f06f05 100644 (file)
@@ -4,10 +4,12 @@ import 'mocha'
 import * as chai from 'chai'
 import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
 import { CustomConfig } from '@shared/models/server'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 import {
   addVideoCommentThread,
   blockUser,
   cleanupTests,
+  closeAllSequelize,
   createUser,
   deleteMe,
   flushAndRunServer,
@@ -24,6 +26,7 @@ import {
   getVideoChannel,
   getVideosList,
   installPlugin,
+  killallServers,
   login,
   makePutBodyRequest,
   rateVideo,
@@ -31,7 +34,9 @@ import {
   removeUser,
   removeVideo,
   reportAbuse,
+  reRunServer,
   ServerInfo,
+  setTokenField,
   testImage,
   unblockUser,
   updateAbuse,
@@ -44,10 +49,9 @@ import {
   waitJobs
 } from '../../../../shared/extra-utils'
 import { follow } from '../../../../shared/extra-utils/server/follows'
-import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
+import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
 import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
 import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
 
 const expect = chai.expect
 
@@ -89,6 +93,7 @@ describe('Test users', function () {
       const client = { id: 'client', secret: server.client.secret }
       const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
 
+      expect(res.body.code).to.equal('invalid_client')
       expect(res.body.error).to.contain('client is invalid')
     })
 
@@ -96,6 +101,7 @@ describe('Test users', function () {
       const client = { id: server.client.id, secret: 'coucou' }
       const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
 
+      expect(res.body.code).to.equal('invalid_client')
       expect(res.body.error).to.contain('client is invalid')
     })
   })
@@ -106,6 +112,7 @@ describe('Test users', function () {
       const user = { username: 'captain crochet', password: server.user.password }
       const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
 
+      expect(res.body.code).to.equal('invalid_grant')
       expect(res.body.error).to.contain('credentials are invalid')
     })
 
@@ -113,6 +120,7 @@ describe('Test users', function () {
       const user = { username: server.user.username, password: 'mew_three' }
       const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
 
+      expect(res.body.code).to.equal('invalid_grant')
       expect(res.body.error).to.contain('credentials are invalid')
     })
 
@@ -245,12 +253,44 @@ describe('Test users', function () {
     })
 
     it('Should be able to login again', async function () {
-      server.accessToken = await serverLogin(server)
+      const res = await login(server.url, server.client, server.user)
+      server.accessToken = res.body.access_token
+      server.refreshToken = res.body.refresh_token
+    })
+
+    it('Should be able to get my user information again', async function () {
+      await getMyUserInformation(server.url, server.accessToken)
+    })
+
+    it('Should have an expired access token', async function () {
+      this.timeout(15000)
+
+      await setTokenField(server.internalServerNumber, server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
+      await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
+
+      killallServers([ server ])
+      await reRunServer(server)
+
+      await getMyUserInformation(server.url, server.accessToken, 401)
+    })
+
+    it('Should not be able to refresh an access token with an expired refresh token', async function () {
+      await refreshToken(server, server.refreshToken, 400)
     })
 
-    it('Should have an expired access token')
+    it('Should refresh the token', async function () {
+      this.timeout(15000)
+
+      const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
+      await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', futureDate)
 
-    it('Should refresh the token')
+      killallServers([ server ])
+      await reRunServer(server)
+
+      const res = await refreshToken(server, server.refreshToken)
+      server.accessToken = res.body.access_token
+      server.refreshToken = res.body.refresh_token
+    })
 
     it('Should be able to get my user information again', async function () {
       await getMyUserInformation(server.url, server.accessToken)
@@ -976,6 +1016,7 @@ describe('Test users', function () {
   })
 
   after(async function () {
+    await closeAllSequelize([ server ])
     await cleanupTests([ server ])
   })
 })
index 242589010910ab6315b5524f6e9437d9231d031a..7e6eebd1780f2bbd867397d61b6d79a736b797f8 100644 (file)
@@ -6,5 +6,6 @@ import './peertube'
 import './plugins'
 import './print-transcode-command'
 import './prune-storage'
+import './regenerate-thumbnails'
 import './reset-password'
 import './update-host'
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts
new file mode 100644 (file)
index 0000000..5600551
--- /dev/null
@@ -0,0 +1,110 @@
+import 'mocha'
+import { expect } from 'chai'
+import { writeFile } from 'fs-extra'
+import { basename, join } from 'path'
+import { Video } from '@shared/models'
+import {
+  buildServerDirectory,
+  cleanupTests,
+  doubleFollow,
+  execCLI,
+  flushAndRunMultipleServers,
+  getEnvCli,
+  getVideo,
+  makeRawRequest,
+  ServerInfo,
+  setAccessTokensToServers,
+  uploadVideoAndGetId,
+  waitJobs
+} from '../../../shared/extra-utils'
+import { HttpStatusCode } from '@shared/core-utils'
+
+describe('Test regenerate thumbnails script', function () {
+  let servers: ServerInfo[]
+
+  let video1: Video
+  let video2: Video
+  let remoteVideo: Video
+
+  let thumbnail1Path: string
+  let thumbnailRemotePath: string
+
+  before(async function () {
+    this.timeout(60000)
+
+    servers = await flushAndRunMultipleServers(2)
+    await setAccessTokensToServers(servers)
+
+    await doubleFollow(servers[0], servers[1])
+
+    {
+      const videoUUID1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid
+      video1 = await (getVideo(servers[0].url, videoUUID1).then(res => res.body))
+
+      thumbnail1Path = join(buildServerDirectory(servers[0], 'thumbnails'), basename(video1.thumbnailPath))
+
+      const videoUUID2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid
+      video2 = await (getVideo(servers[0].url, videoUUID2).then(res => res.body))
+    }
+
+    {
+      const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3' })).uuid
+      await waitJobs(servers)
+
+      remoteVideo = await (getVideo(servers[0].url, videoUUID).then(res => res.body))
+
+      thumbnailRemotePath = join(buildServerDirectory(servers[0], 'thumbnails'), basename(remoteVideo.thumbnailPath))
+    }
+
+    await writeFile(thumbnail1Path, '')
+    await writeFile(thumbnailRemotePath, '')
+  })
+
+  it('Should have empty thumbnails', async function () {
+    {
+      const res = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200)
+      expect(res.body).to.have.lengthOf(0)
+    }
+
+    {
+      const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200)
+      expect(res.body).to.not.have.lengthOf(0)
+    }
+
+    {
+      const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200)
+      expect(res.body).to.have.lengthOf(0)
+    }
+  })
+
+  it('Should regenerate thumbnails from the CLI', async function () {
+    this.timeout(15000)
+
+    const env = getEnvCli(servers[0])
+    await execCLI(`${env} npm run regenerate-thumbnails`)
+  })
+
+  it('Should have regenerated thumbbnails', async function () {
+    {
+      const res1 = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200)
+      expect(res1.body).to.not.have.lengthOf(0)
+
+      const res2 = await makeRawRequest(join(servers[0].url, video1.previewPath), HttpStatusCode.OK_200)
+      expect(res2.body).to.not.have.lengthOf(0)
+    }
+
+    {
+      const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200)
+      expect(res.body).to.not.have.lengthOf(0)
+    }
+
+    {
+      const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200)
+      expect(res.body).to.have.lengthOf(0)
+    }
+  })
+
+  after(async function () {
+    await cleanupTests(servers)
+  })
+})
index 305d9200218686ef2e3f652d613f23e67cfdfa95..ee0bc39f361d343c15e8484dafa48a63873b0cb6 100644 (file)
@@ -184,6 +184,76 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
       return result
     }
   })
+
+  registerHook({
+    target: 'filter:api.download.torrent.allowed.result',
+    handler: (result, params) => {
+      if (params && params.downloadName.includes('bad torrent')) {
+        return { allowed: false, errorMessage: 'Liu Bei' }
+      }
+
+      return result
+    }
+  })
+
+  registerHook({
+    target: 'filter:api.download.video.allowed.result',
+    handler: (result, params) => {
+      if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) {
+        return { allowed: false, errorMessage: 'Cao Cao' }
+      }
+
+      if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) {
+        return { allowed: false, errorMessage: 'Sun Jian' }
+      }
+
+      return result
+    }
+  })
+
+  registerHook({
+    target: 'filter:html.embed.video.allowed.result',
+    handler: (result, params) => {
+      return {
+        allowed: false,
+        html: 'Lu Bu'
+      }
+    }
+  })
+
+  registerHook({
+    target: 'filter:html.embed.video-playlist.allowed.result',
+    handler: (result, params) => {
+      return {
+        allowed: false,
+        html: 'Diao Chan'
+      }
+    }
+  })
+
+  {
+    const searchHooks = [
+      'filter:api.search.videos.local.list.params',
+      'filter:api.search.videos.local.list.result',
+      'filter:api.search.videos.index.list.params',
+      'filter:api.search.videos.index.list.result',
+      'filter:api.search.video-channels.local.list.params',
+      'filter:api.search.video-channels.local.list.result',
+      'filter:api.search.video-channels.index.list.params',
+      'filter:api.search.video-channels.index.list.result',
+    ]
+
+    for (const h of searchHooks) {
+      registerHook({
+        target: h,
+        handler: (obj) => {
+          peertubeHelpers.logger.debug('Run hook %s.', h)
+
+          return obj
+        }
+      })
+    }
+  }
 }
 
 async function unregister () {
index f8b2d599b8f1e606af4cabde456ae4ad136ee348..5e77f129ea5b24bd7ed38c9375612a97ca773ac7 100644 (file)
@@ -1,11 +1,11 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
-import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
-import { join } from 'path'
-import { pathExists, remove } from 'fs-extra'
 import { expect } from 'chai'
+import { pathExists, remove } from 'fs-extra'
+import { join } from 'path'
+import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
+import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
 
 describe('Request helpers', function () {
   const destPath1 = join(root(), 'test-output-1.txt')
@@ -13,7 +13,7 @@ describe('Request helpers', function () {
 
   it('Should throw an error when the bytes limit is exceeded for request', async function () {
     try {
-      await doRequest({ uri: get4KFileUrl() }, 3)
+      await doRequest(get4KFileUrl(), { bodyKBLimit: 3 })
     } catch {
       return
     }
@@ -23,7 +23,7 @@ describe('Request helpers', function () {
 
   it('Should throw an error when the bytes limit is exceeded for request and save file', async function () {
     try {
-      await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath1, 3)
+      await doRequestAndSaveToFile(get4KFileUrl(), destPath1, { bodyKBLimit: 3 })
     } catch {
 
       await wait(500)
@@ -35,8 +35,8 @@ describe('Request helpers', function () {
   })
 
   it('Should succeed if the file is below the limit', async function () {
-    await doRequest({ uri: get4KFileUrl() }, 5)
-    await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath2, 5)
+    await doRequest(get4KFileUrl(), { bodyKBLimit: 5 })
+    await doRequestAndSaveToFile(get4KFileUrl(), destPath2, { bodyKBLimit: 5 })
 
     expect(await pathExists(destPath2)).to.be.true
   })
index a1b5e8f5da06c2a42239f76c9d1c45ccce28d2ea..5addb45c7826be08d5c2b2cf34556792ab481c6b 100644 (file)
@@ -137,7 +137,7 @@ describe('Test external auth plugins', function () {
 
     await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400)
 
-    await waitUntilLog(server, 'expired external auth token')
+    await waitUntilLog(server, 'expired external auth token', 2)
   })
 
   it('Should auto login Cyan, create the user and use the token', async function () {
index d88170201d7df69f55fb4c86b0f239f02ecbe052..ac958c5f571d42612a851b16e28870dc840a6fac 100644 (file)
@@ -2,11 +2,15 @@
 
 import 'mocha'
 import * as chai from 'chai'
+import { advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels'
 import { ServerConfig } from '@shared/models'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import {
   addVideoCommentReply,
   addVideoCommentThread,
+  advancedVideosSearch,
   createLive,
+  createVideoPlaylist,
   doubleFollow,
   getAccountVideos,
   getConfig,
@@ -15,24 +19,33 @@ import {
   getVideo,
   getVideoChannelVideos,
   getVideoCommentThreads,
+  getVideoPlaylist,
   getVideosList,
   getVideosListPagination,
   getVideoThreadComments,
   getVideoWithToken,
   installPlugin,
+  makeRawRequest,
   registerUser,
   setAccessTokensToServers,
   setDefaultVideoChannel,
   updateCustomSubConfig,
   updateVideo,
   uploadVideo,
+  uploadVideoAndGetId,
   waitJobs
 } from '../../../shared/extra-utils'
-import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers'
+import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
 import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports'
-import { VideoDetails, VideoImport, VideoImportState, VideoPrivacy } from '../../../shared/models/videos'
+import {
+  VideoDetails,
+  VideoImport,
+  VideoImportState,
+  VideoPlaylist,
+  VideoPlaylistPrivacy,
+  VideoPrivacy
+} from '../../../shared/models/videos'
 import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 
 const expect = chai.expect
 
@@ -355,6 +368,165 @@ describe('Test plugin filter hooks', function () {
     })
   })
 
+  describe('Download hooks', function () {
+    const downloadVideos: VideoDetails[] = []
+
+    before(async function () {
+      this.timeout(60000)
+
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
+        transcoding: {
+          webtorrent: {
+            enabled: true
+          },
+          hls: {
+            enabled: true
+          }
+        }
+      })
+
+      const uuids: string[] = []
+
+      for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
+        const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
+        uuids.push(uuid)
+      }
+
+      await waitJobs(servers)
+
+      for (const uuid of uuids) {
+        const res = await getVideo(servers[0].url, uuid)
+        downloadVideos.push(res.body)
+      }
+    })
+
+    it('Should run filter:api.download.torrent.allowed.result', async function () {
+      const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403)
+      expect(res.body.error).to.equal('Liu Bei')
+
+      await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200)
+      await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200)
+    })
+
+    it('Should run filter:api.download.video.allowed.result', async function () {
+      {
+        const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403)
+        expect(res.body.error).to.equal('Cao Cao')
+
+        await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200)
+        await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
+      }
+
+      {
+        const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403)
+        expect(res.body.error).to.equal('Sun Jian')
+
+        await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
+
+        await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
+        await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
+      }
+    })
+  })
+
+  describe('Embed filters', function () {
+    const embedVideos: VideoDetails[] = []
+    const embedPlaylists: VideoPlaylist[] = []
+
+    before(async function () {
+      this.timeout(60000)
+
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
+        transcoding: {
+          enabled: false
+        }
+      })
+
+      for (const name of [ 'bad embed', 'good embed' ]) {
+        {
+          const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
+          const res = await getVideo(servers[0].url, uuid)
+          embedVideos.push(res.body)
+        }
+
+        {
+          const playlistAttrs = { displayName: name, videoChannelId: servers[0].videoChannel.id, privacy: VideoPlaylistPrivacy.PUBLIC }
+          const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs })
+
+          const resPlaylist = await getVideoPlaylist(servers[0].url, res.body.videoPlaylist.id)
+          embedPlaylists.push(resPlaylist.body)
+        }
+      }
+    })
+
+    it('Should run filter:html.embed.video.allowed.result', async function () {
+      const res = await makeRawRequest(servers[0].url + embedVideos[0].embedPath, 200)
+      expect(res.text).to.equal('Lu Bu')
+    })
+
+    it('Should run filter:html.embed.video-playlist.allowed.result', async function () {
+      const res = await makeRawRequest(servers[0].url + embedPlaylists[0].embedPath, 200)
+      expect(res.text).to.equal('Diao Chan')
+    })
+  })
+
+  describe('Search filters', function () {
+
+    before(async function () {
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
+        search: {
+          searchIndex: {
+            enabled: true,
+            isDefaultSearch: false,
+            disableLocalSearch: false
+          }
+        }
+      })
+    })
+
+    it('Should run filter:api.search.videos.local.list.{params,result}', async function () {
+      await advancedVideosSearch(servers[0].url, {
+        search: 'Sun Quan'
+      })
+
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
+    })
+
+    it('Should run filter:api.search.videos.index.list.{params,result}', async function () {
+      await advancedVideosSearch(servers[0].url, {
+        search: 'Sun Quan',
+        searchTarget: 'search-index'
+      })
+
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.params', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.result', 1)
+    })
+
+    it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () {
+      await advancedVideoChannelSearch(servers[0].url, {
+        search: 'Sun Ce'
+      })
+
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
+    })
+
+    it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () {
+      await advancedVideoChannelSearch(servers[0].url, {
+        search: 'Sun Ce',
+        searchTarget: 'search-index'
+      })
+
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1)
+      await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1)
+    })
+  })
+
   after(async function () {
     await cleanupTests(servers)
   })
index 9be0834ba9935fcf5a4d7cfbed072eca39ad9cf7..9159950318d4f32316bf3c6789c233c9700e5cec 100644 (file)
@@ -202,10 +202,7 @@ async function uploadVideoOnPeerTube (parameters: {
   if (videoInfo.thumbnail) {
     thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg')
 
-    await doRequestAndSaveToFile({
-      method: 'GET',
-      uri: videoInfo.thumbnail
-    }, thumbnailfile)
+    await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile)
   }
 
   const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
diff --git a/server/types/models/application/application.ts b/server/types/models/application/application.ts
new file mode 100644 (file)
index 0000000..9afb9ad
--- /dev/null
@@ -0,0 +1,5 @@
+import { ApplicationModel } from '@server/models/application/application'
+
+// ############################################################################
+
+export type MApplication = Omit<ApplicationModel, 'Account'>
diff --git a/server/types/models/application/index.ts b/server/types/models/application/index.ts
new file mode 100644 (file)
index 0000000..26e4b03
--- /dev/null
@@ -0,0 +1 @@
+export * from './application'
index affa17425d0afe4d182003b18fb8db3fd90da0da..b4fdb1ff339d5a332980a2803e200ed944ce3b0c 100644 (file)
@@ -1,4 +1,5 @@
 export * from './account'
+export * from './application'
 export * from './moderation'
 export * from './oauth'
 export * from './server'
index 58764a74842a6207563d380c094ac5f972dc3813..6988086f13d53c96eb99f2f6d02f5c5fd7270d61 100644 (file)
@@ -1,5 +1,7 @@
 import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
 import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
+import { ApplicationModel } from '@server/models/application/application'
+import { PluginModel } from '@server/models/server/plugin'
 import { PickWith, PickWithOpt } from '@shared/core-utils'
 import { AbuseModel } from '../../../models/abuse/abuse'
 import { AccountModel } from '../../../models/account/account'
@@ -85,13 +87,19 @@ export module UserNotificationIncludes {
     Pick<ActorFollowModel, 'id' | 'state'> &
     PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
     PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
+
+  export type PluginInclude =
+    Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'>
+
+  export type ApplicationInclude =
+    Pick<ApplicationModel, 'latestPeerTubeVersion'>
 }
 
 // ############################################################################
 
 export type MUserNotification =
   Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
-  'VideoImport' | 'Account' | 'ActorFollow'>
+  'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
 
 // ############################################################################
 
@@ -103,4 +111,6 @@ export type UserNotificationModelForApi =
   Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
   Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
   Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
+  Use<'Plugin', UserNotificationIncludes.PluginInclude> &
+  Use<'Application', UserNotificationIncludes.ApplicationInclude> &
   Use<'Account', UserNotificationIncludes.AccountIncludeActor>
index 66acfb3f5ea7c4db9f6de142cba09988a9b4c9c8..b0004dc7b7818a48ed0355e7a94023c43147f26c 100644 (file)
@@ -17,7 +17,6 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
 import { MVideoImportDefault } from '@server/types/models/video/video-import'
 import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
 import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
-import { UserRole } from '@shared/models'
 import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
 import {
   MAccountDefault,
@@ -49,22 +48,6 @@ declare module 'express' {
 }
 
 interface PeerTubeLocals {
-  bypassLogin?: {
-    bypass: boolean
-    pluginName: string
-    authName?: string
-    user: {
-      username: string
-      email: string
-      displayName: string
-      role: UserRole
-    }
-  }
-
-  refreshTokenAuthName?: string
-
-  explicitLogout?: boolean
-
   videoAll?: MVideoFullLight
   onlyImmutableVideo?: MVideoImmutable
   onlyVideo?: MVideoThumbnail
index 5c95a1b3e446e22974229ca256e583588c840dad..898a92d43198a95c8ca9f39b75cc14bddb6d47fa 100644 (file)
@@ -1,7 +1,7 @@
 export * from './bulk/bulk'
 export * from './cli/cli'
 export * from './feeds/feeds'
-export * from './instances-index/mock-instances-index'
+export * from './mock-servers/mock-instances-index'
 export * from './miscs/miscs'
 export * from './miscs/sql'
 export * from './miscs/stubs'
index 740f0c2d6d817c2dbe78c9d4f1bdd8adeab5596b..35e493456691c695b706a04efacfed33dd48ea10 100644 (file)
@@ -106,12 +106,20 @@ async function closeAllSequelize (servers: ServerInfo[]) {
   }
 }
 
-function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
+function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
   const seq = getSequelize(internalServerNumber)
 
   const options = { type: QueryTypes.UPDATE }
 
-  return seq.query(`UPDATE "plugin" SET "version" = '${newVersion}' WHERE "name" = '${pluginName}'`, options)
+  return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
+}
+
+function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
+  return setPluginField(internalServerNumber, pluginName, 'version', newVersion)
+}
+
+function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
+  return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion)
 }
 
 function setActorFollowScores (internalServerNumber: number, newScore: number) {
@@ -122,14 +130,24 @@ function setActorFollowScores (internalServerNumber: number, newScore: number) {
   return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
 }
 
+function setTokenField (internalServerNumber: number, accessToken: string, field: string, value: string) {
+  const seq = getSequelize(internalServerNumber)
+
+  const options = { type: QueryTypes.UPDATE }
+
+  return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
+}
+
 export {
   setVideoField,
   setPlaylistField,
   setActorField,
   countVideoViewsOf,
   setPluginVersion,
+  setPluginLatestVersion,
   selectQuery,
   deleteAll,
+  setTokenField,
   updateQuery,
   setActorFollowScores,
   closeAllSequelize,
diff --git a/shared/extra-utils/mock-servers/joinpeertube-versions.ts b/shared/extra-utils/mock-servers/joinpeertube-versions.ts
new file mode 100644 (file)
index 0000000..d7d5b2c
--- /dev/null
@@ -0,0 +1,31 @@
+import * as express from 'express'
+
+export class MockJoinPeerTubeVersions {
+  private latestVersion: string
+
+  initialize () {
+    return new Promise<void>(res => {
+      const app = express()
+
+      app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
+        if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
+
+        return next()
+      })
+
+      app.get('/versions.json', (req: express.Request, res: express.Response) => {
+        return res.json({
+          peertube: {
+            latestVersion: this.latestVersion
+          }
+        })
+      })
+
+      app.listen(42102, () => res())
+    })
+  }
+
+  setLatestVersion (latestVersion: string) {
+    this.latestVersion = latestVersion
+  }
+}
index 4762a8665efc0f6da1025b79e1ea16b36156dc1c..ecd8ce82389d5e31de01fbd78922992c07cb594c 100644 (file)
@@ -5,20 +5,19 @@ import { activityPubContextify } from '../../../server/helpers/activitypub'
 
 function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
   const options = {
-    method: 'POST',
-    uri: url,
+    method: 'POST' as 'POST',
     json: body,
     httpSignature,
     headers
   }
 
-  return doRequest(options)
+  return doRequest(url, options)
 }
 
 async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
   const follow = {
     type: 'Follow',
-    id: by.url + '/toto',
+    id: by.url + '/' + new Date().getTime(),
     actor: by.url,
     object: to.url
   }
@@ -34,7 +33,7 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
   }
   const headers = buildGlobalHeaders(body)
 
-  return makePOSTAPRequest(to.url, body, httpSignature, headers)
+  return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
 }
 
 export {
index 08d05ef36ed630a6a558c314935463f64b1013e1..779a3cc36da6988a9bc73c77d434bfc9ce6b8779 100644 (file)
@@ -37,6 +37,7 @@ interface ServerInfo {
   customConfigFile?: string
 
   accessToken?: string
+  refreshToken?: string
   videoChannel?: VideoChannel
 
   video?: {
index 467a3d95910b034d05fe47ae7ee7177001e88638..249e82925c0c7a241c17af2c99fa79e697f67dda 100644 (file)
@@ -2,7 +2,8 @@
 
 import { expect } from 'chai'
 import { inspect } from 'util'
-import { AbuseState } from '@shared/models'
+import { AbuseState, PluginType } from '@shared/models'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
 import { MockSmtpServer } from '../miscs/email'
 import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
@@ -11,7 +12,6 @@ import { flushAndRunMultipleServers, ServerInfo } from '../server/servers'
 import { getUserNotificationSocket } from '../socket/socket-io'
 import { setAccessTokensToServers, userLogin } from './login'
 import { createUser, getMyUserInformation } from './users'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
 
 function updateMyNotificationSettings (
   url: string,
@@ -629,7 +629,59 @@ async function checkNewBlacklistOnMyVideo (
   await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence')
 }
 
-function getAllNotificationsSettings () {
+async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion: string, type: CheckerType) {
+  const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
+
+  function notificationChecker (notification: UserNotification, type: CheckerType) {
+    if (type === 'presence') {
+      expect(notification).to.not.be.undefined
+      expect(notification.type).to.equal(notificationType)
+
+      expect(notification.peertube).to.exist
+      expect(notification.peertube.latestVersion).to.equal(latestVersion)
+    } else {
+      expect(notification).to.satisfy((n: UserNotification) => {
+        return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
+      })
+    }
+  }
+
+  function emailNotificationFinder (email: object) {
+    const text = email['text']
+
+    return text.includes(latestVersion)
+  }
+
+  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+}
+
+async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: PluginType, pluginName: string, type: CheckerType) {
+  const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
+
+  function notificationChecker (notification: UserNotification, type: CheckerType) {
+    if (type === 'presence') {
+      expect(notification).to.not.be.undefined
+      expect(notification.type).to.equal(notificationType)
+
+      expect(notification.plugin.name).to.equal(pluginName)
+      expect(notification.plugin.type).to.equal(pluginType)
+    } else {
+      expect(notification).to.satisfy((n: UserNotification) => {
+        return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
+      })
+    }
+  }
+
+  function emailNotificationFinder (email: object) {
+    const text = email['text']
+
+    return text.includes(pluginName)
+  }
+
+  await checkNotification(base, notificationChecker, emailNotificationFinder, type)
+}
+
+function getAllNotificationsSettings (): UserNotificationSetting {
   return {
     newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
     newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
@@ -644,11 +696,13 @@ function getAllNotificationsSettings () {
     newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
     abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
     abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
-  } as UserNotificationSetting
+    autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
+  }
 }
 
-async function prepareNotificationsTest (serversCount = 3) {
+async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
   const userNotifications: UserNotification[] = []
   const adminNotifications: UserNotification[] = []
   const adminNotificationsServer2: UserNotification[] = []
@@ -665,7 +719,7 @@ async function prepareNotificationsTest (serversCount = 3) {
       limit: 20
     }
   }
-  const servers = await flushAndRunMultipleServers(serversCount, overrideConfig)
+  const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
 
   await setAccessTokensToServers(servers)
 
@@ -749,5 +803,7 @@ export {
   checkNewInstanceFollower,
   prepareNotificationsTest,
   checkNewCommentAbuseForModerators,
-  checkNewAccountAbuseForModerators
+  checkNewAccountAbuseForModerators,
+  checkNewPeerTubeVersion,
+  checkNewPluginVersion
 }
index 2214f7ca3c5c669849e3a282e82c9c2e80600f02..f105303f4617765854b7838aa3266d00e0df6f9d 100644 (file)
@@ -7,6 +7,7 @@ export * from './redundancy'
 export * from './users'
 export * from './videos'
 export * from './feeds'
+export * from './joinpeertube'
 export * from './overviews'
 export * from './plugins'
 export * from './search'
diff --git a/shared/models/joinpeertube/index.ts b/shared/models/joinpeertube/index.ts
new file mode 100644 (file)
index 0000000..9681c35
--- /dev/null
@@ -0,0 +1 @@
+export * from './versions.model'
diff --git a/shared/models/joinpeertube/versions.model.ts b/shared/models/joinpeertube/versions.model.ts
new file mode 100644 (file)
index 0000000..60a7691
--- /dev/null
@@ -0,0 +1,5 @@
+export interface JoinPeerTubeVersions {
+  peertube: {
+    latestVersion: string
+  }
+}
index 7b7144676f2097a685372774d62efbed4392be95..f8ca32771507519fcf1ab035997b77ba579c40d5 100644 (file)
@@ -85,8 +85,27 @@ export const clientActionHookObject = {
   // Fired when the registration page is being initialized
   'action:signup.register.init': true,
 
+  // Fired when the video upload page is being initalized
+  'action:video-upload.init': true,
+  // Fired when the video import by URL page is being initalized
+  'action:video-url-import.init': true,
+  // Fired when the video import by torrent/magnet URI page is being initalized
+  'action:video-torrent-import.init': true,
+  // Fired when the "Go Live" page is being initalized
+  'action:go-live.init': true,
+
+  // Fired when the user explicitely logged in/logged out
+  'action:auth-user.logged-in': true,
+  'action:auth-user.logged-out': true,
+  // Fired when the application loaded user information (using tokens from the local storage or after a successful login)
+  'action:auth-user.information-loaded': true,
+
+  // Fired when the modal to download a video/caption is shown
+  'action:modal.video-download.shown': true,
+
   // ####### Embed hooks #######
-  // In embed scope, peertube helpers are not available
+  // /!\ In embed scope, peertube helpers are not available
+  // ###########################
 
   // Fired when the embed loaded the player
   'action:embed.player.loaded': true
index 082b4b59184fa620c3a6a6edbe3bb8c992bcc8a7..88277af5aef234c783db108652d7e5f5ebdf87c9 100644 (file)
@@ -18,6 +18,16 @@ export const serverFilterHookObject = {
   'filter:api.user.me.videos.list.params': true,
   'filter:api.user.me.videos.list.result': true,
 
+  // Filter params/results to search videos/channels in the DB or on the remote index
+  'filter:api.search.videos.local.list.params': true,
+  'filter:api.search.videos.local.list.result': true,
+  'filter:api.search.videos.index.list.params': true,
+  'filter:api.search.videos.index.list.result': true,
+  'filter:api.search.video-channels.local.list.params': true,
+  'filter:api.search.video-channels.local.list.result': true,
+  'filter:api.search.video-channels.index.list.params': true,
+  'filter:api.search.video-channels.index.list.result': true,
+
   // Filter the result of the get function
   // Used to get detailed video information (video watch page for example)
   'filter:api.video.get.result': true,
@@ -50,7 +60,15 @@ export const serverFilterHookObject = {
   'filter:video.auto-blacklist.result': true,
 
   // Filter result used to check if a user can register on the instance
-  'filter:api.user.signup.allowed.result': true
+  'filter:api.user.signup.allowed.result': true,
+
+  // Filter result used to check if video/torrent download is allowed
+  'filter:api.download.video.allowed.result': true,
+  'filter:api.download.torrent.allowed.result': true,
+
+  // Filter result to check if the embed is allowed for a particular request
+  'filter:html.embed.video.allowed.result': true,
+  'filter:html.embed.video-playlist.allowed.result': true
 }
 
 export type ServerFilterHookName = keyof typeof serverFilterHookObject
index 069ef0bab5253e1d4fc0a1e95a98ee1fe385ffea..39512d306147b8e6ac5301ef5ca0d088af52cdb9 100644 (file)
@@ -1,12 +1,49 @@
-export type SendEmailOptions = {
-  to: string[]
+type From = string | { name?: string, address: string }
 
-  template?: string
+interface Base extends Partial<SendEmailDefaultMessageOptions> {
+  to: string[] | string
+}
+
+interface MailTemplate extends Base {
+  template: string
   locals?: { [key: string]: any }
+  text?: undefined
+}
+
+interface MailText extends Base {
+  text: string
 
-  // override defaults
-  subject?: string
-  text?: string
-  from?: string | { name?: string, address: string }
-  replyTo?: string
+  locals?: Partial<SendEmailDefaultLocalsOptions> & {
+    title?: string
+    action?: {
+      url: string
+      text: string
+    }
+  }
 }
+
+interface SendEmailDefaultLocalsOptions {
+  instanceName: string
+  text: string
+  subject: string
+}
+
+interface SendEmailDefaultMessageOptions {
+  to: string[] | string
+  from: From
+  subject: string
+  replyTo: string
+}
+
+export type SendEmailDefaultOptions = {
+  template: 'common'
+
+  message: SendEmailDefaultMessageOptions
+
+  locals: SendEmailDefaultLocalsOptions & {
+    WEBSERVER: any
+    EMAIL: any
+  }
+}
+
+export type SendEmailOptions = MailTemplate | MailText
index 83ef844570354407b81437c6732bf64dcd284c82..e4acfee8d0ab459be2ceea9510bc24bf06825170 100644 (file)
@@ -59,7 +59,7 @@ export type ActivitypubHttpFetcherPayload = {
 export type ActivitypubHttpUnicastPayload = {
   uri: string
   signatureActorId?: number
-  body: any
+  body: object
   contextType?: ContextType
 }
 
index 473148062b36e1e0b9deb537dc4737c803c1c002..977e6b9858e18f34cc79af692657a16dbd2c6563 100644 (file)
@@ -24,4 +24,7 @@ export interface UserNotificationSetting {
 
   abuseStateChange: UserNotificationSettingValue
   abuseNewMessage: UserNotificationSettingValue
+
+  newPeerTubeVersion: UserNotificationSettingValue
+  newPluginVersion: UserNotificationSettingValue
 }
index e2f2234e48f642f2e85de19745128be106e74423..8b33e3fbdab30d66a90719ba315a0ea06ff68bc5 100644 (file)
@@ -1,7 +1,8 @@
 import { FollowState } from '../actors'
 import { AbuseState } from '../moderation'
+import { PluginType } from '../plugins'
 
-export enum UserNotificationType {
+export const enum UserNotificationType {
   NEW_VIDEO_FROM_SUBSCRIPTION = 1,
   NEW_COMMENT_ON_MY_VIDEO = 2,
   NEW_ABUSE_FOR_MODERATORS = 3,
@@ -26,7 +27,10 @@ export enum UserNotificationType {
 
   ABUSE_STATE_CHANGE = 15,
 
-  ABUSE_NEW_MESSAGE = 16
+  ABUSE_NEW_MESSAGE = 16,
+
+  NEW_PLUGIN_VERSION = 17,
+  NEW_PEERTUBE_VERSION = 18
 }
 
 export interface VideoInfo {
@@ -108,6 +112,16 @@ export interface UserNotification {
     }
   }
 
+  plugin?: {
+    name: string
+    type: PluginType
+    latestVersion: string
+  }
+
+  peertube?: {
+    latestVersion: string
+  }
+
   createdAt: string
   updatedAt: string
 }
index 39c2c5608a89ec942f1a1a4741fba5c839308b43..5cd735edaed52b9bf8791333ef062ba73a6bfad1 100644 (file)
@@ -19,4 +19,4 @@ NODE_APP_INSTANCE=6 NODE_ENV=test npm run start
  * Check the release is okay: https://github.com/Chocobozzz/PeerTube/releases
  * Update https://peertube3.cpy.re and check it works correctly
  * Update all other instances and check it works correctly
- * Communicate
+ * After a couple of days, update https://joinpeertube.org/api/v1/versions.json
index bc10e624de17e154632833e9fdd0159be2f36e6f..20cbec5c739bff8a88e97a106831386567631c7a 100644 (file)
@@ -22,6 +22,7 @@
     - [Custom Modal](#custom-modal)
     - [Translate](#translate)
     - [Get public settings](#get-public-settings)
+    - [Get server config](#get-server-config)
     - [Add custom fields to video form](#add-custom-fields-to-video-form)
   - [Publishing](#publishing)
 - [Write a plugin/theme](#write-a-plugintheme)
@@ -470,6 +471,15 @@ peertubeHelpers.getSettings()
   })
 ```
 
+#### Get server config
+
+```js
+peertubeHelpers.getServerConfig()
+  .then(config => {
+    console.log('Fetched server config.', config)
+  })
+```
+
 #### Add custom fields to video form
 
 To add custom fields in the video form (in *Plugin settings* tab):
index 452b3d039dcdb2a02656183094de27ff762ce70c..175c22cd81cce5bad10169cac4351cf19bfb43ef 100644 (file)
@@ -15,6 +15,7 @@
     - [peertube-redundancy.js](#peertube-redundancyjs)
 - [Server tools](#server-tools)
   - [parse-log](#parse-log)
+  - [regenerate-thumbnails.js](#regenerate-thumbnailsjs)
   - [create-transcoding-job.js](#create-transcoding-jobjs)
   - [create-import-video-file-job.js](#create-import-video-file-jobjs)
   - [prune-storage.js](#prune-storagejs)
@@ -244,6 +245,22 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production
 
 `--level` is optional and could be `info`/`warn`/`error`
 
+You can also remove SQL or HTTP logs using `--not-tags`:
+
+```
+$ cd /var/www/peertube/peertube-latest
+$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run parse-log -- --level debug --not-tags http sql
+```
+
+### regenerate-thumbnails.js
+
+Regenerating local video thumbnails could be useful because new PeerTube releases may increase thumbnail sizes:
+
+```
+$ cd /var/www/peertube/peertube-latest
+$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run regenerate-thumbnails
+```
+
 ### create-transcoding-job.js
 
 You can use this script to force transcoding of an existing video. PeerTube needs to be running.
index cf4e7b417a5027f9e9020740631b533908c15f96..bdeb76b5186815b350a61129efa6e255addad27b 100644 (file)
@@ -8,7 +8,7 @@ Environment=NODE_ENV=production
 Environment=NODE_CONFIG_DIR=/var/www/peertube/config
 User=peertube
 Group=peertube
-ExecStart=/usr/bin/npm start
+ExecStart=/usr/bin/node dist/server
 WorkingDirectory=/var/www/peertube/peertube-latest
 StandardOutput=syslog
 StandardError=syslog
index b2d5a594c4d6c5f3ac4b9bcfb5780874e3743886..92ab314d6e4863241a23765fc87f1b71d819d622 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
 
 "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13":
-  version "7.13.8"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481"
-  integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw==
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
+  integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
   dependencies:
     "@babel/helper-validator-identifier" "^7.12.11"
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
 "@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
-  version "7.13.9"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99"
-  integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409"
+  integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ==
 
 "@babel/runtime@^7.7.2":
-  version "7.13.9"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.9.tgz#97dbe2116e2630c489f22e0656decd60aaa1fcee"
-  integrity sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
+  integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
   dependencies:
     regenerator-runtime "^0.13.4"
 
     tlds "^1.218.0"
 
 "@mapbox/node-pre-gyp@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.0.tgz#2b809e701da0f6729b47fe78ad4b9dc187a7d2e5"
-  integrity sha512-mEaiD1CURETR/dBIiJAwz0M0Q0mH3gCW4pPMaIlNt97mdzYUVeqGcTJSamgJpS6Tg4tBHDrOJpjdh5fJTLnyNQ==
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.1.tgz#1b23a8decb5e6356b04770d586067d2bff2703dd"
+  integrity sha512-CUBdThIZMoLEQQxACwhLsPg/puxBca0abTH3ixuvBQkhjJ80Hdp99jmVjxFCOa52/tZqN9d70IbGUf+OuKDHGA==
   dependencies:
     detect-libc "^1.0.3"
     http-proxy-agent "^4.0.1"
-    mkdirp "^1.0.4"
+    make-dir "^3.1.0"
     node-fetch "^2.6.1"
     nopt "^5.0.0"
     npmlog "^4.1.2"
     semver "^7.3.4"
     tar "^6.1.0"
 
-"@nestjs/common@7.6.13":
-  version "7.6.13"
-  resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-7.6.13.tgz#597558afedfddeb5021fe8a154327ee082279ab8"
-  integrity sha512-xijw6so4yA8Ywi8mnrA7Kz97ZC36u20Eyb5/XvmokdLcgTcTyHVdE39r44JYnjHPf8SKZoJ965zdu/fKl4s4GQ==
+"@nestjs/common@7.6.14":
+  version "7.6.14"
+  resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-7.6.14.tgz#abdad360ef107482345b111eeee74fbef00620c9"
+  integrity sha512-XJrGoGttCsIOvG2+EXl09pl9iCmYXnhPjx3ndPPigMRdXQGLVpF38OdzroWTD7aYU5rHo3Co21G9cYl8aqdt2Q==
   dependencies:
     axios "0.21.1"
     iterare "1.2.1"
     tslib "2.1.0"
     uuid "8.3.2"
 
-"@nestjs/core@7.6.13":
-  version "7.6.13"
-  resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.13.tgz#b7518dceb436e6ed2c1fad2cff86ddf69b143e73"
-  integrity sha512-8oY8yZSgri2DngqmvBMtwYw1GIAaXbUXS7Y0mp/iSZ6jP7CQqYCybdcMPneunrt5PG8rtJsq6n+4JNRvxXrVmA==
+"@nestjs/core@7.6.14":
+  version "7.6.14"
+  resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.14.tgz#b3be15506aee33b847abce993a7371439b292dd9"
+  integrity sha512-iAeQIsC79xcLTpga3he48ROX4g561VFsfbksicqotrFy0k9czKxVtHxevsnwo8KzFsYXQqOCO6XYI8MsuAjMcg==
   dependencies:
     "@nuxtjs/opencollective" "0.3.2"
     fast-safe-stringify "2.0.7"
     node-fetch "^2.6.1"
 
 "@openapitools/openapi-generator-cli@^2.1.4":
-  version "2.1.26"
-  resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.1.26.tgz#69108458c0c1a0a3964d9b3e2f0360b195c8ea5f"
-  integrity sha512-wr4LHQCoZCvLhf0/UY/9AZYTVi3nWvvOT+/JFjZYWDA/TIqC4eWxPjzM5tnSzGed6gBTuNHEh8gUonDz6WOZDw==
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.2.2.tgz#12b2171a0404731e35aa89a2e0c146186480f51c"
+  integrity sha512-Hl0/5bvv/ETYFuPpTPXqAtChHE2+lLrH0ATl8MtNDxtdXRLoQGCeT8jdT600VvCqJToRkNvQ1JPHbcg/hehyBw==
   dependencies:
-    "@nestjs/common" "7.6.13"
-    "@nestjs/core" "7.6.13"
+    "@nestjs/common" "7.6.14"
+    "@nestjs/core" "7.6.14"
     "@nuxtjs/opencollective" "0.3.2"
     chalk "4.1.0"
     commander "6.2.1"
     "@types/express" "*"
 
 "@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.14.28", "@types/node@^14.14.31":
-  version "14.14.31"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
-  integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
+  version "14.14.34"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.34.tgz#07935194fc049069a1c56c0c274265abeddf88da"
+  integrity sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA==
 
 "@types/nodemailer@^6.2.0":
   version "6.4.0"
     "@types/node" "*"
 
 "@types/qs@*":
-  version "6.9.5"
-  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
-  integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==
+  version "6.9.6"
+  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1"
+  integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==
 
 "@types/range-parser@*":
   version "1.2.3"
     "@types/node" "*"
 
 "@typescript-eslint/eslint-plugin@^4.8.1":
-  version "4.16.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651"
-  integrity sha512-SK777klBdlkUZpZLC1mPvyOWk9yAFCWmug13eAjVQ4/Q1LATE/NbcQL1xDHkptQkZOLnPmLUA1Y54m8dqYwnoQ==
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz#6f856eca4e6a52ce9cf127dfd349096ad936aa2d"
+  integrity sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw==
   dependencies:
-    "@typescript-eslint/experimental-utils" "4.16.1"
-    "@typescript-eslint/scope-manager" "4.16.1"
+    "@typescript-eslint/experimental-utils" "4.17.0"
+    "@typescript-eslint/scope-manager" "4.17.0"
     debug "^4.1.1"
     functional-red-black-tree "^1.0.1"
     lodash "^4.17.15"
     semver "^7.3.2"
     tsutils "^3.17.1"
 
-"@typescript-eslint/experimental-utils@4.16.1":
-  version "4.16.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.16.1.tgz#da7a396dc7d0e01922acf102b76efff17320b328"
-  integrity sha512-0Hm3LSlMYFK17jO4iY3un1Ve9x1zLNn4EM50Lia+0EV99NdbK+cn0er7HC7IvBA23mBg3P+8dUkMXy4leL33UQ==
+"@typescript-eslint/experimental-utils@4.17.0":
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80"
+  integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA==
   dependencies:
     "@types/json-schema" "^7.0.3"
-    "@typescript-eslint/scope-manager" "4.16.1"
-    "@typescript-eslint/types" "4.16.1"
-    "@typescript-eslint/typescript-estree" "4.16.1"
+    "@typescript-eslint/scope-manager" "4.17.0"
+    "@typescript-eslint/types" "4.17.0"
+    "@typescript-eslint/typescript-estree" "4.17.0"
     eslint-scope "^5.0.0"
     eslint-utils "^2.0.0"
 
 "@typescript-eslint/parser@^4.0.0":
-  version "4.16.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a"
-  integrity sha512-/c0LEZcDL5y8RyI1zLcmZMvJrsR6SM1uetskFkoh3dvqDKVXPsXI+wFB/CbVw7WkEyyTKobC1mUNp/5y6gRvXg==
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.17.0.tgz#141b647ffc72ebebcbf9b0fe6087f65b706d3215"
+  integrity sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw==
   dependencies:
-    "@typescript-eslint/scope-manager" "4.16.1"
-    "@typescript-eslint/types" "4.16.1"
-    "@typescript-eslint/typescript-estree" "4.16.1"
+    "@typescript-eslint/scope-manager" "4.17.0"
+    "@typescript-eslint/types" "4.17.0"
+    "@typescript-eslint/typescript-estree" "4.17.0"
     debug "^4.1.1"
 
-"@typescript-eslint/scope-manager@4.16.1":
-  version "4.16.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz#244e2006bc60cfe46987e9987f4ff49c9e3f00d5"
-  integrity sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw==
+"@typescript-eslint/scope-manager@4.17.0":
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d"
+  integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw==
   dependencies:
-    "@typescript-eslint/types" "4.16.1"
-    "@typescript-eslint/visitor-keys" "4.16.1"
+    "@typescript-eslint/types" "4.17.0"
+    "@typescript-eslint/visitor-keys" "4.17.0"
 
-"@typescript-eslint/types@4.16.1":
-  version "4.16.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15"
-  integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA==
+"@typescript-eslint/types@4.17.0":
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad"
+  integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g==
 
-"@typescript-eslint/typescript-estree@4.16.1":
-  version "4.16.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz#c2fc46b05a48fbf8bbe8b66a63f0a9ba04b356f1"
-  integrity sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg==
+"@typescript-eslint/typescript-estree@4.17.0":
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1"
+  integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ==
   dependencies:
-    "@typescript-eslint/types" "4.16.1"
-    "@typescript-eslint/visitor-keys" "4.16.1"
+    "@typescript-eslint/types" "4.17.0"
+    "@typescript-eslint/visitor-keys" "4.17.0"
     debug "^4.1.1"
     globby "^11.0.1"
     is-glob "^4.0.1"
     semver "^7.3.2"
     tsutils "^3.17.1"
 
-"@typescript-eslint/visitor-keys@4.16.1":
-  version "4.16.1"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz#d7571fb580749fae621520deeb134370bbfc7293"
-  integrity sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w==
+"@typescript-eslint/visitor-keys@4.17.0":
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d"
+  integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ==
   dependencies:
-    "@typescript-eslint/types" "4.16.1"
+    "@typescript-eslint/types" "4.17.0"
     eslint-visitor-keys "^2.0.0"
 
 "@ungap/promise-all-settled@1.1.2":
@@ -1116,9 +1116,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
     uri-js "^4.2.2"
 
 ajv@^7.0.2:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.1.1.tgz#1e6b37a454021fa9941713f38b952fc1c8d32a84"
-  integrity sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ==
+  version "7.2.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.1.tgz#a5ac226171912447683524fa2f1248fcf8bac83d"
+  integrity sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
@@ -1373,11 +1373,12 @@ at-least-node@^1.0.0:
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
 autocannon@^7.0.4:
-  version "7.0.4"
-  resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.0.4.tgz#c812c11af283254bff4bd75cce8383e79550c882"
-  integrity sha512-+A+kSsVrx9F9fFbPAD7YytGQfCKgkaCIut4KrnYBbY2hmboAT065ClxqBsVqstokvFfdBAfSMPh0VSc6ktiimg==
+  version "7.0.5"
+  resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.0.5.tgz#7c195ba09ae3b299d6f7532950d1e07041538b29"
+  integrity sha512-VMOfWf0e9EB5Crr7/snXTb64oC7I3lofpAjBcPWvHGet94DKjHCsbj05iIt2WTenPKub++6PETb/H9qleV9yJg==
   dependencies:
     chalk "^4.1.0"
+    char-spinner "^1.0.1"
     cli-table3 "^0.6.0"
     clone "^2.1.2"
     color-support "^1.1.1"
@@ -1391,11 +1392,10 @@ autocannon@^7.0.4:
     manage-path "^2.0.0"
     minimist "^1.2.0"
     on-net-listen "^1.1.1"
-    ora "^5.1.0"
     pretty-bytes "^5.4.1"
     progress "^2.0.3"
     reinterval "^1.1.0"
-    retimer "^2.0.0"
+    retimer "^3.0.0"
     semver "^7.3.2"
     timestring "^6.0.0"
 
@@ -1468,7 +1468,7 @@ basic-auth-connect@^1.0.0:
   resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122"
   integrity sha1-/bC0OWLKe0BFanwrtI/hc9otISI=
 
-basic-auth@^2.0.0, basic-auth@~2.0.1:
+basic-auth@2.0.1, basic-auth@~2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
   integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
@@ -1626,15 +1626,6 @@ bittorrent-tracker@^9.0.0:
     bufferutil "^4.0.1"
     utf-8-validate "^5.0.2"
 
-bl@^4.0.3:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
-  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
-  dependencies:
-    buffer "^5.5.0"
-    inherits "^2.0.4"
-    readable-stream "^3.4.0"
-
 blob-to-buffer@^1.2.9:
   version "1.2.9"
   resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a"
@@ -1652,16 +1643,16 @@ block-stream2@^2.0.0, block-stream2@^2.1.0:
   dependencies:
     readable-stream "^3.4.0"
 
+bluebird@3.7.2, bluebird@^3.5.0, bluebird@^3.7.2:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
 bluebird@^2.10.0:
   version "2.11.0"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
   integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=
 
-bluebird@^3.0.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.7.2:
-  version "3.7.2"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
-  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
-
 bmp-js@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233"
@@ -1770,7 +1761,7 @@ buffer-writer@2.0.0:
   resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
   integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
 
-buffer@^5.2.0, buffer@^5.5.0:
+buffer@^5.2.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
   integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@@ -1926,9 +1917,9 @@ chai-xml@^0.4.0:
     xml2js "^0.4.23"
 
 chai@^4.1.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.1.tgz#6fc6af447610709818e5c45116207d60b8a49cfd"
-  integrity sha512-JClPZFGRcSl7X8dYzlCJY7v+X1fBA+9Y339Y8EqhBVfp0QC1hTnaf7nMfR+XZ74clkBC64b0iEw2cWKHt3EVqA==
+  version "4.3.3"
+  resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.3.tgz#f2b2ad9736999d07a7ff95cf1e7086c43a76f72d"
+  integrity sha512-MPSLOZwxxnA0DhLE84klnGPojWFK5KuhP7/j5dTsxpr2S3XlkqJP5WbyYl1gCTWvG2Z5N+HD4F472WsbEZL6Pw==
   dependencies:
     assertion-error "^1.1.0"
     check-error "^1.0.2"
@@ -1962,6 +1953,11 @@ chalk@^3.0.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+char-spinner@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081"
+  integrity sha1-5upnvSR+EHESmDt6sEee02KAAIE=
+
 character-parser@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
@@ -2061,9 +2057,9 @@ chrome-net@^3.3.2, chrome-net@^3.3.3, chrome-net@^3.3.4:
     inherits "^2.0.1"
 
 chunk-store-stream@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.2.0.tgz#18f673c495946c4cdcf14124a3ebd5f31eb0ea35"
-  integrity sha512-90iueoPoqT2isnmy1fyqwzgFy5FokuaxQuijOQG1VgC/6DaXRfeYN0da8iWENkzqElWhqLxo8pWc7pH9dmxlcA==
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e"
+  integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw==
   dependencies:
     block-stream2 "^2.0.0"
     readable-stream "^3.6.0"
@@ -2092,11 +2088,6 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-spinners@^2.5.0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047"
-  integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==
-
 cli-table3@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
@@ -2204,9 +2195,9 @@ color-name@^1.0.0, color-name@~1.1.4:
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 color-string@^1.5.2:
-  version "1.5.4"
-  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6"
-  integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==
+  version "1.5.5"
+  resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
+  integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
   dependencies:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
@@ -2224,7 +2215,7 @@ color@3.0.x:
     color-convert "^1.9.1"
     color-string "^1.5.2"
 
-colorette@^1.2.1:
+colorette@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
   integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
@@ -2352,9 +2343,9 @@ concurrently@^6.0.0:
     yargs "^16.2.0"
 
 config@^3.0.0:
-  version "3.3.4"
-  resolved "https://registry.yarnpkg.com/config/-/config-3.3.4.tgz#55811abc2752b38a7b806cbdbc2da79c428312b7"
-  integrity sha512-URO0m6z+rtENGHqtzO7W7C35iF+H9KVe7JJFps+3TIqZEOHl83NqTAgp5h8ah96m4NPQnx08nPBfbtDU+PgjVA==
+  version "3.3.6"
+  resolved "https://registry.yarnpkg.com/config/-/config-3.3.6.tgz#b87799db7399cc34988f55379b5f43465b1b065c"
+  integrity sha512-Hj5916C5HFawjYJat1epbyY2PlAgLpBtDUlr0MxGLgo3p5+7kylyvnRY18PqJHgnNWXcdd0eWDemT7eYWuFgwg==
   dependencies:
     json5 "^2.1.1"
 
@@ -2584,9 +2575,9 @@ dashdash@^1.12.0:
     assert-plus "^1.0.0"
 
 date-fns@^2.0.1, date-fns@^2.16.1:
-  version "2.18.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.18.0.tgz#08e50aee300ad0d2c5e054e3f0d10d8f9cdfe09e"
-  integrity sha512-NYyAg4wRmGVU4miKq5ivRACOODdZRY3q5WLmOJSq8djyzftYphU7dTHLcEtLqEvfqMKQ0jVv91P4BAwIjsXIcw==
+  version "2.19.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
+  integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==
 
 dateformat@^3.0.3:
   version "3.0.3"
@@ -2840,9 +2831,9 @@ domhandler@^4.0.0:
     domelementtype "^2.1.0"
 
 domutils@^2.0.0, domutils@^2.4.3, domutils@^2.4.4:
-  version "2.4.4"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3"
-  integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.0.tgz#42f49cffdabb92ad243278b331fd761c1c2d3039"
+  integrity sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==
   dependencies:
     dom-serializer "^1.0.1"
     domelementtype "^2.0.1"
@@ -3047,27 +3038,10 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.0-next.0:
-  version "1.17.7"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
-  integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
-  dependencies:
-    es-to-primitive "^1.2.1"
-    function-bind "^1.1.1"
-    has "^1.0.3"
-    has-symbols "^1.0.1"
-    is-callable "^1.2.2"
-    is-regex "^1.1.1"
-    object-inspect "^1.8.0"
-    object-keys "^1.1.1"
-    object.assign "^4.1.1"
-    string.prototype.trimend "^1.0.1"
-    string.prototype.trimstart "^1.0.1"
-
-es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2:
-  version "1.18.0-next.3"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.3.tgz#56bc8b5cc36b2cca25a13be07f3c02c2343db6b7"
-  integrity sha512-VMzHx/Bczjg59E6jZOQjHeN3DEoptdhejpARgflAViidlqSpjdq9zA6lKwlhRRs/lOw1gHJv2xkkSFRgvEwbQg==
+es-abstract@^1.17.0-next.0, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4"
+  integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==
   dependencies:
     call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
@@ -3401,15 +3375,6 @@ exif-parser@^0.1.12:
   resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
   integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI=
 
-express-oauth-server@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/express-oauth-server/-/express-oauth-server-2.0.0.tgz#57b08665c1201532f52c4c02f19709238b99a48d"
-  integrity sha1-V7CGZcEgFTL1LEwC8ZcJI4uZpI0=
-  dependencies:
-    bluebird "^3.0.5"
-    express "^4.13.3"
-    oauth2-server "3.0.0"
-
 express-rate-limit@^5.0.0:
   version "5.2.6"
   resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0"
@@ -3423,7 +3388,7 @@ express-validator@^6.4.0:
     lodash "^4.17.20"
     validator "^13.5.2"
 
-express@^4.12.4, express@^4.13.3, express@^4.16.4, express@^4.17.1:
+express@^4.12.4, express@^4.16.4, express@^4.17.1:
   version "4.17.1"
   resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
   integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
@@ -3873,9 +3838,9 @@ gifwrap@^0.9.2:
     omggif "^1.0.10"
 
 glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
-  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
   dependencies:
     is-glob "^4.0.1"
 
@@ -3925,6 +3890,23 @@ globby@^11.0.1:
     merge2 "^1.3.0"
     slash "^3.0.0"
 
+got@^11.8.2, got@~11.8.1:
+  version "11.8.2"
+  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
+  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
+  dependencies:
+    "@sindresorhus/is" "^4.0.0"
+    "@szmarczak/http-timer" "^4.0.5"
+    "@types/cacheable-request" "^6.0.1"
+    "@types/responselike" "^1.0.0"
+    cacheable-lookup "^5.0.3"
+    cacheable-request "^7.0.1"
+    decompress-response "^6.0.0"
+    http2-wrapper "^1.0.0-beta.5.2"
+    lowercase-keys "^2.0.0"
+    p-cancelable "^2.0.0"
+    responselike "^2.0.0"
+
 got@^9.6.0:
   version "9.6.0"
   resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@@ -3942,23 +3924,6 @@ got@^9.6.0:
     to-readable-stream "^1.0.0"
     url-parse-lax "^3.0.0"
 
-got@~11.8.1:
-  version "11.8.2"
-  resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
-  integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
-  dependencies:
-    "@sindresorhus/is" "^4.0.0"
-    "@szmarczak/http-timer" "^4.0.5"
-    "@types/cacheable-request" "^6.0.1"
-    "@types/responselike" "^1.0.0"
-    cacheable-lookup "^5.0.3"
-    cacheable-request "^7.0.1"
-    decompress-response "^6.0.0"
-    http2-wrapper "^1.0.0-beta.5.2"
-    lowercase-keys "^2.0.0"
-    p-cancelable "^2.0.0"
-    responselike "^2.0.0"
-
 graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
   version "4.2.6"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
@@ -4104,9 +4069,9 @@ htmlparser2@^4.0.0, htmlparser2@^4.1.0:
     entities "^2.0.0"
 
 htmlparser2@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01"
-  integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.1.tgz#422521231ef6d42e56bd411da8ba40aa36e91446"
+  integrity sha512-GDKPd+vk4jvSuvCbyuzx/unmXkk090Azec7LovXP8as1Hn8q9p3hbjmDGbUqqhknw0ajwit6LiiWqfiTUPMK7w==
   dependencies:
     domelementtype "^2.0.1"
     domhandler "^4.0.0"
@@ -4167,7 +4132,7 @@ http-proxy-agent@^4.0.1:
     agent-base "6"
     debug "4"
 
-http-signature@1.3.5, http-signature@~1.2.0:
+http-signature@1.3.5:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"
   integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw==
@@ -4176,6 +4141,15 @@ http-signature@1.3.5, http-signature@~1.2.0:
     jsprim "^1.2.2"
     sshpk "^1.14.1"
 
+http-signature@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+  integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+  dependencies:
+    assert-plus "^1.0.0"
+    jsprim "^1.2.2"
+    sshpk "^1.7.0"
+
 http2-wrapper@^1.0.0-beta.5.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
@@ -4446,7 +4420,7 @@ is-buffer@~1.1.6:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.2, is-callable@^1.2.3:
+is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
   integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
@@ -4542,11 +4516,6 @@ is-installed-globally@^0.3.1:
     global-dirs "^2.0.1"
     is-path-inside "^3.0.1"
 
-is-interactive@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
-  integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
-
 is-nan@^1.3.0:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
@@ -4581,9 +4550,9 @@ is-obj@^2.0.0:
   integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
 
 is-path-inside@^3.0.1:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
-  integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
+  integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
 
 is-plain-obj@^1.1.0:
   version "1.1.0"
@@ -4605,7 +4574,7 @@ is-promise@^2.0.0, is-promise@^2.2.2:
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
   integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
 
-is-regex@^1.0.3, is-regex@^1.1.1, is-regex@^1.1.2:
+is-regex@^1.0.3, is-regex@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
   integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
@@ -5063,12 +5032,17 @@ lodash.isequal@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
   integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
 
+lodash@4.17.19:
+  version "4.17.19"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
+  integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
+
 lodash@4.17.21, lodash@>=4.17.13, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-log-symbols@4.0.0, log-symbols@^4.0.0:
+log-symbols@4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
   integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
@@ -5199,7 +5173,7 @@ mailsplit@5.0.1:
     libmime "5.0.0"
     libqp "1.1.0"
 
-make-dir@^3.0.0:
+make-dir@^3.0.0, make-dir@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
   integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -5496,15 +5470,15 @@ mkdirp@1.0.3:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea"
   integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==
 
-mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4:
+mkdirp@^1.0.3, mkdirp@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
 mocha@^8.0.1:
-  version "8.3.0"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.0.tgz#a83a7432d382ae1ca29686062d7fdc2c36f63fe5"
-  integrity sha512-TQqyC89V1J/Vxx0DhJIXlq9gbbL9XFNdeLQ1+JsnZsVaSOV1z3tWfw0qZmQJGQRIfkvZcs7snQnZnOCKoldq1Q==
+  version "8.3.2"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.2.tgz#53406f195fa86fbdebe71f8b1c6fb23221d69fcc"
+  integrity sha512-UdmISwr/5w+uXLPKspgoV7/RXZwKRTiTjJ2/AC5ZiEztIoOYdfKb19+9jNmEInzx5pBsCyJQzarAxqIGBNYJhg==
   dependencies:
     "@ungap/promise-all-settled" "1.1.2"
     ansi-colors "4.1.1"
@@ -5640,11 +5614,16 @@ nan@~2.14.0:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
   integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
 
-nanoid@3.1.20, nanoid@^3.1.20:
+nanoid@3.1.20:
   version "3.1.20"
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
   integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
 
+nanoid@^3.1.20:
+  version "3.1.21"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.21.tgz#25bfee7340ac4185866fbfb2c9006d299da1be7f"
+  integrity sha512-A6oZraK4DJkAOICstsGH98dvycPr/4GGDH7ZWKmMdd3vGcOurZ6JmWFUt0DA5bzrrn2FrUjmv6mFNWvv8jpppA==
+
 napi-macros@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
@@ -5883,17 +5862,17 @@ oauth-sign@~0.9.0:
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
   integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
 
-oauth2-server@3.0.0, oauth2-server@3.1.0-beta.1:
-  version "3.1.0-beta.1"
-  resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.0-beta.1.tgz#159ee4d32d148c2dc7a39f7b1ce872e039b91a41"
-  integrity sha512-FWLl/YC5NGvGzAtclhmlY9fG0nKwDP7xPiPOi5fZ4APO34BmF/vxsEp22spJNuSOrGEsp9W7jKtFCI3UBSvx5w==
+oauth2-server@3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.1.tgz#be291da840a307a50368736ab766bd68f2eeb3a9"
+  integrity sha512-4dv+fE9hrK+xTaCygOLh/kQeFzbFr7UqSyHvBDbrQq8Hg52sAkV2vTsyH3Z42hoeaKpbhM7udhL8Y4GYbl6TGQ==
   dependencies:
-    basic-auth "^2.0.0"
-    bluebird "^3.5.1"
-    lodash "^4.17.10"
-    promisify-any "^2.0.1"
-    statuses "^1.5.0"
-    type-is "^1.6.16"
+    basic-auth "2.0.1"
+    bluebird "3.7.2"
+    lodash "4.17.19"
+    promisify-any "2.0.1"
+    statuses "1.5.0"
+    type-is "1.6.18"
 
 object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1:
   version "4.1.1"
@@ -5910,7 +5889,7 @@ object-hash@2.1.1:
   resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09"
   integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==
 
-object-inspect@^1.8.0, object-inspect@^1.9.0:
+object-inspect@^1.9.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
   integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
@@ -5920,7 +5899,7 @@ object-keys@^1.0.12, object-keys@^1.1.1:
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-object.assign@^4.1.1, object.assign@^4.1.2:
+object.assign@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
   integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@@ -6029,20 +6008,6 @@ optionator@^0.9.1:
     type-check "^0.4.0"
     word-wrap "^1.2.3"
 
-ora@^5.1.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f"
-  integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==
-  dependencies:
-    bl "^4.0.3"
-    chalk "^4.1.0"
-    cli-cursor "^3.1.0"
-    cli-spinners "^2.5.0"
-    is-interactive "^1.0.0"
-    log-symbols "^4.0.0"
-    strip-ansi "^6.0.0"
-    wcwidth "^1.0.1"
-
 os-homedir@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
@@ -6067,9 +6032,9 @@ p-cancelable@^1.0.0:
   integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
 
 p-cancelable@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e"
-  integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.0.tgz#4d51c3b91f483d02a0d300765321fca393d758dd"
+  integrity sha512-HAZyB3ZodPo+BDpb4/Iu7Jv4P6cSazBz9ZM0ChhEXp70scx834aWCEjQRwgt41UzzejUAPdbqqONfRWTPYrPAQ==
 
 p-finally@^1.0.0:
   version "1.0.0"
@@ -6490,11 +6455,11 @@ pngjs@^3.0.0, pngjs@^3.3.3:
   integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
 
 postcss@^8.0.2:
-  version "8.2.6"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe"
-  integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==
+  version "8.2.8"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
+  integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
   dependencies:
-    colorette "^1.2.1"
+    colorette "^1.2.2"
     nanoid "^3.1.20"
     source-map "^0.6.1"
 
@@ -6579,7 +6544,7 @@ promise@^7.0.1:
   dependencies:
     asap "~2.0.3"
 
-promisify-any@^2.0.1:
+promisify-any@2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/promisify-any/-/promisify-any-2.0.1.tgz#403e00a8813f175242ab50fe33a69f8eece47305"
   integrity sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU=
@@ -7060,7 +7025,7 @@ render-media@^4.1.0:
     stream-to-blob-url "^3.0.2"
     videostream "^3.2.2"
 
-request@^2.81.0, request@^2.88.0:
+request@^2.88.0:
   version "2.88.2"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
   integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -7153,10 +7118,10 @@ restore-cursor@^3.1.0:
     onetime "^5.1.0"
     signal-exit "^3.0.2"
 
-retimer@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/retimer/-/retimer-2.0.0.tgz#e8bd68c5e5a8ec2f49ccb5c636db84c04063bbca"
-  integrity sha512-KLXY85WkEq2V2bKex/LOO1ViXVn2KGYe4PYysAdYdjmraYIUsVkXu8O4am+8+5UbaaGl1qho4aqAAPHNQ4GSbg==
+retimer@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df"
+  integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==
 
 retry-as-promised@^3.2.0:
   version "3.2.0"
@@ -7544,9 +7509,9 @@ socket.io-client@2.2.0:
     to-array "0.1.4"
 
 socket.io-client@^3.0.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.2.tgz#77be8c180cef29121970856e8f48e5463631020a"
-  integrity sha512-fXhF8plHrd7U14A7K0JPOmZzpmGkLpIS6623DzrBZqYzI/yvlP4fA3LnxwthEVgiHmn2uJ4KjdnQD8A03PuBWQ==
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.3.tgz#57ddcefea58cfab71f0e94c21124de8e3c5aa3e2"
+  integrity sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==
   dependencies:
     "@types/component-emitter" "^1.2.10"
     backo2 "~1.0.2"
@@ -7700,7 +7665,7 @@ srt-to-vtt@^1.1.2:
     through2 "^0.6.3"
     to-utf-8 "^1.2.0"
 
-sshpk@^1.14.1:
+sshpk@^1.14.1, sshpk@^1.7.0:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
   integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
@@ -7725,7 +7690,7 @@ standard-as-callback@^2.0.1:
   resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126"
   integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==
 
-"statuses@>= 1.5.0 < 2", statuses@^1.5.0, statuses@~1.5.0:
+statuses@1.5.0, "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
@@ -7811,7 +7776,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
-string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.4:
+string.prototype.trimend@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
   integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
@@ -7819,7 +7784,7 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.4:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
 
-string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.4:
+string.prototype.trimstart@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
   integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
@@ -8248,9 +8213,9 @@ tslib@^1.8.1, tslib@^1.9.0:
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
 tsutils@^3.17.1:
-  version "3.20.0"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698"
-  integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
+  integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
   dependencies:
     tslib "^1.8.1"
 
@@ -8298,7 +8263,7 @@ type-fest@^0.8.1:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
   integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
 
-type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
+type-is@1.6.18, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
   integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -8312,9 +8277,9 @@ type@^1.0.1:
   integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
 
 type@^2.0.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/type/-/type-2.3.0.tgz#ada7c045f07ead08abf9e2edd29be1a0c0661132"
-  integrity sha512-rgPIqOdfK/4J9FhiVrZ3cveAjRRo5rsQBAIhnylX874y1DX/kEKSVdLsnuHB6l1KTjHyU01VjiMBHgU2adejyg==
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
+  integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
 
 typedarray-to-buffer@^3.0.0, typedarray-to-buffer@^3.1.5:
   version "3.1.5"
@@ -8329,9 +8294,9 @@ typedarray@^0.0.6:
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
 typescript@^4.0.5:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c"
-  integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
+  integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
 
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
@@ -8541,9 +8506,9 @@ uuid@^3.3.2, uuid@^3.4.0:
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
 v8-compile-cache@^2.0.3:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"
-  integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
 
 valid-data-url@^3.0.0:
   version "3.0.1"
@@ -8604,7 +8569,7 @@ void-elements@^3.1.0:
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
   integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
 
-wcwidth@>=1.0.1, wcwidth@^1.0.1:
+wcwidth@>=1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
   integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
@@ -8631,9 +8596,9 @@ webfinger.js@^2.6.6:
     xhr2 "^0.1.4"
 
 webtorrent@^0.115.1:
-  version "0.115.1"
-  resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.115.1.tgz#3984e6b17fdb8ad68b5cdbd42c46a2288e1b3bb2"
-  integrity sha512-8kq498EMUjYu18wlfoZ42wvz9oUAJrobJbHQGRHl0sbrPVBt17H4FVoAc502XSMCbFzhMx5Vqd7Wz4JTTCPvuQ==
+  version "0.115.3"
+  resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.115.3.tgz#2d0a53b65326ffd0124b3592950c4c75e299730a"
+  integrity sha512-DNryTNoAHse+zxArBZg25U8B97KNPeVjGzrjRB+oDnGROuKfQcvLh8/9K79FDfQTYVpInMmr9l0ksIsEjz/L2g==
   dependencies:
     addr-to-ip-port "^1.5.1"
     bitfield "^4.0.0"
@@ -8839,9 +8804,9 @@ ws@^5.2.2:
     async-limiter "~1.0.0"
 
 ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2:
-  version "7.4.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd"
-  integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==
+  version "7.4.4"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
+  integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
 
 ws@~6.1.0:
   version "6.1.4"
@@ -8957,9 +8922,9 @@ yargs-parser@^18.1.2:
     decamelize "^1.2.0"
 
 yargs-parser@^20.2.2:
-  version "20.2.6"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20"
-  integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA==
+  version "20.2.7"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
+  integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
 
 yargs-unparser@2.0.0:
   version "2.0.0"