1
0
Fork 0
-----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCAAdFiEExEqtY4NnkSypPt1XWDphLYkBWb4FAmYvTgIACgkQWDphLYkB
 Wb6g6wgAljLbcIKveQgJGWcwlwLefkVDd6up+kBv+0pCFOkdIihV6G7SQRdUujS3
 U3/mjOB+hgtIbv9Bu9ahY30831px7gm0/5d7hntkCqoM805kkaf2U2y62oFY+SNq
 VeiUu0L3tnHdaxIdgsKq+2sX/dZKA0bai+IjcjJaEXc8ZVwiNVU+qXLzEk3xVECM
 o2LtJp10q8qIA+YjIc9niMfz94a/iwUaix9VY3uG3o8xNeBVVM9PcZDRywFc7NJI
 Ve/OeEYQ4ydjWYV/RaNZQAVc9zFJObrFwOjWwdYshSz8NbwM8GaxwU36SQUpvfg5
 JEF2jmGRA6FzTaaOeA8gwR8O6rq3FA==
 =PULO
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQJNBAABCAA3FiEEGCBYi9NGimfngx9dVTwOu+tdXwgFAmYxd7MZHGtvdG92YWxl
 eGFyaWFuQGdtYWlsLmNvbQAKCRBVPA67611fCOHjD/44LHXuzlMJBFvW7CLxmao8
 ryUuO/BPd2eBHzhZ3b+qJzv9A1bIVqh4ocgRtn8Kukll+z1JWaJi9Tc79ReoOmOK
 kI9kEM8YTKWwoyun0akaEn+K9xm13XeqJdWLmaIsHhChuj5fvAnfaXHuFOb13LCG
 wN03RePZfXZMX9OziuXKbCN29f4zthzpObompzw2Lz7qOfVv5FFIJPE2v/vP8Mnq
 09KTVnb2hHluy1pAjpDPRNS9yzIqXXrifGjxKF8PSnoUqzlz/KN4/wfzD1PBXa3m
 io5b5w/yPRvRZd8u6Co5MMoFZvV2veYEEuIQIklRuw0Ag5wIr4unCfoEo/dMcwBZ
 AWRizDxo6C95Y2mbCnXfw7Sx8oDWWH623qUcIJPW0llE/K0LyVvwM6LGdszcFUjD
 iHAWARg01p8xX/TUz4baAP5Rt0i1v4lUhAg/kR+x2kcVuxcMj6/b+jhZElF2KTPt
 LcQwhTovvz7WtspP4hmRY9i3DGydr/4LSl8+n6hHDbvs4gznsQwcD2sSJoN1cSzR
 XrAhMN2HyKw/ok4Sqen01oPJZL5XdzJVFFWLfAZZ/A1J9uk9UHAzBsyNXWfwK9KN
 /p9u2JN/uXgvmtj4uc8ECaFwyuK05NFS1Ax5PT649Ugein/shHIQCchVArVahtm8
 xAt7J4WEPTB+KfUi+vK6yQ==
 =LCjE
 -----END PGP SIGNATURE-----

Merge tag 'v6.1.0' into changes

v6.1.0
This commit is contained in:
Alex Kotov 2024-05-01 02:58:57 +04:00
commit 29ac4fc084
118 changed files with 19921 additions and 9930 deletions

View File

@ -1,6 +1,6 @@
# Changelog
## v6.1.0-rc.1
## v6.1.0
### IMPORTANT NOTES
@ -11,6 +11,11 @@
* Views use a *Session ID* generated by the web browser instead of using the request IP (former behavior can be restored in YAML config)
* The goal of this change is to get closer to how other video platforms like Mux, Vimeo, or Instagram work
### SECURITY
* Compact ActivityPub JSON-LD objects before using them to prevent incorrect access control @tesaguri
* Protect ActivityPub information related to private/internal/blocked videos
### Admin config (non-exhaustive)
* **Breaking changes**:
@ -127,6 +132,9 @@
* Fix view endpoint crash on geoip update failure
* Fix setting video subtitle from URL query
* Fix selecting "Display all languages/categories/licences" in videos search resulting in an empty search
* Fix followers/following counter of local ActivityPub actors
* Fix notification button link on mobile
* Fix player subtitles on iOS
## v6.0.4

View File

@ -1,6 +1,6 @@
{
"name": "peertube-client",
"version": "6.1.0-rc.1",
"version": "6.1.0",
"private": true,
"license": "AGPL-3.0",
"author": {

View File

@ -216,7 +216,10 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
getFilesSize (video: Video) {
let total = getAllFiles(video).reduce((p, f) => p += f.size, 0)
total += video.videoSource?.size || 0
if (video.videoSource?.fileDownloadUrl) {
total += video.videoSource.size || 0
}
return total
}

View File

@ -309,7 +309,7 @@ export class VideoStatsComponent implements OnInit {
{
label: $localize`Views`,
value: this.numberFormatter.transform(this.video.views),
help: $localize`A view means that someone watched the video for at least 30 seconds`
help: $localize`A view means that someone watched the video for several seconds (10 seconds by default)`
},
{
label: $localize`Likes`,

View File

@ -1,21 +1,29 @@
import { Subject, Subscription } from 'rxjs'
import { filter } from 'rxjs/operators'
import { Component, EventEmitter, Output, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { NavigationEnd, Router } from '@angular/router'
import { Notifier, PeerTubeSocket, ScreenService } from '@app/core'
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { UserNotificationsComponent } from '@app/shared/standalone-notifications/user-notifications.component'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { CommonModule } from '@angular/common'
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'
import { Notifier, PeerTubeSocket, ScreenService } from '@app/core'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { LoaderComponent } from '@app/shared/shared-main/loaders/loader.component'
import { UserNotificationService } from '@app/shared/shared-main/users/user-notification.service'
import { UserNotificationsComponent } from '@app/shared/standalone-notifications/user-notifications.component'
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { Subject, Subscription } from 'rxjs'
import { filter } from 'rxjs/operators'
@Component({
selector: 'my-notification',
templateUrl: './notification.component.html',
styleUrls: [ './notification.component.scss' ],
standalone: true,
imports: [ CommonModule, NgbPopoverModule, UserNotificationsComponent, GlobalIconComponent, LoaderComponent ]
imports: [
CommonModule,
NgbPopoverModule,
UserNotificationsComponent,
GlobalIconComponent,
LoaderComponent,
RouterLink,
RouterLinkActive
]
})
export class NotificationComponent implements OnInit, OnDestroy {
@ViewChild('popover', { static: true }) popover: NgbPopover

View File

@ -361,7 +361,10 @@ export class PeerTubePlayer {
getVideojsOptions (): videojs.PlayerOptions {
const html5 = {
preloadTextTracks: false
preloadTextTracks: false,
// Prevent a bug on iOS where the text tracks added by peertube plugin are removed on play
// See https://github.com/Chocobozzz/PeerTube/issues/6351
nativeTextTracks: false
}
const plugins: VideoJSPluginOptions = {

View File

@ -83,7 +83,7 @@
}
</script>
<my-app role="main">
<my-app>
</my-app>
</body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file source-language="en-US" datatype="plaintext" original="ng2.template" target-language="ro">
<body>
@ -106,33 +106,33 @@
<context context-type="linenumber">5</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.first-aria" datatype="html">
<trans-unit id="ngb.pagination.first-aria" datatype="html" xml:space="preserve">
<source>First</source>
<target/>
<target state="translated">Primul</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">14</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
<trans-unit id="ngb.pagination.previous-aria" datatype="html" xml:space="preserve">
<source>Previous</source>
<target/>
<target state="translated">Anterior</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">24</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.next-aria" datatype="html">
<trans-unit id="ngb.pagination.next-aria" datatype="html" xml:space="preserve">
<source>Next</source>
<target/>
<target state="translated">Următorul</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">44</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.pagination.last-aria" datatype="html">
<trans-unit id="ngb.pagination.last-aria" datatype="html" xml:space="preserve">
<source>Last</source>
<target/>
<target state="translated">Ultimul</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">53</context>
@ -146,9 +146,9 @@
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
<trans-unit id="ngb.timepicker.increment-hours" datatype="html" xml:space="preserve">
<source>Increment hours</source>
<target/>
<target state="translated">Adaugă ore</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">9</context>
@ -170,17 +170,17 @@
<context context-type="linenumber">15</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html" xml:space="preserve">
<source>Decrement hours</source>
<target/>
<target state="translated">Scade ore</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html" xml:space="preserve">
<source>Increment minutes</source>
<target/>
<target state="translated">Adaugă minute</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">32</context>
@ -202,17 +202,17 @@
<context context-type="linenumber">37</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html" xml:space="preserve">
<source>Decrement minutes</source>
<target/>
<target state="translated">Scade minute</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">45</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html" xml:space="preserve">
<source>Increment seconds</source>
<target/>
<target state="translated">Adaugă secunde</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">54</context>
@ -234,9 +234,9 @@
<context context-type="linenumber">59</context>
</context-group>
</trans-unit>
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html" xml:space="preserve">
<source>Decrement seconds</source>
<target/>
<target state="translated">Scade secunde</target>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">67</context>
@ -298,17 +298,17 @@
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="066903c4bc7d397c799979d64ce8c450792eb664" datatype="html">
<trans-unit id="066903c4bc7d397c799979d64ce8c450792eb664" datatype="html" xml:space="preserve">
<source>Your video <x id="START_LINK" ctype="x-a" equiv-text="&lt;a&gt;"/><x id="INTERPOLATION" equiv-text="{{ notification.videoBlacklist.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> has been blacklisted </source>
<target/>
<target state="translated">Videoul tău  <x id="START_LINK" ctype="x-a" equiv-text="&lt;a&gt;"/><x id="INTERPOLATION" equiv-text="{{ notification.videoBlacklist.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> a fost blocat. </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context>
<context context-type="linenumber">40</context>
</context-group>
</trans-unit>
<trans-unit id="b5e2152dd5b4222093fcec9c8289f12308a598e7" datatype="html">
<trans-unit id="b5e2152dd5b4222093fcec9c8289f12308a598e7" datatype="html" xml:space="preserve">
<source><x id="START_LINK" ctype="x-a" equiv-text="&lt;a&gt;"/>A new video abuse<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> has been created on video <x id="START_LINK_1" ctype="x-a" equiv-text="&lt;a&gt;"/><x id="INTERPOLATION" equiv-text="{{ notification.videoAbuse.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> </source>
<target/>
<target state="translated"><x id="START_LINK" ctype="x-a" equiv-text="&lt;a&gt;"/>A new video abuse<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> has been created on video <x id="START_LINK_1" ctype="x-a" equiv-text="&lt;a&gt;"/><x id="INTERPOLATION" equiv-text="{{ notification.videoAbuse.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context>
<context context-type="linenumber">48</context>
@ -438,9 +438,9 @@
<context context-type="linenumber">42</context>
</context-group>
</trans-unit>
<trans-unit id="1394835141143590910" datatype="html">
<trans-unit id="1394835141143590910" datatype="html" xml:space="preserve">
<source>Start at</source>
<target/>
<target state="translated">Începe la</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context>
<context context-type="linenumber">17</context>
@ -454,9 +454,9 @@
<context context-type="linenumber">75</context>
</context-group>
</trans-unit>
<trans-unit id="5964984095397511808" datatype="html">
<trans-unit id="5964984095397511808" datatype="html" xml:space="preserve">
<source>Stop at</source>
<target/>
<target state="translated">Oprește la</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context>
<context context-type="linenumber">31</context>
@ -766,17 +766,17 @@
<context context-type="linenumber">26</context>
</context-group>
</trans-unit>
<trans-unit id="8558962068274430520" datatype="html">
<trans-unit id="8558962068274430520" datatype="html" xml:space="preserve">
<source>Unfederate the video</source>
<target/>
<target state="translated">Decuplați videoclipul</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/modals/video-blacklist.component.html</context>
<context context-type="linenumber">23</context>
</context-group>
</trans-unit>
<trans-unit id="7539427273132299890" datatype="html">
<trans-unit id="7539427273132299890" datatype="html" xml:space="preserve">
<source>Unlisted</source>
<target/>
<target state="translated">Nelistat</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
<context context-type="linenumber">6</context>
@ -802,9 +802,9 @@
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="7688104409544625220" datatype="html">
<trans-unit id="7688104409544625220" datatype="html" xml:space="preserve">
<source>{VAR_PLURAL, plural, =1 {1 view} other {<x id="INTERPOLATION" equiv-text="{{ video.views | myNumberFormatter }}"/> views} }</source>
<target/>
<target state="translated">{VAR_PLURAL, plural, =1 {1 view} other {<x id="INTERPOLATION" equiv-text="{{ video.views | myNumberFormatter }}"/> views} }</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
<context context-type="linenumber">23</context>
@ -818,9 +818,9 @@
<context context-type="linenumber">41</context>
</context-group>
</trans-unit>
<trans-unit id="3514509630940272440" datatype="html">
<trans-unit id="3514509630940272440" datatype="html" xml:space="preserve">
<source>Sensitive</source>
<target/>
<target state="translated">Sensitiv</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
<context context-type="linenumber">45</context>
@ -874,9 +874,9 @@
<context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="5263519165976128456" datatype="html">
<trans-unit id="5263519165976128456" datatype="html" xml:space="preserve">
<source>Edit starts/stops at</source>
<target/>
<target state="translated">Editarea începe/oprește la</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video-playlist/video-playlist-element-miniature.component.html</context>
<context context-type="linenumber">50</context>
@ -1182,9 +1182,9 @@
<context context-type="linenumber">17</context>
</context-group>
</trans-unit>
<trans-unit id="c590f63488a5179f20a46930cf5e673f8136673d" datatype="html">
<trans-unit id="c590f63488a5179f20a46930cf5e673f8136673d" datatype="html" xml:space="preserve">
<source>You can interact with this via any ActivityPub-capable fediverse instance.<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/><x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/> For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there. </source>
<target/>
<target state="translated">Poți interacționa cu acesta prin orice ActivityPub-compatibil instanță fediverse.<x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/><x id="LINE_BREAK" ctype="lb" equiv-text="&lt;br/&gt;"/>De exemplu cu Mastodon sau Pleroma poți scrie URL-ul curent în cutia de căutare și interacționa cu el acolo. </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/user-subscription/remote-subscribe.component.html</context>
<context context-type="linenumber">26</context>
@ -1198,9 +1198,9 @@
<context context-type="linenumber">5</context>
</context-group>
</trans-unit>
<trans-unit id="5975923297757530070" datatype="html">
<trans-unit id="5975923297757530070" datatype="html" xml:space="preserve">
<source><x id="START_TAG_DIV" ctype="x-div" equiv-text="&lt;div&gt;"/>Default NSFW/sensitive videos policy<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="&lt;/div&gt;"/> <x id="START_TAG_DIV_1" ctype="x-div" equiv-text="&lt;div&gt;"/>can be redefined by the users<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="&lt;/div&gt;"/> </source>
<target/>
<target state="translated"><x id="START_TAG_DIV" ctype="x-div" equiv-text="&lt;div&gt;"/>Predefinit NSFW/sensitive videoclipuri regulamentului<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="&lt;/div&gt;"/><x id="START_TAG_DIV_1" ctype="x-div" equiv-text="&lt;div&gt;"/>poate fii redefinit de<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="&lt;/div&gt;"/>. </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">11</context>
@ -1266,9 +1266,9 @@
<context context-type="linenumber">139</context>
</context-group>
</trans-unit>
<trans-unit id="1502595455339510144" datatype="html">
<trans-unit id="1502595455339510144" datatype="html" xml:space="preserve">
<source>Unlimited <x id="START_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="&lt;ng-container&gt;"/>(<x id="INTERPOLATION" equiv-text="{{ dailyUserVideoQuota | bytes: 0 }}"/> per day)<x id="CLOSE_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="&lt;/ng-container&gt;"/> </source>
<target/>
<target state="translated">Nelimitat<x id="START_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="&lt;ng-container&gt;"/><x id="INTERPOLATION" equiv-text="{{ dailyUserVideoQuota | bytes: 0 }}"/>pe zi<x id="CLOSE_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="&lt;/ng-container&gt;"/>/ </target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">59</context>
@ -1306,9 +1306,9 @@
<context context-type="linenumber">77</context>
</context-group>
</trans-unit>
<trans-unit id="7683705529753923369" datatype="html">
<trans-unit id="7683705529753923369" datatype="html" xml:space="preserve">
<source>Player</source>
<target/>
<target state="translated">Jucător</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">85</context>
@ -1334,9 +1334,9 @@
<context context-type="linenumber">7</context>
</context-group>
</trans-unit>
<trans-unit id="8630916846096019339" datatype="html">
<trans-unit id="8630916846096019339" datatype="html" xml:space="preserve">
<source>Users can resolve distant content</source>
<target/>
<target state="translated">Utilizatorii pot accesa conținutul de la distanță</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">100</context>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4228,11 +4228,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">281</context>
<context context-type="linenumber">284</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">336</context>
<context context-type="linenumber">339</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts</context>
@ -6444,91 +6444,91 @@
<source>Are you sure you want to delete this <x id="PH" equiv-text="file.resolution.label"/> file?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">225</context>
<context context-type="linenumber">228</context>
</context-group>
</trans-unit>
<trans-unit id="6693349469471580292" datatype="html">
<source>Delete file</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">226</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="7016764388104297354" datatype="html">
<source>File removed.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">232</context>
<context context-type="linenumber">235</context>
</context-group>
</trans-unit>
<trans-unit id="925076027211452339" datatype="html">
<source>Are you sure you want to delete the original file of this video?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">241</context>
<context context-type="linenumber">244</context>
</context-group>
</trans-unit>
<trans-unit id="3014914668468316940" datatype="html">
<source>Delete original file</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">242</context>
<context context-type="linenumber">245</context>
</context-group>
</trans-unit>
<trans-unit id="6647462936549531405" datatype="html">
<source>Original file removed.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">248</context>
<context context-type="linenumber">251</context>
</context-group>
</trans-unit>
<trans-unit id="1314383205093440631" datatype="html">
<source>Are you sure you want to delete {count, plural, =1 {this video} other {these <x id="count"/> videos}}?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">277</context>
<context context-type="linenumber">280</context>
</context-group>
</trans-unit>
<trans-unit id="5779580280418408097" datatype="html">
<source>Deleted {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">289</context>
<context context-type="linenumber">292</context>
</context-group>
</trans-unit>
<trans-unit id="9164541937317586242" datatype="html">
<source>Unblocked {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">307</context>
<context context-type="linenumber">310</context>
</context-group>
</trans-unit>
<trans-unit id="6228449077605046873" datatype="html">
<source>Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {<x id="count"/> HLS streaming playlists}}?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">325</context>
<context context-type="linenumber">328</context>
</context-group>
</trans-unit>
<trans-unit id="4435640428611044716" datatype="html">
<source>Are you sure you want to delete Web Video files of {count, plural, =1 {1 video} other {<x id="count"/> videos}}?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">331</context>
<context context-type="linenumber">334</context>
</context-group>
</trans-unit>
<trans-unit id="1571742433738679426" datatype="html">
<source>Files were removed.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">342</context>
<context context-type="linenumber">345</context>
</context-group>
</trans-unit>
<trans-unit id="7054344823477412274" datatype="html">
<source>Transcoding jobs created.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
<context context-type="linenumber">354</context>
<context context-type="linenumber">357</context>
</context-group>
</trans-unit>
<trans-unit id="2591467977473302125" datatype="html">

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,51 +14,51 @@
"Copy magnet URI": "Copiar lo magnet URI",
"Total downloaded: ": "Total telecargat: ",
"Total uploaded: ": "Total enviat: ",
"From servers: ": "From servers: ",
"From peers: ": "From peers: ",
"From servers: ": "Dels servidors : ",
"From peers: ": "Dels pars : ",
"Normal mode": "Normal mode",
"Stats for nerds": "Stats for nerds",
"Theater mode": "Theater mode",
"Video UUID": "Video UUID",
"Viewport / Frames": "Viewport / Frames",
"Resolution": "Resolution",
"Stats for nerds": "Estatisticas pels nerds",
"Theater mode": "Mòde cinèma",
"Video UUID": "UUID de la vidèo",
"Viewport / Frames": "Fenèstra dafichatge / Quadres",
"Resolution": "Resolucion",
"Volume": "Volume",
"Codecs": "Codecs",
"Color": "Color",
"Go back to the live": "Go back to the live",
"Connection Speed": "Connection Speed",
"Network Activity": "Network Activity",
"Total Transfered": "Total Transfered",
"Download Breakdown": "Download Breakdown",
"Go back to the live": "Tornar al dirèct",
"Connection Speed": "Velocitat de la connexion",
"Network Activity": "Activitat ret",
"Total Transfered": "Total Transferit",
"Download Breakdown": "Reparticion dels telecargaments",
"Buffer Progress": "Buffer Progress",
"Buffer State": "Buffer State",
"Live Latency": "Live Latency",
"Buffer State": "Estat memòria tampon",
"Live Latency": "Laténcia en viu",
"P2P": "P2P",
"{1} seconds": "{1} seconds",
"enabled": "enabled",
"{1} seconds": "{1} segondas",
"enabled": "activat",
"Playlist: {1}": "Playlist: {1}",
"disabled": "disabled",
"disabled": "desactivat",
" off": " desactivats",
"Player mode": "Player mode",
"Play in loop": "Play in loop",
"This live has not started yet.": "This live has not started yet.",
"This live has ended.": "This live has ended.",
"Play in loop": "Lector en bocla",
"This live has not started yet.": "Lo dirècte a pas encara començat.",
"This live has ended.": "Lo dirèct es acabat.",
"The video failed to play, will try to fast forward.": "The video failed to play, will try to fast forward.",
"{1} / {2} dropped of {3}": "{1} / {2} dropped of {3}",
" (muted)": " (muted)",
"{1} from servers · {2} from peers": "{1} from servers · {2} from peers",
"Previous video": "Previous video",
"Video page (new window)": "Video page (new window)",
" (muted)": " (mut)",
"{1} from servers · {2} from peers": "{1} dels servidors · {2} dels pars",
"Previous video": "Vidèo precedenta",
"Video page (new window)": "Pagina vidèo (fenèstra novèla)",
"Next video": "Next video",
"This video is password protected": "This video is password protected",
"You need a password to watch this video.": "You need a password to watch this video.",
"Incorrect password, please enter a correct password": "Incorrect password, please enter a correct password",
"Cancel": "Cancel",
"This video is password protected": "Un senhal protegís aquesta vidèo",
"You need a password to watch this video.": "Devètz fornir un senhal per agachar aquesta vidèo.",
"Incorrect password, please enter a correct password": "Senhal incorrècte, mercés de picar un senhal corrècte",
"Cancel": "Anullar",
"Up Next": "Up Next",
"Autoplay is suspended": "Autoplay is suspended",
"Autoplay is suspended": "La lectura automatica es suspenduda",
"{1} (from edge: {2})": "{1} (from edge: {2})",
"Disable subtitles": "Disable subtitles",
"Enable {1} subtitle": "Enable {1} subtitle",
"Disable subtitles": "Desactivar los sostítols",
"Enable {1} subtitle": "Activar {1} sostítol",
"Audio Player": "Lector àudio",
"Video Player": "Lector vidèo",
"Play": "Lectura",

View File

@ -272,5 +272,5 @@
"Traditional Chinese": "Traditional Chinese",
"Misc": "Divèrs",
"Normal mode": "Normal mode",
"Theater mode": "Theater mode"
"Theater mode": "Mòde cinèma"
}

View File

@ -1,7 +1,7 @@
{
"name": "peertube",
"description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.",
"version": "6.1.0-rc.1",
"version": "6.1.0",
"private": true,
"licence": "AGPL-3.0",
"engines": {
@ -237,7 +237,7 @@
"pngjs": "^7.0.0",
"proxy": "^2.1.1",
"socket.io-client": "^4.5.4",
"supertest": "^6.0.1",
"supertest": "^7.0.0",
"swagger-cli": "^4.0.2",
"tsc-watch": "^6.0.0",
"tsx": "^4.7.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -3,12 +3,12 @@
import { expect } from 'chai'
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { sortObjectComparator } from '@peertube/peertube-core-utils'
import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
import { HttpStatusCode, UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
import {
BlacklistCommand,
cleanupTests,
createMultipleServers,
doubleFollow, PeerTubeServer,
doubleFollow, makeActivityPubGetRequest, PeerTubeServer,
setAccessTokensToServers,
setDefaultChannelAvatar,
waitJobs
@ -298,6 +298,13 @@ describe('Test video blacklist', function () {
expect(video4Blacklisted.unfederated).to.be.true
})
it('Should not have AP comments/announces/likes/dislikes', async function () {
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/comments`, HttpStatusCode.UNAUTHORIZED_401)
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/announces`, HttpStatusCode.UNAUTHORIZED_401)
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/likes`, HttpStatusCode.UNAUTHORIZED_401)
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/dislikes`, HttpStatusCode.UNAUTHORIZED_401)
})
it('Should remove the video from blacklist and refederate the video', async function () {
await command.remove({ videoId: video4UUID })

View File

@ -165,15 +165,17 @@ describe('Test video source management', function () {
expect(data[1].videoSource.fileDownloadUrl).to.exist
expect(data[2].videoSource).to.exist
expect(data[2].videoSource.fileDownloadUrl).to.not.exist
expect(data[2].videoSource.createdAt).to.exist
expect(data[2].videoSource.fps).to.be.null
expect(data[2].videoSource.height).to.be.null
expect(data[2].videoSource.width).to.be.null
expect(data[2].videoSource.resolution.id).to.be.null
expect(data[2].videoSource.resolution.label).to.be.null
expect(data[2].videoSource.size).to.be.null
expect(data[2].videoSource.metadata).to.be.null
expect(data[2].videoSource.fps).to.to.exist
expect(data[2].videoSource.height).to.to.exist
expect(data[2].videoSource.width).to.to.exist
expect(data[2].videoSource.resolution.id).to.to.exist
expect(data[2].videoSource.resolution.label).to.to.exist
expect(data[2].videoSource.size).to.to.exist
expect(data[2].videoSource.metadata).to.to.exist
})
it('Should delete all videos and do not have original files anymore', async function () {

View File

@ -3,7 +3,7 @@
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import { signAndContextify } from '@peertube/peertube-server/core/helpers/activity-pub-utils.js'
import { isHTTPSignatureVerified, parseHTTPSignature } from '@peertube/peertube-server/core/helpers/peertube-crypto.js'
import { isJsonLDSignatureVerified, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
import { compactJSONLDAndCheckSignature, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
import { expect } from 'chai'
import { readJsonSync } from 'fs-extra/esm'
import cloneDeep from 'lodash-es/cloneDeep.js'
@ -24,6 +24,10 @@ function fakeFilter () {
return (data: any) => Promise.resolve(data)
}
function fakeExpressReq (body: any) {
return { body }
}
describe('Test activity pub helpers', function () {
describe('When checking the Linked Signature', function () {
@ -33,7 +37,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
expect(result).to.be.false
})
@ -43,7 +47,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
expect(result).to.be.false
})
@ -53,7 +57,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
expect(result).to.be.true
})
@ -72,7 +76,7 @@ describe('Test activity pub helpers', function () {
})
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(signedBody))
expect(result).to.be.false
})
@ -91,7 +95,7 @@ describe('Test activity pub helpers', function () {
})
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(signedBody))
expect(result).to.be.true
})

View File

@ -120,7 +120,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
activityPubClientRouter.get('/videos/watch/:id/announces',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoAnnouncesController)
)
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
@ -132,19 +132,19 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
activityPubClientRouter.get('/videos/watch/:id/likes',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoLikesController)
)
activityPubClientRouter.get('/videos/watch/:id/dislikes',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoDislikesController)
)
activityPubClientRouter.get('/videos/watch/:id/comments',
executeIfActivityPub,
activityPubRateLimiter,
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoCommentsController)
)
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
@ -175,7 +175,7 @@ activityPubClientRouter.get('/videos/watch/:id/chapters',
activityPubRateLimiter,
apVideoChaptersSetCacheKey,
chaptersCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
asyncMiddleware(videosCustomGetValidator('only-video')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(videoChaptersController)
)
@ -330,7 +330,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
}
async function videoAnnouncesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
@ -347,7 +347,7 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
}
async function videoLikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
@ -357,7 +357,7 @@ async function videoLikesController (req: express.Request, res: express.Response
}
async function videoDislikesController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return
@ -367,7 +367,7 @@ async function videoDislikesController (req: express.Request, res: express.Respo
}
async function videoCommentsController (req: express.Request, res: express.Response) {
const video = res.locals.onlyImmutableVideo
const video = res.locals.onlyVideo
if (redirectIfNotOwned(video.url, res)) return

View File

@ -11,7 +11,7 @@ import { replaceChapters } from '@server/lib/video-chapters.js'
const videoChaptersRouter = express.Router()
videoChaptersRouter.get('/:id/chapters',
asyncMiddleware(videosCustomGetValidator('only-video')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
asyncMiddleware(listVideoChapters)
)

View File

@ -1,6 +1,7 @@
import express from 'express'
import { HttpStatusCode, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models'
import { HttpStatusCode, VideoChangeOwnershipStatus } from '@peertube/peertube-models'
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
import { MVideoFullLight } from '@server/types/models/index.js'
import express from 'express'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
@ -113,7 +114,7 @@ function acceptOwnership (req: express.Request, res: express.Response) {
const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
targetVideoUpdated.VideoChannel = channel
if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) {
if (canVideoBeFederated(targetVideoUpdated)) {
await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t)
await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor)
}

View File

@ -68,12 +68,8 @@ async function deleteVideoLatestSourceFile (req: express.Request, res: express.R
await video.removeOriginalFile(videoSource)
videoSource.keptOriginalFilename = null
videoSource.fps = null
videoSource.resolution = null
videoSource.width = null
videoSource.height = null
videoSource.metadata = null
videoSource.size = null
videoSource.storage = null
await videoSource.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)

View File

@ -7,7 +7,7 @@ const tokenRouter = express.Router()
tokenRouter.post('/:id/token',
optionalAuthenticate,
asyncMiddleware(videosCustomGetValidator('only-video')),
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
videoFileTokenValidator,
generateToken
)

View File

@ -1,9 +1,11 @@
import express, { UploadFiles } from 'express'
import { Transaction } from 'sequelize'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode, ThumbnailType, VideoPrivacy, VideoPrivacyType, VideoUpdate } from '@peertube/peertube-models'
import { exists } from '@server/helpers/custom-validators/misc.js'
import { changeVideoChannelShare } from '@server/lib/activitypub/share.js'
import { isNewVideoPrivacyForFederation, isPrivacyForFederation } from '@server/lib/activitypub/videos/federate.js'
import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js'
import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js'
import { VideoPathManager } from '@server/lib/video-path-manager.js'
import { setVideoPrivacy } from '@server/lib/video-privacy.js'
import { setVideoTags } from '@server/lib/video.js'
@ -11,7 +13,9 @@ import { openapiOperationDoc } from '@server/middlewares/doc.js'
import { VideoPasswordModel } from '@server/models/video/video-password.js'
import { FilteredModelAttributes } from '@server/types/index.js'
import { MVideoFullLight, MVideoThumbnail } from '@server/types/models/index.js'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
import express, { UploadFiles } from 'express'
import { Transaction } from 'sequelize'
import { VideoAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js'
import { resetSequelizeInstance } from '../../../helpers/database-utils.js'
import { createReqFiles } from '../../../helpers/express-utils.js'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
@ -22,9 +26,6 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist.js'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares/index.js'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update.js'
import { VideoModel } from '../../../models/video/video.js'
import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js'
import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js'
const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
@ -53,7 +54,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON())
const videoInfoToUpdate: VideoUpdate = req.body
const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation()
const hadPrivacyForFederation = isPrivacyForFederation(videoFromReq.privacy)
const oldPrivacy = videoFromReq.privacy
const thumbnails = await buildVideoThumbnailsFromReq(videoFromReq, req.files)
@ -192,7 +193,7 @@ async function updateVideoPrivacy (options: {
transaction: Transaction
}) {
const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
const isNewVideoForFederation = videoInstance.isNewVideoForFederation(videoInfoToUpdate.privacy)
const isNewVideoForFederation = isNewVideoPrivacyForFederation(videoInstance.privacy, videoInfoToUpdate.privacy)
const newPrivacy = forceNumber(videoInfoToUpdate.privacy) as VideoPrivacyType
setVideoPrivacy(videoInstance, newPrivacy)
@ -208,7 +209,7 @@ async function updateVideoPrivacy (options: {
}
// Unfederate the video if the new privacy is not compatible with federation
if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
if (hadPrivacyForFederation && !isPrivacyForFederation(videoInstance.privacy)) {
await VideoModel.sendDelete(videoInstance, { transaction })
}

View File

@ -1,9 +1,9 @@
import { ContextType } from '@peertube/peertube-models'
import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js'
import { isArray } from './custom-validators/misc.js'
import { buildDigest } from './peertube-crypto.js'
import type { signJsonLDObject } from './peertube-jsonld.js'
import { doJSONRequest } from './requests.js'
import { isArray } from './custom-validators/misc.js'
export type ContextFilter = <T> (arg: T) => Promise<T>
@ -49,6 +49,18 @@ export async function getApplicationActorOfHost (host: string) {
return found?.href || undefined
}
export function getAPPublicValue () {
return 'https://www.w3.org/ns/activitystreams#Public'
}
export function hasAPPublic (toOrCC: string[]) {
if (!isArray(toOrCC)) return false
const publicValue = getAPPublicValue()
return toOrCC.some(f => f === 'as:Public' || publicValue)
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
@ -58,7 +70,6 @@ type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string
const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = {
Video: buildContext({
Hashtag: 'as:Hashtag',
uuid: 'sc:identifier',
category: 'sc:category',
licence: 'sc:license',
subtitleLanguage: 'sc:subtitleLanguage',
@ -99,6 +110,11 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
'@id': 'pt:aspectRatio'
},
uuid: {
'@type': 'sc:identifier',
'@id': 'pt:uuid'
},
originallyPublishedAt: 'sc:datePublished',
uploadDate: 'sc:uploadDate',
@ -170,12 +186,23 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
'@type': 'sc:Number',
'@id': 'pt:stopTimestamp'
},
uuid: 'sc:identifier'
uuid: {
'@type': 'sc:identifier',
'@id': 'pt:uuid'
}
}),
CacheFile: buildContext({
expires: 'sc:expires',
CacheFile: 'pt:CacheFile'
CacheFile: 'pt:CacheFile',
size: {
'@type': 'sc:Number',
'@id': 'pt:size'
},
fps: {
'@type': 'sc:Number',
'@id': 'pt:fps'
}
}),
Flag: buildContext({
@ -205,15 +232,21 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
'@type': 'sc:Number',
'@id': 'pt:startTimestamp'
},
stopTimestamp: {
endTimestamp: {
'@type': 'sc:Number',
'@id': 'pt:stopTimestamp'
'@id': 'pt:endTimestamp'
},
watchSection: {
'@type': 'sc:Number',
'@id': 'pt:stopTimestamp'
uuid: {
'@type': 'sc:identifier',
'@id': 'pt:uuid'
},
uuid: 'sc:identifier'
actionStatus: 'sc:actionStatus',
watchSections: {
'@type': '@id',
'@id': 'pt:watchSections'
},
addressRegion: 'sc:addressRegion',
addressCountry: 'sc:addressCountry'
}),
View: buildContext({
@ -233,13 +266,46 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
Rate: buildContext(),
Chapters: buildContext({
name: 'sc:name',
hasPart: 'sc:hasPart',
endOffset: 'sc:endOffset',
startOffset: 'sc:startOffset'
})
}
let allContext: (string | ContextValue)[]
export function getAllContext () {
if (allContext) return allContext
const processed = new Set<string>()
allContext = []
let staticContext: ContextValue = {}
for (const v of Object.values(contextStore)) {
for (const item of v) {
if (typeof item === 'string') {
if (!processed.has(item)) {
allContext.push(item)
}
processed.add(item)
} else {
for (const subKey of Object.keys(item)) {
if (!processed.has(subKey)) {
staticContext = { ...staticContext, [subKey]: item[subKey] }
}
processed.add(subKey)
}
}
}
}
allContext = [ ...allContext, staticContext ]
return allContext
}
async function getContextData (type: ContextType, contextFilter: ContextFilter) {
const contextData = contextFilter
? await contextFilter(contextStore[type])

View File

@ -1,6 +1,6 @@
import jsonld from 'jsonld'
const CACHE = {
const STATIC_CACHE = {
'https://w3id.org/security/v1': {
'@context': {
id: '@id',
@ -53,19 +53,29 @@ const CACHE = {
}
}
const localCache = new Map<string, any>()
const nodeDocumentLoader = (jsonld as any).documentLoaders.node();
/* eslint-disable no-import-assign */
(jsonld as any).documentLoader = (url) => {
if (url in CACHE) {
return Promise.resolve({
(jsonld as any).documentLoader = async (url: string) => {
if (url in STATIC_CACHE) {
return {
contextUrl: null,
document: CACHE[url],
document: STATIC_CACHE[url],
documentUrl: url
})
}
}
return nodeDocumentLoader(url)
if (localCache.has(url)) return localCache.get(url)
const remoteDoc = await nodeDocumentLoader(url)
if (localCache.size < 100) {
localCache.set(url, remoteDoc)
}
return remoteDoc
}
export { jsonld }

View File

@ -1,20 +1,15 @@
import { CacheFileObject } from '@peertube/peertube-models'
import { exists, isDateValid } from '../misc.js'
import { MIMETYPES } from '@server/initializers/constants.js'
import validator from 'validator'
import { isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
import { isRemoteVideoUrlValid } from './videos.js'
function isCacheFileObjectValid (object: CacheFileObject) {
return exists(object) &&
object.type === 'CacheFile' &&
(object.expires === null || isDateValid(object.expires)) &&
export function isCacheFileObjectValid (object: CacheFileObject) {
if (!object || object.type !== 'CacheFile') return false
return (!object.expires || isDateValid(object.expires)) &&
isActivityPubUrlValid(object.object) &&
(isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
export {
isCacheFileObjectValid
(isRedundancyUrlVideoValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
@ -24,3 +19,15 @@ function isPlaylistRedundancyUrlValid (url: any) {
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href)
}
// TODO: compat with < 6.1, use isRemoteVideoUrlValid instead in 7.0
function isRedundancyUrlVideoValid (url: any) {
const size = url.size || url['_:size']
const fps = url.fps || url['_fps']
return MIMETYPES.AP_VIDEO.MIMETYPE_EXT[url.mediaType] &&
isActivityPubUrlValid(url.href) &&
validator.default.isInt(url.height + '', { min: 0 }) &&
validator.default.isInt(size + '', { min: 0 }) &&
(!fps || validator.default.isInt(fps + '', { min: -1 }))
}

View File

@ -1,29 +1,25 @@
import validator from 'validator'
import { PlaylistElementObject, PlaylistObject } from '@peertube/peertube-models'
import validator from 'validator'
import { exists, isDateValid, isUUIDValid } from '../misc.js'
import { isVideoPlaylistNameValid } from '../video-playlists.js'
import { isActivityPubUrlValid } from './misc.js'
function isPlaylistObjectValid (object: PlaylistObject) {
return exists(object) &&
object.type === 'Playlist' &&
validator.default.isInt(object.totalItems + '') &&
export function isPlaylistObjectValid (object: PlaylistObject) {
if (!object || object.type !== 'Playlist') return false
// TODO: compat with < 6.1, remove in 7.0
if (!object.uuid && object['identifier']) object.uuid = object['identifier']
return validator.default.isInt(object.totalItems + '') &&
isVideoPlaylistNameValid(object.name) &&
isUUIDValid(object.uuid) &&
isDateValid(object.published) &&
isDateValid(object.updated)
}
function isPlaylistElementObjectValid (object: PlaylistElementObject) {
export function isPlaylistElementObjectValid (object: PlaylistElementObject) {
return exists(object) &&
object.type === 'PlaylistElement' &&
validator.default.isInt(object.position + '') &&
isActivityPubUrlValid(object.url)
}
// ---------------------------------------------------------------------------
export {
isPlaylistObjectValid,
isPlaylistElementObjectValid
}

View File

@ -1,5 +1,5 @@
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
import validator from 'validator'
import { ACTIVITY_PUB } from '../../../initializers/constants.js'
import { exists, isArray, isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js'
@ -23,10 +23,7 @@ function sanitizeAndCheckVideoCommentObject (comment: any) {
isDateValid(comment.published) &&
isActivityPubUrlValid(comment.url) &&
isArray(comment.to) &&
(
comment.to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ||
comment.cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1
) // Only accept public comments
(hasAPPublic(comment.to) || hasAPPublic(comment.cc)) // Only accept public comments
}
// ---------------------------------------------------------------------------

View File

@ -27,7 +27,7 @@ function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
sanitizeAndCheckVideoTorrentObject(activity.object)
}
function sanitizeAndCheckVideoTorrentObject (video: any) {
function sanitizeAndCheckVideoTorrentObject (video: VideoObject) {
if (!video || video.type !== 'Video') return false
if (!setValidRemoteTags(video)) {
@ -59,6 +59,9 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
return false
}
// TODO: compat with < 6.1, remove in 7.0
if (!video.uuid && video['identifier']) video.uuid = video['identifier']
// Default attributes
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false

View File

@ -1,19 +1,26 @@
import { arrayify } from '@peertube/peertube-core-utils'
import { WatchActionObject } from '@peertube/peertube-models'
import { exists, isDateValid, isUUIDValid } from '../misc.js'
import { isDateValid, isUUIDValid } from '../misc.js'
import { isVideoTimeValid } from '../video-view.js'
import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js'
function isWatchActionObjectValid (action: WatchActionObject) {
return exists(action) &&
action.type === 'WatchAction' &&
isObjectValid(action.id) &&
if (!action || action.type !== 'WatchAction') return false
// TODO: compat with < 6.1, remove in 7.0
if (!action.uuid && action['identifier']) action.uuid = action['identifier']
if (action['_:actionStatus'] && !action.actionStatus) action.actionStatus = action['_:actionStatus']
if (action['_:watchSections'] && !action.watchSections) action.watchSections = arrayify(action['_:watchSections'])
return isObjectValid(action.id) &&
isActivityPubVideoDurationValid(action.duration) &&
isDateValid(action.startTime) &&
isDateValid(action.endTime) &&
isLocationValid(action.location) &&
isUUIDValid(action.uuid) &&
isObjectValid(action.object) &&
isWatchSectionsValid(action.watchSections)
areWatchSectionsValid(action.watchSections)
}
// ---------------------------------------------------------------------------
@ -34,8 +41,11 @@ function isLocationValid (location: any) {
return true
}
function isWatchSectionsValid (sections: WatchActionObject['watchSections']) {
function areWatchSectionsValid (sections: WatchActionObject['watchSections']) {
return Array.isArray(sections) && sections.every(s => {
// TODO: compat with < 6.1, remove in 7.0
if (s['_:endTimestamp'] && !s.endTimestamp) s.endTimestamp = s['_:endTimestamp']
return isVideoTimeValid(s.startTimestamp) && isVideoTimeValid(s.endTimestamp)
})
}

View File

@ -70,7 +70,7 @@ export function areVideoTagsValid (tags: string[]) {
)
}
export function isVideoViewsValid (value: string) {
export function isVideoViewsValid (value: string | number) {
return exists(value) && validator.default.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
}

View File

@ -1,26 +1,51 @@
import { omit } from '@peertube/peertube-core-utils'
import { sha256 } from '@peertube/peertube-node-utils'
import { createSign, createVerify } from 'crypto'
import cloneDeep from 'lodash-es/cloneDeep.js'
import { MActor } from '../types/models/index.js'
import { getAllContext } from './activity-pub-utils.js'
import { jsonld } from './custom-jsonld-signature.js'
import { isArray } from './custom-validators/misc.js'
import { logger } from './logger.js'
import { assertIsInWorkerThread } from './threads.js'
import { jsonld } from './custom-jsonld-signature.js'
export function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
if (signedDocument.signature.type === 'RsaSignature2017') {
return isJsonLDRSA2017Verified(fromActor, signedDocument)
type ExpressRequest = { body: any }
export function compactJSONLDAndCheckSignature (fromActor: MActor, req: ExpressRequest): Promise<boolean> {
if (req.body.signature.type === 'RsaSignature2017') {
return compactJSONLDAndCheckRSA2017Signature(fromActor, req)
}
logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument)
logger.warn('Unknown JSON LD signature %s.', req.body.signature.type, req.body)
return Promise.resolve(false)
}
// Backward compatibility with "other" implementations
export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) {
export async function compactJSONLDAndCheckRSA2017Signature (fromActor: MActor, req: ExpressRequest) {
const compacted = await jsonldCompact(omit(req.body, [ 'signature' ]))
fixCompacted(req.body, compacted)
req.body = { ...compacted, signature: req.body.signature }
if (compacted['@include']) {
logger.warn('JSON-LD @include is not supported')
return false
}
// TODO: compat with < 6.1, remove in 7.0
let safe = true
if (
(compacted.type === 'Create' && (compacted?.object?.type === 'WatchAction' || compacted?.object?.type === 'CacheFile')) ||
(compacted.type === 'Undo' && compacted?.object?.type === 'Create' && compacted?.object?.object.type === 'CacheFile')
) {
safe = false
}
const [ documentHash, optionsHash ] = await Promise.all([
createDocWithoutSignatureHash(signedDocument),
createSignatureHash(signedDocument.signature)
hashObject(compacted, safe),
createSignatureHash(req.body.signature, safe)
])
const toVerify = optionsHash + documentHash
@ -28,7 +53,39 @@ export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument
const verify = createVerify('RSA-SHA256')
verify.update(toVerify, 'utf8')
return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
return verify.verify(fromActor.publicKey, req.body.signature.signatureValue, 'base64')
}
function fixCompacted (original: any, compacted: any) {
if (!original || !compacted) return
for (const [ k, v ] of Object.entries(original)) {
if (k === '@context' || k === 'signature') continue
if (v === undefined || v === null) continue
const cv = compacted[k]
if (cv === undefined || cv === null) continue
if (typeof v === 'string') {
if (v === 'https://www.w3.org/ns/activitystreams#Public' && cv === 'as:Public') {
compacted[k] = v
}
}
if (isArray(v) && !isArray(cv)) {
compacted[k] = [ cv ]
for (let i = 0; i < v.length; i++) {
if (v[i] === 'https://www.w3.org/ns/activitystreams#Public' && cv[i] === 'as:Public') {
compacted[k][i] = v[i]
}
}
}
if (typeof v === 'object') {
fixCompacted(original[k], compacted[k])
}
}
}
export async function signJsonLDObject <T> (options: {
@ -66,35 +123,40 @@ export async function signJsonLDObject <T> (options: {
// Private
// ---------------------------------------------------------------------------
async function hashObject (obj: any): Promise<any> {
const res = await (jsonld as any).promises.normalize(obj, {
safe: false,
algorithm: 'URDNA2015',
format: 'application/n-quads'
})
async function hashObject (obj: any, safe: boolean): Promise<any> {
const res = await jsonldNormalize(obj, safe)
return sha256(res)
}
function createSignatureHash (signature: any) {
const signatureCopy = cloneDeep(signature)
Object.assign(signatureCopy, {
function jsonldCompact (obj: any) {
return (jsonld as any).promises.compact(obj, getAllContext())
}
function jsonldNormalize (obj: any, safe: boolean) {
return (jsonld as any).promises.normalize(obj, {
safe,
algorithm: 'URDNA2015',
format: 'application/n-quads'
})
}
// ---------------------------------------------------------------------------
function createSignatureHash (signature: any, safe = true) {
return hashObject({
'@context': [
'https://w3id.org/security/v1',
{ RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
]
})
],
delete signatureCopy.type
delete signatureCopy.id
delete signatureCopy.signatureValue
return hashObject(signatureCopy)
...omit(signature, [ 'type', 'id', 'signatureValue' ])
}, safe)
}
function createDocWithoutSignatureHash (doc: any) {
const docWithoutSignature = cloneDeep(doc)
delete docWithoutSignature.signature
return hashObject(docWithoutSignature)
return hashObject(docWithoutSignature, true)
}

View File

@ -1,51 +1,28 @@
import { Response } from 'express'
import { forceNumber } from '@peertube/peertube-core-utils'
import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models'
import { VideoPrivacy } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models/index.js'
import { Response } from 'express'
function getVideoWithAttributes (res: Response) {
export function getVideoWithAttributes (res: Response) {
return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo
}
function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
export function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
return isStreamingPlaylist(videoOrPlaylist)
? videoOrPlaylist.Video
: videoOrPlaylist
}
function isPrivacyForFederation (privacy: VideoPrivacyType) {
const castedPrivacy = forceNumber(privacy)
return castedPrivacy === VideoPrivacy.PUBLIC ||
(CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED)
}
function isStateForFederation (state: VideoStateType) {
const castedState = forceNumber(state)
return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED
}
function getPrivaciesForFederation () {
export function getPrivaciesForFederation () {
return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true)
? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ]
: [ { privacy: VideoPrivacy.PUBLIC } ]
}
function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) {
export function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) {
const value = mimeTypes[mimeType]
if (Array.isArray(value)) return value[0]
return value
}
export {
getVideoWithAttributes,
extractVideo,
getExtFromMimetype,
isStateForFederation,
isPrivacyForFederation,
getPrivaciesForFederation
}

View File

@ -45,7 +45,7 @@ import { cpus } from 'os'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 830
const LAST_MIGRATION_VERSION = 835
// ---------------------------------------------------------------------------
@ -775,7 +775,6 @@ const ACTIVITY_PUB = {
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
],
ACCEPT_HEADER: 'application/activity+json, application/ld+json',
PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
COLLECTION_ITEMS_PER_PAGE: 10,
FETCH_PAGE_LIMIT: 2000,
MAX_RECURSION_COMMENTS: 100,

View File

@ -0,0 +1,24 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
const { transaction } = utils
{
await utils.queryInterface.changeColumn('videoSource', 'size', {
type: Sequelize.BIGINT,
allowNull: true
}, { transaction })
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
down, up
}

View File

@ -1,17 +1,17 @@
import { ActivityAudience } from '@peertube/peertube-models'
import { ACTIVITY_PUB } from '../../initializers/constants.js'
import { getAPPublicValue } from '@server/helpers/activity-pub-utils.js'
import { MActorFollowersUrl } from '../../types/models/index.js'
function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
export function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
return buildAudience([ actorSender.followersUrl ], isPublic)
}
function buildAudience (followerUrls: string[], isPublic = true) {
export function buildAudience (followerUrls: string[], isPublic = true) {
let to: string[] = []
let cc: string[] = []
if (isPublic) {
to = [ ACTIVITY_PUB.PUBLIC ]
to = [ getAPPublicValue() ]
cc = followerUrls
} else { // Unlisted
to = []
@ -21,14 +21,6 @@ function buildAudience (followerUrls: string[], isPublic = true) {
return { to, cc }
}
function audiencify<T> (object: T, audience: ActivityAudience) {
export function audiencify<T> (object: T, audience: ActivityAudience) {
return { ...audience, ...object }
}
// ---------------------------------------------------------------------------
export {
buildAudience,
getAudience,
audiencify
}

View File

@ -2,6 +2,7 @@ import { Transaction } from 'sequelize'
import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models/index.js'
import { CacheFileObject, VideoStreamingPlaylistType } from '@peertube/peertube-models'
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy.js'
import { exists } from '@server/helpers/custom-validators/misc.js'
async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
@ -65,11 +66,15 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
}
const url = cacheFileObject.url
const urlFPS = exists(url.fps) // TODO: compat with < 6.1, remove in 7.0
? url.fps
: url['_:fps']
const videoFile = video.VideoFiles.find(f => {
return f.resolution === url.height && f.fps === url.fps
return f.resolution === url.height && f.fps === urlFPS
})
if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`)
if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${urlFPS} of video ${video.url}`)
return {
expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,

View File

@ -6,7 +6,7 @@ import { Activity } from '@peertube/peertube-models'
import { StatsManager } from '../stat-manager.js'
import { processActivities } from './process/index.js'
class InboxManager {
export class InboxManager {
private static instance: InboxManager
private readonly inboxQueue: PQueue
@ -39,9 +39,3 @@ class InboxManager {
return this.instance || (this.instance = new this())
}
}
// ---------------------------------------------------------------------------
export {
InboxManager
}

View File

@ -145,7 +145,7 @@ async function buildElementsDBAttributes (elementUrls: string[], playlist: MVide
try {
const { elementObject } = await fetchRemotePlaylistElement(elementUrl)
const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video' })
const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video-and-blacklist' })
elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video))
} catch (err) {

View File

@ -1,12 +1,12 @@
import { ACTIVITY_PUB } from '@server/initializers/constants.js'
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
import { MVideoId, MVideoPlaylistId } from '@server/types/models/index.js'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { PlaylistElementObject, PlaylistObject, VideoPlaylistPrivacy } from '@peertube/peertube-models'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
import { MVideoId, MVideoPlaylistId } from '@server/types/models/index.js'
function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) {
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
export function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) {
const privacy = hasAPPublic(to)
? VideoPlaylistPrivacy.PUBLIC
: VideoPlaylistPrivacy.UNLISTED
@ -23,7 +23,11 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: strin
} as AttributesOnly<VideoPlaylistModel>
}
function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) {
export function playlistElementObjectToDBAttributes (
elementObject: PlaylistElementObject,
videoPlaylist: MVideoPlaylistId,
video: MVideoId
) {
return {
position: elementObject.position,
url: elementObject.id,
@ -33,8 +37,3 @@ function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObje
videoId: video.id
} as AttributesOnly<VideoPlaylistElementModel>
}
export {
playlistObjectToDBAttributes,
playlistElementObjectToDBAttributes
}

View File

@ -24,7 +24,7 @@ import { createOrUpdateLocalVideoViewer } from '../local-video-viewer.js'
import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
import { resolveThread } from '../video-comments.js'
import { getOrCreateAPVideo } from '../videos/index.js'
import { canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js'
async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) {
const { activity, byActor } = options
@ -87,6 +87,11 @@ async function processCreateCacheFile (
const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
if (video.isOwned() && !canVideoBeFederated(video)) {
logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`)
return
}
await sequelizeTypescript.transaction(async t => {
return createOrUpdateCacheFile(cacheFile, video, byActor, t)
})

View File

@ -1,11 +1,12 @@
import { VideoModel } from '@server/models/video/video.js'
import { ActivityDislike } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { VideoModel } from '@server/models/video/video.js'
import { retryTransactionWrapper } from '../../../helpers/database-utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
import { MActorSignature } from '../../../types/models/index.js'
import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) {
const { activity, byActor } = options
@ -21,14 +22,19 @@ export {
// ---------------------------------------------------------------------------
async function processDislike (activity: ActivityDislike, byActor: MActorSignature) {
const dislikeObject = activity.object
const videoUrl = activity.object
const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' })
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return
if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`)
return
}
return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadFull(onlyVideo.id, t)

View File

@ -1,4 +1,5 @@
import { ActivityLike } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { VideoModel } from '@server/models/video/video.js'
import { retryTransactionWrapper } from '../../../helpers/database-utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
@ -6,7 +7,7 @@ import { getAPId } from '../../../lib/activitypub/activity.js'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
import { MActorSignature } from '../../../types/models/index.js'
import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
const { activity, byActor } = options
@ -28,9 +29,14 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' })
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return
if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`)
return
}
return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadFull(onlyVideo.id, t)

View File

@ -20,7 +20,7 @@ import { APActorUpdater } from '../actors/updater.js'
import { createOrUpdateCacheFile } from '../cache-file.js'
import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
import { APVideoUpdater, getOrCreateAPVideo } from '../videos/index.js'
import { APVideoUpdater, canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js'
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
const { activity, byActor } = options
@ -93,6 +93,11 @@ async function processUpdateCacheFile (
const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
if (video.isOwned() && !canVideoBeFederated(video)) {
logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`)
return
}
await sequelizeTypescript.transaction(async t => {
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
})

View File

@ -24,7 +24,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
const { video } = await getOrCreateAPVideo({
videoObject,
fetchType: 'only-video',
fetchType: 'only-video-and-blacklist',
allowRefresh: false
})
@ -32,8 +32,8 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
video,
viewerId: activity.id,
viewerExpires: activity.expires
? new Date(activity.expires)
viewerExpires: getExpires(activity)
? new Date(getExpires(activity))
: undefined,
viewerResultCounter: getViewerResultCounter(activity)
})
@ -49,10 +49,15 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
function getViewerResultCounter (activity: ActivityView) {
const result = activity.result
if (!activity.expires || result?.interactionType !== 'WatchAction' || result?.type !== 'InteractionCounter') return undefined
if (!getExpires(activity) || result?.interactionType !== 'WatchAction' || result?.type !== 'InteractionCounter') return undefined
const counter = parseInt(result.userInteractionCount + '')
if (isNaN(counter)) return undefined
return counter
}
// TODO: compat with < 6.1, remove in 7.0
function getExpires (activity: ActivityView) {
return activity.expires || activity['expiration'] as string
}

View File

@ -34,7 +34,7 @@ const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Act
View: processViewActivity
}
async function processActivities (
export async function processActivities (
activities: Activity[],
options: {
signatureActor?: MActorSignature
@ -86,7 +86,3 @@ async function processActivities (
}
}
}
export {
processActivities
}

View File

@ -1,5 +1,3 @@
import { Transaction } from 'sequelize'
import { getServerActor } from '@server/models/application/application.js'
import {
ActivityAudience,
ActivityCreate,
@ -9,19 +7,24 @@ import {
VideoPlaylistPrivacy,
VideoPrivacy
} from '@peertube/peertube-models'
import { AccountModel } from '@server/models/account/account.js'
import { getServerActor } from '@server/models/application/application.js'
import { VideoModel } from '@server/models/video/video.js'
import { Transaction } from 'sequelize'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { VideoCommentModel } from '../../../models/video/video-comment.js'
import {
MActorLight,
MCommentOwnerVideo,
MLocalVideoViewerWithWatchSections,
MVideoAccountLight,
MVideoAP,
MVideoAccountLight,
MVideoPlaylistFull,
MVideoRedundancyFileVideo,
MVideoRedundancyStreamingPlaylistVideo
} from '../../../types/models/index.js'
import { audiencify, getAudience } from '../audience.js'
import { canVideoBeFederated } from '../videos/federate.js'
import {
broadcastToActors,
broadcastToFollowers,
@ -32,12 +35,11 @@ import {
sendVideoRelatedActivity,
unicastTo
} from './shared/index.js'
import { AccountModel } from '@server/models/account/account.js'
const lTags = loggerTagsFactory('ap', 'create')
async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
if (!video.hasPrivacyForFederation()) return undefined
export async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
if (!canVideoBeFederated(video)) return undefined
logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
@ -56,7 +58,7 @@ async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
})
}
async function sendCreateCacheFile (
export async function sendCreateCacheFile (
byActor: MActorLight,
video: MVideoAccountLight,
fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
@ -72,7 +74,7 @@ async function sendCreateCacheFile (
})
}
async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) {
export async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) {
logger.info('Creating job to send create watch action %s.', stats.url, lTags(stats.uuid))
const byActor = await getServerActor()
@ -84,7 +86,7 @@ async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections,
return sendVideoActivityToOrigin(activityBuilder, { byActor, video: stats.Video, transaction, contextType: 'WatchAction' })
}
async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) {
export async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) {
if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
@ -109,11 +111,20 @@ async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transactio
})
}
async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) {
logger.info('Creating job to send comment %s.', comment.url)
export async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) {
const isOrigin = comment.Video.isOwned()
if (isOrigin) {
const videoWithBlacklist = await VideoModel.loadWithBlacklist(comment.Video.id)
if (!canVideoBeFederated(videoWithBlacklist)) {
logger.debug(`Do not send comment ${comment.url} on a video that cannot be federated`)
return undefined
}
}
logger.info('Creating job to send comment %s.', comment.url)
const byActor = comment.Account.Actor
const videoAccount = await AccountModel.load(comment.Video.VideoChannel.Account.id, transaction)
@ -179,7 +190,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction:
})
}
function buildCreateActivity <T extends ActivityCreateObject> (
export function buildCreateActivity <T extends ActivityCreateObject> (
url: string,
byActor: MActorLight,
object: T,
@ -201,16 +212,7 @@ function buildCreateActivity <T extends ActivityCreateObject> (
}
// ---------------------------------------------------------------------------
export {
sendCreateVideo,
buildCreateActivity,
sendCreateVideoComment,
sendCreateVideoPlaylist,
sendCreateCacheFile,
sendCreateWatchAction
}
// Private
// ---------------------------------------------------------------------------
async function sendVideoRelatedCreateActivity (options: {

View File

@ -1,10 +1,10 @@
import { Transaction } from 'sequelize'
import { getServerActor } from '@server/models/application/application.js'
import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
import { getServerActor } from '@server/models/application/application.js'
import { Transaction } from 'sequelize'
import { logger } from '../../../helpers/logger.js'
import { AccountModel } from '../../../models/account/account.js'
import { VideoModel } from '../../../models/video/video.js'
import { VideoShareModel } from '../../../models/video/video-share.js'
import { VideoModel } from '../../../models/video/video.js'
import {
MAccountDefault,
MActor,
@ -16,11 +16,12 @@ import {
} from '../../../types/models/index.js'
import { audiencify, getAudience } from '../audience.js'
import { getUpdateActivityPubUrl } from '../url.js'
import { canVideoBeFederated } from '../videos/federate.js'
import { getActorsInvolvedInVideo } from './shared/index.js'
import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils.js'
async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) {
if (!videoArg.hasPrivacyForFederation()) return undefined
export async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) {
if (!canVideoBeFederated(videoArg)) return undefined
const video = await videoArg.lightAPToFullAP(transaction)
@ -47,7 +48,7 @@ async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transactio
})
}
async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) {
export async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) {
const byActor = accountOrChannel.Actor
logger.info('Creating job to update actor %s.', byActor.url)
@ -77,7 +78,7 @@ async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefa
})
}
async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) {
export async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) {
logger.info('Creating job to update cache file %s.', redundancyModel.url)
const associatedVideo = redundancyModel.getVideo()
@ -98,7 +99,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide
return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' })
}
async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) {
export async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) {
if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
const byActor = videoPlaylist.OwnerAccount.Actor
@ -127,14 +128,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, trans
}
// ---------------------------------------------------------------------------
export {
sendUpdateActor,
sendUpdateVideo,
sendUpdateCacheFile,
sendUpdateVideoPlaylist
}
// Private
// ---------------------------------------------------------------------------
function buildUpdateActivity (

View File

@ -1,25 +1,25 @@
import { Transaction } from 'sequelize'
import { ACTIVITY_PUB } from '@server/initializers/constants.js'
import { ActorModel } from '@server/models/actor/actor.js'
import { VideoModel } from '@server/models/video/video.js'
import { VideoShareModel } from '@server/models/video/video-share.js'
import { MActorFollowersUrl, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models/index.js'
import { ActivityAudience } from '@peertube/peertube-models'
import { getAPPublicValue } from '@server/helpers/activity-pub-utils.js'
import { ActorModel } from '@server/models/actor/actor.js'
import { VideoShareModel } from '@server/models/video/video-share.js'
import { VideoModel } from '@server/models/video/video.js'
import { MActorFollowersUrl, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models/index.js'
import { Transaction } from 'sequelize'
function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
export function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
return {
to: [ accountActor.url ],
cc: actorsInvolvedInVideo.map(a => a.followersUrl)
}
}
function getVideoCommentAudience (
export function getVideoCommentAudience (
videoComment: MCommentOwnerVideo,
threadParentComments: MCommentOwner[],
actorsInvolvedInVideo: MActorFollowersUrl[],
isOrigin = false
): ActivityAudience {
const to = [ ACTIVITY_PUB.PUBLIC ]
const to = [ getAPPublicValue() ]
const cc: string[] = []
// Owner of the video we comment
@ -43,14 +43,14 @@ function getVideoCommentAudience (
}
}
function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
export function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
return {
to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
to: [ getAPPublicValue() ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
cc: []
}
}
async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
export async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
const actors = await VideoShareModel.listActorIdsAndFollowerUrlsByShare(video.id, t)
const alreadyLoadedActor = (video as VideoModel).VideoChannel?.Account?.Actor
@ -63,12 +63,3 @@ async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
return actors
}
// ---------------------------------------------------------------------------
export {
getOriginVideoAudience,
getActorsInvolvedInVideo,
getAudienceFromFollowersOf,
getVideoCommentAudience
}

View File

@ -258,7 +258,6 @@ function unicastTo (options: {
export {
broadcastToFollowers,
unicastTo,
forwardActivity,
broadcastToActors,
sendVideoActivityToOrigin,
forwardVideoRelatedActivity,

View File

@ -1,6 +1,6 @@
import { getServerActor } from '@server/models/application/application.js'
import Bluebird from 'bluebird'
import { Transaction } from 'sequelize'
import { getServerActor } from '@server/models/application/application.js'
import { logger, loggerTagsFactory } from '../../helpers/logger.js'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants.js'
import { VideoShareModel } from '../../models/video/video-share.js'
@ -12,16 +12,7 @@ import { checkUrlsSameHost, getLocalVideoAnnounceActivityPubUrl } from './url.js
const lTags = loggerTagsFactory('share')
async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
if (!video.hasPrivacyForFederation()) return undefined
return Promise.all([
shareByServer(video, t),
shareByVideoChannel(video, t)
])
}
async function changeVideoChannelShare (
export async function changeVideoChannelShare (
video: MVideoAccountLight,
oldVideoChannel: MChannelActorLight,
t: Transaction
@ -36,7 +27,7 @@ async function changeVideoChannelShare (
await shareByVideoChannel(video, t)
}
async function addVideoShares (shareUrls: string[], video: MVideoId) {
export async function addVideoShares (shareUrls: string[], video: MVideoId) {
await Bluebird.map(shareUrls, async shareUrl => {
try {
await addVideoShare(shareUrl, video)
@ -46,12 +37,44 @@ async function addVideoShares (shareUrls: string[], video: MVideoId) {
}, { concurrency: CRAWL_REQUEST_CONCURRENCY })
}
export {
changeVideoChannelShare,
addVideoShares,
shareVideoByServerAndChannel
export async function shareByServer (video: MVideo, t: Transaction) {
const serverActor = await getServerActor()
const serverShareUrl = getLocalVideoAnnounceActivityPubUrl(serverActor, video)
const [ serverShare ] = await VideoShareModel.findOrCreate({
defaults: {
actorId: serverActor.id,
videoId: video.id,
url: serverShareUrl
},
where: {
url: serverShareUrl
},
transaction: t
})
return sendVideoAnnounce(serverActor, serverShare, video, t)
}
export async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) {
const videoChannelShareUrl = getLocalVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
defaults: {
actorId: video.VideoChannel.actorId,
videoId: video.id,
url: videoChannelShareUrl
},
where: {
url: videoChannelShareUrl
},
transaction: t
})
return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
}
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
async function addVideoShare (shareUrl: string, video: MVideoId) {
@ -74,42 +97,6 @@ async function addVideoShare (shareUrl: string, video: MVideoId) {
await VideoShareModel.upsert(entry)
}
async function shareByServer (video: MVideo, t: Transaction) {
const serverActor = await getServerActor()
const serverShareUrl = getLocalVideoAnnounceActivityPubUrl(serverActor, video)
const [ serverShare ] = await VideoShareModel.findOrCreate({
defaults: {
actorId: serverActor.id,
videoId: video.id,
url: serverShareUrl
},
where: {
url: serverShareUrl
},
transaction: t
})
return sendVideoAnnounce(serverActor, serverShare, video, t)
}
async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) {
const videoChannelShareUrl = getLocalVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
defaults: {
actorId: video.VideoChannel.actorId,
videoId: video.id,
url: videoChannelShareUrl
},
where: {
url: videoChannelShareUrl
},
transaction: t
})
return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
}
async function undoShareByVideoChannel (video: MVideo, oldVideoChannel: MChannelActorLight, t: Transaction) {
// Load old share
const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t)

View File

@ -9,7 +9,7 @@ import { Hooks } from '../plugins/hooks.js'
import { fetchAP } from './activity.js'
import { getOrCreateAPActor } from './actors/index.js'
import { checkUrlsSameHost } from './url.js'
import { getOrCreateAPVideo } from './videos/index.js'
import { canVideoBeFederated, getOrCreateAPVideo } from './videos/index.js'
type ResolveThreadParams = {
url: string
@ -92,8 +92,8 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false }
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
if (video.isOwned() && !video.hasPrivacyForFederation()) {
throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation')
if (video.isOwned() && !canVideoBeFederated(video)) {
throw new Error('Cannot resolve thread of video that is not compatible with federation')
}
let resultComment: MCommentOwnerVideo

View File

@ -1,29 +1,53 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js'
import { MVideoAPLight, MVideoWithBlacklistRights } from '@server/types/models/index.js'
import { Transaction } from 'sequelize'
import { MVideoAP, MVideoAPLight } from '@server/types/models/index.js'
import { sendCreateVideo, sendUpdateVideo } from '../send/index.js'
import { shareVideoByServerAndChannel } from '../share.js'
import { shareByServer, shareByVideoChannel } from '../share.js'
async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) {
const video = videoArg as MVideoAP
export async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) {
if (!canVideoBeFederated(videoArg, isNewVideo)) return
if (
// Check this is not a blacklisted video, or unfederated blacklisted video
(video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
// Check the video is public/unlisted and published
video.hasPrivacyForFederation() && video.hasStateForFederation()
) {
const video = await videoArg.lightAPToFullAP(transaction)
const video = await videoArg.lightAPToFullAP(transaction)
if (isNewVideo) {
// Now we'll add the video's meta data to our followers
await sendCreateVideo(video, transaction)
await shareVideoByServerAndChannel(video, transaction)
} else {
await sendUpdateVideo(video, transaction)
}
if (isNewVideo) {
// Now we'll add the video's meta data to our followers
await sendCreateVideo(video, transaction)
await Promise.all([
shareByServer(video, transaction),
shareByVideoChannel(video, transaction)
])
} else {
await sendUpdateVideo(video, transaction)
}
}
export {
federateVideoIfNeeded
export function canVideoBeFederated (video: MVideoWithBlacklistRights, isNewVideo = false) {
// Check this is not a blacklisted video
if (video.isBlacklisted() === true) {
if (isNewVideo === false) return false
if (video.VideoBlacklist.unfederated === true) return false
}
// Check the video is public/unlisted and published
return isPrivacyForFederation(video.privacy) && isStateForFederation(video.state)
}
export function isNewVideoPrivacyForFederation (currentPrivacy: VideoPrivacyType, newPrivacy: VideoPrivacyType) {
return !isPrivacyForFederation(currentPrivacy) && isPrivacyForFederation(newPrivacy)
}
export function isPrivacyForFederation (privacy: VideoPrivacyType) {
const castedPrivacy = forceNumber(privacy)
return castedPrivacy === VideoPrivacy.PUBLIC ||
(CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED)
}
export function isStateForFederation (state: VideoStateType) {
const castedState = forceNumber(state)
return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED
}

View File

@ -1,9 +1,14 @@
import { APObjectId } from '@peertube/peertube-models'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { logger } from '@server/helpers/logger.js'
import { JobQueue } from '@server/lib/job-queue/index.js'
import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders/index.js'
import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models/index.js'
import { APObjectId } from '@peertube/peertube-models'
import {
MVideoAccountLightBlacklistAllFiles,
MVideoImmutable,
MVideoThumbnail,
MVideoThumbnailBlacklist
} from '@server/types/models/index.js'
import { getAPId } from '../activity.js'
import { refreshVideoIfNeeded } from './refresh.js'
import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared/index.js'
@ -24,23 +29,25 @@ type GetVideoParamAll = {
type GetVideoParamImmutable = {
videoObject: APObjectId
syncParam?: SyncParam
fetchType: 'only-immutable-attributes'
fetchType: 'unsafe-only-immutable-attributes'
allowRefresh: false
}
type GetVideoParamOther = {
videoObject: APObjectId
syncParam?: SyncParam
fetchType?: 'all' | 'only-video'
fetchType?: 'all' | 'only-video-and-blacklist'
allowRefresh?: boolean
}
export function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
export function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
export function getOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
export function getOrCreateAPVideo (
options: GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist>
export async function getOrCreateAPVideo (
options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist | MVideoImmutable> {
// Default params
const syncParam = options.syncParam || { rates: true, shares: true, comments: true, refreshVideo: false }
const fetchType = options.fetchType || 'all'
@ -52,7 +59,7 @@ export async function getOrCreateAPVideo (
if (videoFromDatabase) {
if (allowRefresh === true) {
// Typings ensure allowRefresh === false in only-immutable-attributes fetch type
// Typings ensure allowRefresh === false in unsafe-only-immutable-attributes fetch type
videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam)
}
@ -87,7 +94,9 @@ export async function getOrCreateAPVideo (
export function maybeGetOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
export function maybeGetOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
export function maybeGetOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
export function maybeGetOrCreateAPVideo (
options: GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist>
export async function maybeGetOrCreateAPVideo (options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther) {
try {
const result = await getOrCreateAPVideo(options as any)

View File

@ -11,13 +11,14 @@ import {
VideoPrivacy,
VideoStreamingPlaylistType
} from '@peertube/peertube-models'
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos.js'
import { isArray } from '@server/helpers/custom-validators/misc.js'
import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos.js'
import { generateImageFilename } from '@server/helpers/image-utils.js'
import { logger } from '@server/helpers/logger.js'
import { getExtFromMimetype } from '@server/helpers/video.js'
import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants.js'
import { MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants.js'
import { generateTorrentFileName } from '@server/lib/paths.js'
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
@ -191,7 +192,7 @@ export function getStoryboardAttributeFromObject (video: MVideoId, videoObject:
}
export function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
const privacy = hasAPPublic(to)
? VideoPrivacy.PUBLIC
: VideoPrivacy.UNLISTED

View File

@ -6,57 +6,57 @@ import {
MVideoFullLight,
MVideoId,
MVideoImmutable,
MVideoThumbnail
MVideoThumbnailBlacklist
} from '@server/types/models/index.js'
import { getOrCreateAPVideo } from '../activitypub/videos/get.js'
type VideoLoadType = 'for-api' | 'all' | 'only-video' | 'id' | 'none' | 'only-immutable-attributes'
type VideoLoadType = 'for-api' | 'all' | 'only-video-and-blacklist' | 'id' | 'none' | 'unsafe-only-immutable-attributes'
function loadVideo (id: number | string, fetchType: 'for-api', userId?: number): Promise<MVideoFormattableDetails>
function loadVideo (id: number | string, fetchType: 'all', userId?: number): Promise<MVideoFullLight>
function loadVideo (id: number | string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable>
function loadVideo (id: number | string, fetchType: 'only-video', userId?: number): Promise<MVideoThumbnail>
function loadVideo (id: number | string, fetchType: 'unsafe-only-immutable-attributes'): Promise<MVideoImmutable>
function loadVideo (id: number | string, fetchType: 'only-video-and-blacklist', userId?: number): Promise<MVideoThumbnailBlacklist>
function loadVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Promise<MVideoId>
function loadVideo (
id: number | string,
fetchType: VideoLoadType,
userId?: number
): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable>
): Promise<MVideoFullLight | MVideoThumbnailBlacklist | MVideoId | MVideoImmutable>
function loadVideo (
id: number | string,
fetchType: VideoLoadType,
userId?: number
): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable> {
): Promise<MVideoFullLight | MVideoThumbnailBlacklist | MVideoId | MVideoImmutable> {
if (fetchType === 'for-api') return VideoModel.loadForGetAPI({ id, userId })
if (fetchType === 'all') return VideoModel.loadFull(id, undefined, userId)
if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
if (fetchType === 'unsafe-only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
if (fetchType === 'only-video') return VideoModel.load(id)
if (fetchType === 'only-video-and-blacklist') return VideoModel.loadWithBlacklist(id)
if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
}
type VideoLoadByUrlType = 'all' | 'only-video' | 'only-immutable-attributes'
type VideoLoadByUrlType = 'all' | 'only-video-and-blacklist' | 'unsafe-only-immutable-attributes'
function loadVideoByUrl (url: string, fetchType: 'all'): Promise<MVideoAccountLightBlacklistAllFiles>
function loadVideoByUrl (url: string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable>
function loadVideoByUrl (url: string, fetchType: 'only-video'): Promise<MVideoThumbnail>
function loadVideoByUrl (url: string, fetchType: 'unsafe-only-immutable-attributes'): Promise<MVideoImmutable>
function loadVideoByUrl (url: string, fetchType: 'only-video-and-blacklist'): Promise<MVideoThumbnailBlacklist>
function loadVideoByUrl (
url: string,
fetchType: VideoLoadByUrlType
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable>
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist | MVideoImmutable>
function loadVideoByUrl (
url: string,
fetchType: VideoLoadByUrlType
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist | MVideoImmutable> {
if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccountAndFiles(url)
if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url)
if (fetchType === 'unsafe-only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url)
if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
if (fetchType === 'only-video-and-blacklist') return VideoModel.loadByUrlWithBlacklist(url)
}
async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) {
@ -64,7 +64,7 @@ async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) {
try {
const res = await getOrCreateAPVideo({
videoObject: videoUrl,
fetchType: 'only-immutable-attributes',
fetchType: 'unsafe-only-immutable-attributes',
allowRefresh: false
})
@ -78,10 +78,8 @@ async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) {
}
export {
type VideoLoadType,
type VideoLoadByUrlType,
loadVideo,
loadOrCreateVideoIfAllowedForUser, loadVideo,
loadVideoByUrl,
loadOrCreateVideoIfAllowedForUser
type VideoLoadByUrlType,
type VideoLoadType
}

Some files were not shown because too many files have changed in this diff Show More