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 # Changelog
## v6.1.0-rc.1 ## v6.1.0
### IMPORTANT NOTES ### 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) * 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 * 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) ### Admin config (non-exhaustive)
* **Breaking changes**: * **Breaking changes**:
@ -127,6 +132,9 @@
* Fix view endpoint crash on geoip update failure * Fix view endpoint crash on geoip update failure
* Fix setting video subtitle from URL query * Fix setting video subtitle from URL query
* Fix selecting "Display all languages/categories/licences" in videos search resulting in an empty search * 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 ## v6.0.4

View File

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

View File

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

View File

@ -309,7 +309,7 @@ export class VideoStatsComponent implements OnInit {
{ {
label: $localize`Views`, label: $localize`Views`,
value: this.numberFormatter.transform(this.video.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`, 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 { 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 { LoaderComponent } from '@app/shared/shared-main/loaders/loader.component'
import { UserNotificationService } from '@app/shared/shared-main/users/user-notification.service' 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({ @Component({
selector: 'my-notification', selector: 'my-notification',
templateUrl: './notification.component.html', templateUrl: './notification.component.html',
styleUrls: [ './notification.component.scss' ], styleUrls: [ './notification.component.scss' ],
standalone: true, standalone: true,
imports: [ CommonModule, NgbPopoverModule, UserNotificationsComponent, GlobalIconComponent, LoaderComponent ] imports: [
CommonModule,
NgbPopoverModule,
UserNotificationsComponent,
GlobalIconComponent,
LoaderComponent,
RouterLink,
RouterLinkActive
]
}) })
export class NotificationComponent implements OnInit, OnDestroy { export class NotificationComponent implements OnInit, OnDestroy {
@ViewChild('popover', { static: true }) popover: NgbPopover @ViewChild('popover', { static: true }) popover: NgbPopover

View File

@ -361,7 +361,10 @@ export class PeerTubePlayer {
getVideojsOptions (): videojs.PlayerOptions { getVideojsOptions (): videojs.PlayerOptions {
const html5 = { 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 = { const plugins: VideoJSPluginOptions = {

View File

@ -83,7 +83,7 @@
} }
</script> </script>
<my-app role="main"> <my-app>
</my-app> </my-app>
</body> </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"> <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"> <file source-language="en-US" datatype="plaintext" original="ng2.template" target-language="ro">
<body> <body>
@ -106,33 +106,33 @@
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
</context-group> </context-group>
</trans-unit> </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> <source>First</source>
<target/> <target state="translated">Primul</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
</trans-unit> </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> <source>Previous</source>
<target/> <target state="translated">Anterior</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">24</context> <context context-type="linenumber">24</context>
</context-group> </context-group>
</trans-unit> </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> <source>Next</source>
<target/> <target state="translated">Următorul</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">44</context>
</context-group> </context-group>
</trans-unit> </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> <source>Last</source>
<target/> <target state="translated">Ultimul</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
<context context-type="linenumber">53</context> <context context-type="linenumber">53</context>
@ -146,9 +146,9 @@
<context context-type="linenumber">7</context> <context context-type="linenumber">7</context>
</context-group> </context-group>
</trans-unit> </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> <source>Increment hours</source>
<target/> <target state="translated">Adaugă ore</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">9</context>
@ -170,17 +170,17 @@
<context context-type="linenumber">15</context> <context context-type="linenumber">15</context>
</context-group> </context-group>
</trans-unit> </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> <source>Decrement hours</source>
<target/> <target state="translated">Scade ore</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">23</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
</trans-unit> </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> <source>Increment minutes</source>
<target/> <target state="translated">Adaugă minute</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">32</context> <context context-type="linenumber">32</context>
@ -202,17 +202,17 @@
<context context-type="linenumber">37</context> <context context-type="linenumber">37</context>
</context-group> </context-group>
</trans-unit> </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> <source>Decrement minutes</source>
<target/> <target state="translated">Scade minute</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">45</context>
</context-group> </context-group>
</trans-unit> </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> <source>Increment seconds</source>
<target/> <target state="translated">Adaugă secunde</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">54</context>
@ -234,9 +234,9 @@
<context context-type="linenumber">59</context> <context context-type="linenumber">59</context>
</context-group> </context-group>
</trans-unit> </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> <source>Decrement seconds</source>
<target/> <target state="translated">Scade secunde</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context> <context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
<context context-type="linenumber">67</context> <context context-type="linenumber">67</context>
@ -298,17 +298,17 @@
<context context-type="linenumber">32</context> <context context-type="linenumber">32</context>
</context-group> </context-group>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context> <context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">40</context>
</context-group> </context-group>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context> <context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">48</context>
@ -438,9 +438,9 @@
<context context-type="linenumber">42</context> <context context-type="linenumber">42</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1394835141143590910" datatype="html"> <trans-unit id="1394835141143590910" datatype="html" xml:space="preserve">
<source>Start at</source> <source>Start at</source>
<target/> <target state="translated">Începe la</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context> <context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context>
<context context-type="linenumber">17</context> <context context-type="linenumber">17</context>
@ -454,9 +454,9 @@
<context context-type="linenumber">75</context> <context context-type="linenumber">75</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5964984095397511808" datatype="html"> <trans-unit id="5964984095397511808" datatype="html" xml:space="preserve">
<source>Stop at</source> <source>Stop at</source>
<target/> <target state="translated">Oprește la</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context> <context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
@ -766,17 +766,17 @@
<context context-type="linenumber">26</context> <context context-type="linenumber">26</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8558962068274430520" datatype="html"> <trans-unit id="8558962068274430520" datatype="html" xml:space="preserve">
<source>Unfederate the video</source> <source>Unfederate the video</source>
<target/> <target state="translated">Decuplați videoclipul</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/modals/video-blacklist.component.html</context> <context context-type="sourcefile">src/app/shared/video/modals/video-blacklist.component.html</context>
<context context-type="linenumber">23</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7539427273132299890" datatype="html"> <trans-unit id="7539427273132299890" datatype="html" xml:space="preserve">
<source>Unlisted</source> <source>Unlisted</source>
<target/> <target state="translated">Nelistat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context> <context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">6</context>
@ -802,9 +802,9 @@
<context context-type="linenumber">7</context> <context context-type="linenumber">7</context>
</context-group> </context-group>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context> <context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
<context context-type="linenumber">23</context> <context context-type="linenumber">23</context>
@ -818,9 +818,9 @@
<context context-type="linenumber">41</context> <context context-type="linenumber">41</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3514509630940272440" datatype="html"> <trans-unit id="3514509630940272440" datatype="html" xml:space="preserve">
<source>Sensitive</source> <source>Sensitive</source>
<target/> <target state="translated">Sensitiv</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context> <context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">45</context>
@ -874,9 +874,9 @@
<context context-type="linenumber">100</context> <context context-type="linenumber">100</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5263519165976128456" datatype="html"> <trans-unit id="5263519165976128456" datatype="html" xml:space="preserve">
<source>Edit starts/stops at</source> <source>Edit starts/stops at</source>
<target/> <target state="translated">Editarea începe/oprește la</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/video-playlist/video-playlist-element-miniature.component.html</context> <context context-type="sourcefile">src/app/shared/video-playlist/video-playlist-element-miniature.component.html</context>
<context context-type="linenumber">50</context> <context context-type="linenumber">50</context>
@ -1182,9 +1182,9 @@
<context context-type="linenumber">17</context> <context context-type="linenumber">17</context>
</context-group> </context-group>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">src/app/shared/user-subscription/remote-subscribe.component.html</context> <context context-type="sourcefile">src/app/shared/user-subscription/remote-subscribe.component.html</context>
<context context-type="linenumber">26</context> <context context-type="linenumber">26</context>
@ -1198,9 +1198,9 @@
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
</context-group> </context-group>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context> <context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">11</context> <context context-type="linenumber">11</context>
@ -1266,9 +1266,9 @@
<context context-type="linenumber">139</context> <context context-type="linenumber">139</context>
</context-group> </context-group>
</trans-unit> </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> <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-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context> <context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">59</context> <context context-type="linenumber">59</context>
@ -1306,9 +1306,9 @@
<context context-type="linenumber">77</context> <context context-type="linenumber">77</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7683705529753923369" datatype="html"> <trans-unit id="7683705529753923369" datatype="html" xml:space="preserve">
<source>Player</source> <source>Player</source>
<target/> <target state="translated">Jucător</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context> <context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">85</context> <context context-type="linenumber">85</context>
@ -1334,9 +1334,9 @@
<context context-type="linenumber">7</context> <context context-type="linenumber">7</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8630916846096019339" datatype="html"> <trans-unit id="8630916846096019339" datatype="html" xml:space="preserve">
<source>Users can resolve distant content</source> <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-group purpose="location">
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context> <context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
<context context-type="linenumber">100</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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts</context> <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> <source>Are you sure you want to delete this <x id="PH" equiv-text="file.resolution.label"/> file?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6693349469471580292" datatype="html"> <trans-unit id="6693349469471580292" datatype="html">
<source>Delete file</source> <source>Delete file</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7016764388104297354" datatype="html"> <trans-unit id="7016764388104297354" datatype="html">
<source>File removed.</source> <source>File removed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="925076027211452339" datatype="html"> <trans-unit id="925076027211452339" datatype="html">
<source>Are you sure you want to delete the original file of this video?</source> <source>Are you sure you want to delete the original file of this video?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3014914668468316940" datatype="html"> <trans-unit id="3014914668468316940" datatype="html">
<source>Delete original file</source> <source>Delete original file</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6647462936549531405" datatype="html"> <trans-unit id="6647462936549531405" datatype="html">
<source>Original file removed.</source> <source>Original file removed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1314383205093440631" datatype="html"> <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> <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-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5779580280418408097" datatype="html"> <trans-unit id="5779580280418408097" datatype="html">
<source>Deleted {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source> <source>Deleted {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9164541937317586242" datatype="html"> <trans-unit id="9164541937317586242" datatype="html">
<source>Unblocked {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source> <source>Unblocked {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6228449077605046873" datatype="html"> <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> <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-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4435640428611044716" datatype="html"> <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> <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-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1571742433738679426" datatype="html"> <trans-unit id="1571742433738679426" datatype="html">
<source>Files were removed.</source> <source>Files were removed.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7054344823477412274" datatype="html"> <trans-unit id="7054344823477412274" datatype="html">
<source>Transcoding jobs created.</source> <source>Transcoding jobs created.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2591467977473302125" datatype="html"> <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", "Copy magnet URI": "Copiar lo magnet URI",
"Total downloaded: ": "Total telecargat: ", "Total downloaded: ": "Total telecargat: ",
"Total uploaded: ": "Total enviat: ", "Total uploaded: ": "Total enviat: ",
"From servers: ": "From servers: ", "From servers: ": "Dels servidors : ",
"From peers: ": "From peers: ", "From peers: ": "Dels pars : ",
"Normal mode": "Normal mode", "Normal mode": "Normal mode",
"Stats for nerds": "Stats for nerds", "Stats for nerds": "Estatisticas pels nerds",
"Theater mode": "Theater mode", "Theater mode": "Mòde cinèma",
"Video UUID": "Video UUID", "Video UUID": "UUID de la vidèo",
"Viewport / Frames": "Viewport / Frames", "Viewport / Frames": "Fenèstra dafichatge / Quadres",
"Resolution": "Resolution", "Resolution": "Resolucion",
"Volume": "Volume", "Volume": "Volume",
"Codecs": "Codecs", "Codecs": "Codecs",
"Color": "Color", "Color": "Color",
"Go back to the live": "Go back to the live", "Go back to the live": "Tornar al dirèct",
"Connection Speed": "Connection Speed", "Connection Speed": "Velocitat de la connexion",
"Network Activity": "Network Activity", "Network Activity": "Activitat ret",
"Total Transfered": "Total Transfered", "Total Transfered": "Total Transferit",
"Download Breakdown": "Download Breakdown", "Download Breakdown": "Reparticion dels telecargaments",
"Buffer Progress": "Buffer Progress", "Buffer Progress": "Buffer Progress",
"Buffer State": "Buffer State", "Buffer State": "Estat memòria tampon",
"Live Latency": "Live Latency", "Live Latency": "Laténcia en viu",
"P2P": "P2P", "P2P": "P2P",
"{1} seconds": "{1} seconds", "{1} seconds": "{1} segondas",
"enabled": "enabled", "enabled": "activat",
"Playlist: {1}": "Playlist: {1}", "Playlist: {1}": "Playlist: {1}",
"disabled": "disabled", "disabled": "desactivat",
" off": " desactivats", " off": " desactivats",
"Player mode": "Player mode", "Player mode": "Player mode",
"Play in loop": "Play in loop", "Play in loop": "Lector en bocla",
"This live has not started yet.": "This live has not started yet.", "This live has not started yet.": "Lo dirècte a pas encara començat.",
"This live has ended.": "This live has ended.", "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.", "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}", "{1} / {2} dropped of {3}": "{1} / {2} dropped of {3}",
" (muted)": " (muted)", " (muted)": " (mut)",
"{1} from servers · {2} from peers": "{1} from servers · {2} from peers", "{1} from servers · {2} from peers": "{1} dels servidors · {2} dels pars",
"Previous video": "Previous video", "Previous video": "Vidèo precedenta",
"Video page (new window)": "Video page (new window)", "Video page (new window)": "Pagina vidèo (fenèstra novèla)",
"Next video": "Next video", "Next video": "Next video",
"This video is password protected": "This video is password protected", "This video is password protected": "Un senhal protegís aquesta vidèo",
"You need a password to watch this video.": "You need a password to watch this video.", "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": "Incorrect password, please enter a correct password", "Incorrect password, please enter a correct password": "Senhal incorrècte, mercés de picar un senhal corrècte",
"Cancel": "Cancel", "Cancel": "Anullar",
"Up Next": "Up Next", "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})", "{1} (from edge: {2})": "{1} (from edge: {2})",
"Disable subtitles": "Disable subtitles", "Disable subtitles": "Desactivar los sostítols",
"Enable {1} subtitle": "Enable {1} subtitle", "Enable {1} subtitle": "Activar {1} sostítol",
"Audio Player": "Lector àudio", "Audio Player": "Lector àudio",
"Video Player": "Lector vidèo", "Video Player": "Lector vidèo",
"Play": "Lectura", "Play": "Lectura",

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "peertube", "name": "peertube",
"description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.", "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, "private": true,
"licence": "AGPL-3.0", "licence": "AGPL-3.0",
"engines": { "engines": {
@ -237,7 +237,7 @@
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"proxy": "^2.1.1", "proxy": "^2.1.1",
"socket.io-client": "^4.5.4", "socket.io-client": "^4.5.4",
"supertest": "^6.0.1", "supertest": "^7.0.0",
"swagger-cli": "^4.0.2", "swagger-cli": "^4.0.2",
"tsc-watch": "^6.0.0", "tsc-watch": "^6.0.0",
"tsx": "^4.7.1", "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 { expect } from 'chai'
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js' import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { sortObjectComparator } from '@peertube/peertube-core-utils' 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 { import {
BlacklistCommand, BlacklistCommand,
cleanupTests, cleanupTests,
createMultipleServers, createMultipleServers,
doubleFollow, PeerTubeServer, doubleFollow, makeActivityPubGetRequest, PeerTubeServer,
setAccessTokensToServers, setAccessTokensToServers,
setDefaultChannelAvatar, setDefaultChannelAvatar,
waitJobs waitJobs
@ -298,6 +298,13 @@ describe('Test video blacklist', function () {
expect(video4Blacklisted.unfederated).to.be.true 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 () { it('Should remove the video from blacklist and refederate the video', async function () {
await command.remove({ videoId: video4UUID }) 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[1].videoSource.fileDownloadUrl).to.exist
expect(data[2].videoSource).to.exist expect(data[2].videoSource).to.exist
expect(data[2].videoSource.fileDownloadUrl).to.not.exist expect(data[2].videoSource.fileDownloadUrl).to.not.exist
expect(data[2].videoSource.createdAt).to.exist expect(data[2].videoSource.createdAt).to.exist
expect(data[2].videoSource.fps).to.be.null expect(data[2].videoSource.fps).to.to.exist
expect(data[2].videoSource.height).to.be.null expect(data[2].videoSource.height).to.to.exist
expect(data[2].videoSource.width).to.be.null expect(data[2].videoSource.width).to.to.exist
expect(data[2].videoSource.resolution.id).to.be.null expect(data[2].videoSource.resolution.id).to.to.exist
expect(data[2].videoSource.resolution.label).to.be.null expect(data[2].videoSource.resolution.label).to.to.exist
expect(data[2].videoSource.size).to.be.null expect(data[2].videoSource.size).to.to.exist
expect(data[2].videoSource.metadata).to.be.null expect(data[2].videoSource.metadata).to.to.exist
}) })
it('Should delete all videos and do not have original files anymore', async function () { 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 { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import { signAndContextify } from '@peertube/peertube-server/core/helpers/activity-pub-utils.js' 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 { 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 { expect } from 'chai'
import { readJsonSync } from 'fs-extra/esm' import { readJsonSync } from 'fs-extra/esm'
import cloneDeep from 'lodash-es/cloneDeep.js' import cloneDeep from 'lodash-es/cloneDeep.js'
@ -24,6 +24,10 @@ function fakeFilter () {
return (data: any) => Promise.resolve(data) return (data: any) => Promise.resolve(data)
} }
function fakeExpressReq (body: any) {
return { body }
}
describe('Test activity pub helpers', function () { describe('Test activity pub helpers', function () {
describe('When checking the Linked Signature', 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 publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } 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 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 publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } 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 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 publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } 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 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 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 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 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 expect(result).to.be.true
}) })

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import express from 'express' import { HttpStatusCode, VideoChangeOwnershipStatus } from '@peertube/peertube-models'
import { HttpStatusCode, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models' import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
import { MVideoFullLight } from '@server/types/models/index.js' import { MVideoFullLight } from '@server/types/models/index.js'
import express from 'express'
import { logger } from '../../../helpers/logger.js' import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js' import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.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 const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
targetVideoUpdated.VideoChannel = channel targetVideoUpdated.VideoChannel = channel
if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) { if (canVideoBeFederated(targetVideoUpdated)) {
await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t) await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t)
await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) 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) await video.removeOriginalFile(videoSource)
videoSource.keptOriginalFilename = null videoSource.keptOriginalFilename = null
videoSource.fps = null videoSource.storage = null
videoSource.resolution = null
videoSource.width = null
videoSource.height = null
videoSource.metadata = null
videoSource.size = null
await videoSource.save() await videoSource.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.sendStatus(HttpStatusCode.NO_CONTENT_204)

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { ContextType } from '@peertube/peertube-models' import { ContextType } from '@peertube/peertube-models'
import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js' import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js'
import { isArray } from './custom-validators/misc.js'
import { buildDigest } from './peertube-crypto.js' import { buildDigest } from './peertube-crypto.js'
import type { signJsonLDObject } from './peertube-jsonld.js' import type { signJsonLDObject } from './peertube-jsonld.js'
import { doJSONRequest } from './requests.js' import { doJSONRequest } from './requests.js'
import { isArray } from './custom-validators/misc.js'
export type ContextFilter = <T> (arg: T) => Promise<T> export type ContextFilter = <T> (arg: T) => Promise<T>
@ -49,6 +49,18 @@ export async function getApplicationActorOfHost (host: string) {
return found?.href || undefined 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 // Private
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -58,7 +70,6 @@ type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string
const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = { const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = {
Video: buildContext({ Video: buildContext({
Hashtag: 'as:Hashtag', Hashtag: 'as:Hashtag',
uuid: 'sc:identifier',
category: 'sc:category', category: 'sc:category',
licence: 'sc:license', licence: 'sc:license',
subtitleLanguage: 'sc:subtitleLanguage', subtitleLanguage: 'sc:subtitleLanguage',
@ -99,6 +110,11 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
'@id': 'pt:aspectRatio' '@id': 'pt:aspectRatio'
}, },
uuid: {
'@type': 'sc:identifier',
'@id': 'pt:uuid'
},
originallyPublishedAt: 'sc:datePublished', originallyPublishedAt: 'sc:datePublished',
uploadDate: 'sc:uploadDate', uploadDate: 'sc:uploadDate',
@ -170,12 +186,23 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
'@type': 'sc:Number', '@type': 'sc:Number',
'@id': 'pt:stopTimestamp' '@id': 'pt:stopTimestamp'
}, },
uuid: 'sc:identifier' uuid: {
'@type': 'sc:identifier',
'@id': 'pt:uuid'
}
}), }),
CacheFile: buildContext({ CacheFile: buildContext({
expires: 'sc:expires', 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({ Flag: buildContext({
@ -205,15 +232,21 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
'@type': 'sc:Number', '@type': 'sc:Number',
'@id': 'pt:startTimestamp' '@id': 'pt:startTimestamp'
}, },
stopTimestamp: { endTimestamp: {
'@type': 'sc:Number', '@type': 'sc:Number',
'@id': 'pt:stopTimestamp' '@id': 'pt:endTimestamp'
}, },
watchSection: { uuid: {
'@type': 'sc:Number', '@type': 'sc:identifier',
'@id': 'pt:stopTimestamp' '@id': 'pt:uuid'
}, },
uuid: 'sc:identifier' actionStatus: 'sc:actionStatus',
watchSections: {
'@type': '@id',
'@id': 'pt:watchSections'
},
addressRegion: 'sc:addressRegion',
addressCountry: 'sc:addressCountry'
}), }),
View: buildContext({ View: buildContext({
@ -233,13 +266,46 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
Rate: buildContext(), Rate: buildContext(),
Chapters: buildContext({ Chapters: buildContext({
name: 'sc:name',
hasPart: 'sc:hasPart', hasPart: 'sc:hasPart',
endOffset: 'sc:endOffset', endOffset: 'sc:endOffset',
startOffset: 'sc:startOffset' 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) { async function getContextData (type: ContextType, contextFilter: ContextFilter) {
const contextData = contextFilter const contextData = contextFilter
? await contextFilter(contextStore[type]) ? await contextFilter(contextStore[type])

View File

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

View File

@ -1,20 +1,15 @@
import { CacheFileObject } from '@peertube/peertube-models' 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 { isActivityPubUrlValid } from './misc.js'
import { isRemoteVideoUrlValid } from './videos.js'
function isCacheFileObjectValid (object: CacheFileObject) { export function isCacheFileObjectValid (object: CacheFileObject) {
return exists(object) && if (!object || object.type !== 'CacheFile') return false
object.type === 'CacheFile' &&
(object.expires === null || isDateValid(object.expires)) && return (!object.expires || isDateValid(object.expires)) &&
isActivityPubUrlValid(object.object) && isActivityPubUrlValid(object.object) &&
(isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) (isRedundancyUrlVideoValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
export {
isCacheFileObjectValid
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -24,3 +19,15 @@ function isPlaylistRedundancyUrlValid (url: any) {
(url.mediaType || url.mimeType) === 'application/x-mpegURL' && (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href) 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 { PlaylistElementObject, PlaylistObject } from '@peertube/peertube-models'
import validator from 'validator'
import { exists, isDateValid, isUUIDValid } from '../misc.js' import { exists, isDateValid, isUUIDValid } from '../misc.js'
import { isVideoPlaylistNameValid } from '../video-playlists.js' import { isVideoPlaylistNameValid } from '../video-playlists.js'
import { isActivityPubUrlValid } from './misc.js' import { isActivityPubUrlValid } from './misc.js'
function isPlaylistObjectValid (object: PlaylistObject) { export function isPlaylistObjectValid (object: PlaylistObject) {
return exists(object) && if (!object || object.type !== 'Playlist') return false
object.type === 'Playlist' &&
validator.default.isInt(object.totalItems + '') && // 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) && isVideoPlaylistNameValid(object.name) &&
isUUIDValid(object.uuid) && isUUIDValid(object.uuid) &&
isDateValid(object.published) && isDateValid(object.published) &&
isDateValid(object.updated) isDateValid(object.updated)
} }
function isPlaylistElementObjectValid (object: PlaylistElementObject) { export function isPlaylistElementObjectValid (object: PlaylistElementObject) {
return exists(object) && return exists(object) &&
object.type === 'PlaylistElement' && object.type === 'PlaylistElement' &&
validator.default.isInt(object.position + '') && validator.default.isInt(object.position + '') &&
isActivityPubUrlValid(object.url) 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 validator from 'validator'
import { ACTIVITY_PUB } from '../../../initializers/constants.js'
import { exists, isArray, isDateValid } from '../misc.js' import { exists, isArray, isDateValid } from '../misc.js'
import { isActivityPubUrlValid } from './misc.js' import { isActivityPubUrlValid } from './misc.js'
@ -23,10 +23,7 @@ function sanitizeAndCheckVideoCommentObject (comment: any) {
isDateValid(comment.published) && isDateValid(comment.published) &&
isActivityPubUrlValid(comment.url) && isActivityPubUrlValid(comment.url) &&
isArray(comment.to) && isArray(comment.to) &&
( (hasAPPublic(comment.to) || hasAPPublic(comment.cc)) // Only accept public comments
comment.to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ||
comment.cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1
) // Only accept public comments
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -27,7 +27,7 @@ function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
sanitizeAndCheckVideoTorrentObject(activity.object) sanitizeAndCheckVideoTorrentObject(activity.object)
} }
function sanitizeAndCheckVideoTorrentObject (video: any) { function sanitizeAndCheckVideoTorrentObject (video: VideoObject) {
if (!video || video.type !== 'Video') return false if (!video || video.type !== 'Video') return false
if (!setValidRemoteTags(video)) { if (!setValidRemoteTags(video)) {
@ -59,6 +59,9 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
return false return false
} }
// TODO: compat with < 6.1, remove in 7.0
if (!video.uuid && video['identifier']) video.uuid = video['identifier']
// Default attributes // Default attributes
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false 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 { 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 { isVideoTimeValid } from '../video-view.js'
import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js' import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js'
function isWatchActionObjectValid (action: WatchActionObject) { function isWatchActionObjectValid (action: WatchActionObject) {
return exists(action) && if (!action || action.type !== 'WatchAction') return false
action.type === 'WatchAction' &&
isObjectValid(action.id) && // 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) && isActivityPubVideoDurationValid(action.duration) &&
isDateValid(action.startTime) && isDateValid(action.startTime) &&
isDateValid(action.endTime) && isDateValid(action.endTime) &&
isLocationValid(action.location) && isLocationValid(action.location) &&
isUUIDValid(action.uuid) && isUUIDValid(action.uuid) &&
isObjectValid(action.object) && isObjectValid(action.object) &&
isWatchSectionsValid(action.watchSections) areWatchSectionsValid(action.watchSections)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -34,8 +41,11 @@ function isLocationValid (location: any) {
return true return true
} }
function isWatchSectionsValid (sections: WatchActionObject['watchSections']) { function areWatchSectionsValid (sections: WatchActionObject['watchSections']) {
return Array.isArray(sections) && sections.every(s => { 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) 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) 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 { sha256 } from '@peertube/peertube-node-utils'
import { createSign, createVerify } from 'crypto' import { createSign, createVerify } from 'crypto'
import cloneDeep from 'lodash-es/cloneDeep.js' import cloneDeep from 'lodash-es/cloneDeep.js'
import { MActor } from '../types/models/index.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 { logger } from './logger.js'
import { assertIsInWorkerThread } from './threads.js' import { assertIsInWorkerThread } from './threads.js'
import { jsonld } from './custom-jsonld-signature.js'
export function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> { type ExpressRequest = { body: any }
if (signedDocument.signature.type === 'RsaSignature2017') {
return isJsonLDRSA2017Verified(fromActor, signedDocument) 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) return Promise.resolve(false)
} }
// Backward compatibility with "other" implementations // 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([ const [ documentHash, optionsHash ] = await Promise.all([
createDocWithoutSignatureHash(signedDocument), hashObject(compacted, safe),
createSignatureHash(signedDocument.signature) createSignatureHash(req.body.signature, safe)
]) ])
const toVerify = optionsHash + documentHash const toVerify = optionsHash + documentHash
@ -28,7 +53,39 @@ export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument
const verify = createVerify('RSA-SHA256') const verify = createVerify('RSA-SHA256')
verify.update(toVerify, 'utf8') 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: { export async function signJsonLDObject <T> (options: {
@ -66,35 +123,40 @@ export async function signJsonLDObject <T> (options: {
// Private // Private
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function hashObject (obj: any): Promise<any> { async function hashObject (obj: any, safe: boolean): Promise<any> {
const res = await (jsonld as any).promises.normalize(obj, { const res = await jsonldNormalize(obj, safe)
safe: false,
algorithm: 'URDNA2015',
format: 'application/n-quads'
})
return sha256(res) return sha256(res)
} }
function createSignatureHash (signature: any) { function jsonldCompact (obj: any) {
const signatureCopy = cloneDeep(signature) return (jsonld as any).promises.compact(obj, getAllContext())
Object.assign(signatureCopy, { }
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': [ '@context': [
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
{ RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' } { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
] ],
})
delete signatureCopy.type ...omit(signature, [ 'type', 'id', 'signatureValue' ])
delete signatureCopy.id }, safe)
delete signatureCopy.signatureValue
return hashObject(signatureCopy)
} }
function createDocWithoutSignatureHash (doc: any) { function createDocWithoutSignatureHash (doc: any) {
const docWithoutSignature = cloneDeep(doc) const docWithoutSignature = cloneDeep(doc)
delete docWithoutSignature.signature delete docWithoutSignature.signature
return hashObject(docWithoutSignature) return hashObject(docWithoutSignature, true)
} }

View File

@ -1,51 +1,28 @@
import { Response } from 'express' import { VideoPrivacy } from '@peertube/peertube-models'
import { forceNumber } from '@peertube/peertube-core-utils'
import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models'
import { CONFIG } from '@server/initializers/config.js' import { CONFIG } from '@server/initializers/config.js'
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models/index.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 return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo
} }
function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { export function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
return isStreamingPlaylist(videoOrPlaylist) return isStreamingPlaylist(videoOrPlaylist)
? videoOrPlaylist.Video ? videoOrPlaylist.Video
: videoOrPlaylist : videoOrPlaylist
} }
function isPrivacyForFederation (privacy: VideoPrivacyType) { export function getPrivaciesForFederation () {
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 () {
return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true) return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true)
? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ] ? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ]
: [ { privacy: VideoPrivacy.PUBLIC } ] : [ { 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] const value = mimeTypes[mimeType]
if (Array.isArray(value)) return value[0] if (Array.isArray(value)) return value[0]
return value 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"' 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
], ],
ACCEPT_HEADER: 'application/activity+json, application/ld+json', ACCEPT_HEADER: 'application/activity+json, application/ld+json',
PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
COLLECTION_ITEMS_PER_PAGE: 10, COLLECTION_ITEMS_PER_PAGE: 10,
FETCH_PAGE_LIMIT: 2000, FETCH_PAGE_LIMIT: 2000,
MAX_RECURSION_COMMENTS: 100, 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 { 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' 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) return buildAudience([ actorSender.followersUrl ], isPublic)
} }
function buildAudience (followerUrls: string[], isPublic = true) { export function buildAudience (followerUrls: string[], isPublic = true) {
let to: string[] = [] let to: string[] = []
let cc: string[] = [] let cc: string[] = []
if (isPublic) { if (isPublic) {
to = [ ACTIVITY_PUB.PUBLIC ] to = [ getAPPublicValue() ]
cc = followerUrls cc = followerUrls
} else { // Unlisted } else { // Unlisted
to = [] to = []
@ -21,14 +21,6 @@ function buildAudience (followerUrls: string[], isPublic = true) {
return { to, cc } return { to, cc }
} }
function audiencify<T> (object: T, audience: ActivityAudience) { export function audiencify<T> (object: T, audience: ActivityAudience) {
return { ...audience, ...object } 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 { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models/index.js'
import { CacheFileObject, VideoStreamingPlaylistType } from '@peertube/peertube-models' import { CacheFileObject, VideoStreamingPlaylistType } from '@peertube/peertube-models'
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy.js' 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) { async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
@ -65,11 +66,15 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
} }
const url = cacheFileObject.url 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 => { 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 { return {
expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, 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 { StatsManager } from '../stat-manager.js'
import { processActivities } from './process/index.js' import { processActivities } from './process/index.js'
class InboxManager { export class InboxManager {
private static instance: InboxManager private static instance: InboxManager
private readonly inboxQueue: PQueue private readonly inboxQueue: PQueue
@ -39,9 +39,3 @@ class InboxManager {
return this.instance || (this.instance = new this()) return this.instance || (this.instance = new this())
} }
} }
// ---------------------------------------------------------------------------
export {
InboxManager
}

View File

@ -145,7 +145,7 @@ async function buildElementsDBAttributes (elementUrls: string[], playlist: MVide
try { try {
const { elementObject } = await fetchRemotePlaylistElement(elementUrl) 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)) elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video))
} catch (err) { } 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 { 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[]) { export function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) {
const privacy = to.includes(ACTIVITY_PUB.PUBLIC) const privacy = hasAPPublic(to)
? VideoPlaylistPrivacy.PUBLIC ? VideoPlaylistPrivacy.PUBLIC
: VideoPlaylistPrivacy.UNLISTED : VideoPlaylistPrivacy.UNLISTED
@ -23,7 +23,11 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: strin
} as AttributesOnly<VideoPlaylistModel> } as AttributesOnly<VideoPlaylistModel>
} }
function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) { export function playlistElementObjectToDBAttributes (
elementObject: PlaylistElementObject,
videoPlaylist: MVideoPlaylistId,
video: MVideoId
) {
return { return {
position: elementObject.position, position: elementObject.position,
url: elementObject.id, url: elementObject.id,
@ -33,8 +37,3 @@ function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObje
videoId: video.id videoId: video.id
} as AttributesOnly<VideoPlaylistElementModel> } 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 { createOrUpdateVideoPlaylist } from '../playlists/index.js'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js' import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
import { resolveThread } from '../video-comments.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>>) { async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -87,6 +87,11 @@ async function processCreateCacheFile (
const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) 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 => { await sequelizeTypescript.transaction(async t => {
return createOrUpdateCacheFile(cacheFile, video, byActor, 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 { 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 { retryTransactionWrapper } from '../../../helpers/database-utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js' import { sequelizeTypescript } from '../../../initializers/database.js'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js' import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js' import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
import { MActorSignature } from '../../../types/models/index.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>) { async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -21,14 +22,19 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processDislike (activity: ActivityDislike, byActor: MActorSignature) { async function processDislike (activity: ActivityDislike, byActor: MActorSignature) {
const dislikeObject = activity.object const videoUrl = activity.object
const byAccount = byActor.Account const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) 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 (!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 => { return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadFull(onlyVideo.id, t) const video = await VideoModel.loadFull(onlyVideo.id, t)

View File

@ -1,4 +1,5 @@
import { ActivityLike } from '@peertube/peertube-models' import { ActivityLike } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { retryTransactionWrapper } from '../../../helpers/database-utils.js' import { retryTransactionWrapper } from '../../../helpers/database-utils.js'
import { sequelizeTypescript } from '../../../initializers/database.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 { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js' import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
import { MActorSignature } from '../../../types/models/index.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>) { async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -28,9 +29,14 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
const byAccount = byActor.Account const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) 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 (!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 => { return sequelizeTypescript.transaction(async t => {
const video = await VideoModel.loadFull(onlyVideo.id, 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 { createOrUpdateCacheFile } from '../cache-file.js'
import { createOrUpdateVideoPlaylist } from '../playlists/index.js' import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.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>>) { async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
const { activity, byActor } = options const { activity, byActor } = options
@ -93,6 +93,11 @@ async function processUpdateCacheFile (
const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) 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 sequelizeTypescript.transaction(async t => {
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t) await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
}) })

View File

@ -24,7 +24,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
const { video } = await getOrCreateAPVideo({ const { video } = await getOrCreateAPVideo({
videoObject, videoObject,
fetchType: 'only-video', fetchType: 'only-video-and-blacklist',
allowRefresh: false allowRefresh: false
}) })
@ -32,8 +32,8 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
video, video,
viewerId: activity.id, viewerId: activity.id,
viewerExpires: activity.expires viewerExpires: getExpires(activity)
? new Date(activity.expires) ? new Date(getExpires(activity))
: undefined, : undefined,
viewerResultCounter: getViewerResultCounter(activity) viewerResultCounter: getViewerResultCounter(activity)
}) })
@ -49,10 +49,15 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
function getViewerResultCounter (activity: ActivityView) { function getViewerResultCounter (activity: ActivityView) {
const result = activity.result 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 + '') const counter = parseInt(result.userInteractionCount + '')
if (isNaN(counter)) return undefined if (isNaN(counter)) return undefined
return counter 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 View: processViewActivity
} }
async function processActivities ( export async function processActivities (
activities: Activity[], activities: Activity[],
options: { options: {
signatureActor?: MActorSignature 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 { import {
ActivityAudience, ActivityAudience,
ActivityCreate, ActivityCreate,
@ -9,19 +7,24 @@ import {
VideoPlaylistPrivacy, VideoPlaylistPrivacy,
VideoPrivacy VideoPrivacy
} from '@peertube/peertube-models' } 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 { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { VideoCommentModel } from '../../../models/video/video-comment.js' import { VideoCommentModel } from '../../../models/video/video-comment.js'
import { import {
MActorLight, MActorLight,
MCommentOwnerVideo, MCommentOwnerVideo,
MLocalVideoViewerWithWatchSections, MLocalVideoViewerWithWatchSections,
MVideoAccountLight,
MVideoAP, MVideoAP,
MVideoAccountLight,
MVideoPlaylistFull, MVideoPlaylistFull,
MVideoRedundancyFileVideo, MVideoRedundancyFileVideo,
MVideoRedundancyStreamingPlaylistVideo MVideoRedundancyStreamingPlaylistVideo
} from '../../../types/models/index.js' } from '../../../types/models/index.js'
import { audiencify, getAudience } from '../audience.js' import { audiencify, getAudience } from '../audience.js'
import { canVideoBeFederated } from '../videos/federate.js'
import { import {
broadcastToActors, broadcastToActors,
broadcastToFollowers, broadcastToFollowers,
@ -32,12 +35,11 @@ import {
sendVideoRelatedActivity, sendVideoRelatedActivity,
unicastTo unicastTo
} from './shared/index.js' } from './shared/index.js'
import { AccountModel } from '@server/models/account/account.js'
const lTags = loggerTagsFactory('ap', 'create') const lTags = loggerTagsFactory('ap', 'create')
async function sendCreateVideo (video: MVideoAP, transaction: Transaction) { export async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
if (!video.hasPrivacyForFederation()) return undefined if (!canVideoBeFederated(video)) return undefined
logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid)) 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, byActor: MActorLight,
video: MVideoAccountLight, video: MVideoAccountLight,
fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo 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)) logger.info('Creating job to send create watch action %s.', stats.url, lTags(stats.uuid))
const byActor = await getServerActor() const byActor = await getServerActor()
@ -84,7 +86,7 @@ async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections,
return sendVideoActivityToOrigin(activityBuilder, { byActor, video: stats.Video, transaction, contextType: 'WatchAction' }) 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 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid)) 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) { export async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) {
logger.info('Creating job to send comment %s.', comment.url)
const isOrigin = comment.Video.isOwned() 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 byActor = comment.Account.Actor
const videoAccount = await AccountModel.load(comment.Video.VideoChannel.Account.id, transaction) 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, url: string,
byActor: MActorLight, byActor: MActorLight,
object: T, object: T,
@ -201,16 +212,7 @@ function buildCreateActivity <T extends ActivityCreateObject> (
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Private
export {
sendCreateVideo,
buildCreateActivity,
sendCreateVideoComment,
sendCreateVideoPlaylist,
sendCreateCacheFile,
sendCreateWatchAction
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function sendVideoRelatedCreateActivity (options: { 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 { 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 { logger } from '../../../helpers/logger.js'
import { AccountModel } from '../../../models/account/account.js' import { AccountModel } from '../../../models/account/account.js'
import { VideoModel } from '../../../models/video/video.js'
import { VideoShareModel } from '../../../models/video/video-share.js' import { VideoShareModel } from '../../../models/video/video-share.js'
import { VideoModel } from '../../../models/video/video.js'
import { import {
MAccountDefault, MAccountDefault,
MActor, MActor,
@ -16,11 +16,12 @@ import {
} from '../../../types/models/index.js' } from '../../../types/models/index.js'
import { audiencify, getAudience } from '../audience.js' import { audiencify, getAudience } from '../audience.js'
import { getUpdateActivityPubUrl } from '../url.js' import { getUpdateActivityPubUrl } from '../url.js'
import { canVideoBeFederated } from '../videos/federate.js'
import { getActorsInvolvedInVideo } from './shared/index.js' import { getActorsInvolvedInVideo } from './shared/index.js'
import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils.js' import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils.js'
async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) { export async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) {
if (!videoArg.hasPrivacyForFederation()) return undefined if (!canVideoBeFederated(videoArg)) return undefined
const video = await videoArg.lightAPToFullAP(transaction) 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 const byActor = accountOrChannel.Actor
logger.info('Creating job to update actor %s.', byActor.url) 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) logger.info('Creating job to update cache file %s.', redundancyModel.url)
const associatedVideo = redundancyModel.getVideo() const associatedVideo = redundancyModel.getVideo()
@ -98,7 +99,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide
return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' }) 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 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
const byActor = videoPlaylist.OwnerAccount.Actor const byActor = videoPlaylist.OwnerAccount.Actor
@ -127,14 +128,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, trans
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Private
export {
sendUpdateActor,
sendUpdateVideo,
sendUpdateCacheFile,
sendUpdateVideoPlaylist
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function buildUpdateActivity ( 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 { 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 { return {
to: [ accountActor.url ], to: [ accountActor.url ],
cc: actorsInvolvedInVideo.map(a => a.followersUrl) cc: actorsInvolvedInVideo.map(a => a.followersUrl)
} }
} }
function getVideoCommentAudience ( export function getVideoCommentAudience (
videoComment: MCommentOwnerVideo, videoComment: MCommentOwnerVideo,
threadParentComments: MCommentOwner[], threadParentComments: MCommentOwner[],
actorsInvolvedInVideo: MActorFollowersUrl[], actorsInvolvedInVideo: MActorFollowersUrl[],
isOrigin = false isOrigin = false
): ActivityAudience { ): ActivityAudience {
const to = [ ACTIVITY_PUB.PUBLIC ] const to = [ getAPPublicValue() ]
const cc: string[] = [] const cc: string[] = []
// Owner of the video we comment // Owner of the video we comment
@ -43,14 +43,14 @@ function getVideoCommentAudience (
} }
} }
function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience { export function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
return { return {
to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)), to: [ getAPPublicValue() ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
cc: [] 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 actors = await VideoShareModel.listActorIdsAndFollowerUrlsByShare(video.id, t)
const alreadyLoadedActor = (video as VideoModel).VideoChannel?.Account?.Actor const alreadyLoadedActor = (video as VideoModel).VideoChannel?.Account?.Actor
@ -63,12 +63,3 @@ async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
return actors return actors
} }
// ---------------------------------------------------------------------------
export {
getOriginVideoAudience,
getActorsInvolvedInVideo,
getAudienceFromFollowersOf,
getVideoCommentAudience
}

View File

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

View File

@ -1,6 +1,6 @@
import { getServerActor } from '@server/models/application/application.js'
import Bluebird from 'bluebird' import Bluebird from 'bluebird'
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { getServerActor } from '@server/models/application/application.js'
import { logger, loggerTagsFactory } from '../../helpers/logger.js' import { logger, loggerTagsFactory } from '../../helpers/logger.js'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants.js' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants.js'
import { VideoShareModel } from '../../models/video/video-share.js' import { VideoShareModel } from '../../models/video/video-share.js'
@ -12,16 +12,7 @@ import { checkUrlsSameHost, getLocalVideoAnnounceActivityPubUrl } from './url.js
const lTags = loggerTagsFactory('share') const lTags = loggerTagsFactory('share')
async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { export async function changeVideoChannelShare (
if (!video.hasPrivacyForFederation()) return undefined
return Promise.all([
shareByServer(video, t),
shareByVideoChannel(video, t)
])
}
async function changeVideoChannelShare (
video: MVideoAccountLight, video: MVideoAccountLight,
oldVideoChannel: MChannelActorLight, oldVideoChannel: MChannelActorLight,
t: Transaction t: Transaction
@ -36,7 +27,7 @@ async function changeVideoChannelShare (
await shareByVideoChannel(video, t) 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 => { await Bluebird.map(shareUrls, async shareUrl => {
try { try {
await addVideoShare(shareUrl, video) await addVideoShare(shareUrl, video)
@ -46,12 +37,44 @@ async function addVideoShares (shareUrls: string[], video: MVideoId) {
}, { concurrency: CRAWL_REQUEST_CONCURRENCY }) }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
} }
export { export async function shareByServer (video: MVideo, t: Transaction) {
changeVideoChannelShare, const serverActor = await getServerActor()
addVideoShares,
shareVideoByServerAndChannel 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) { async function addVideoShare (shareUrl: string, video: MVideoId) {
@ -74,42 +97,6 @@ async function addVideoShare (shareUrl: string, video: MVideoId) {
await VideoShareModel.upsert(entry) 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) { async function undoShareByVideoChannel (video: MVideo, oldVideoChannel: MChannelActorLight, t: Transaction) {
// Load old share // Load old share
const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t) 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 { fetchAP } from './activity.js'
import { getOrCreateAPActor } from './actors/index.js' import { getOrCreateAPActor } from './actors/index.js'
import { checkUrlsSameHost } from './url.js' import { checkUrlsSameHost } from './url.js'
import { getOrCreateAPVideo } from './videos/index.js' import { canVideoBeFederated, getOrCreateAPVideo } from './videos/index.js'
type ResolveThreadParams = { type ResolveThreadParams = {
url: string url: string
@ -92,8 +92,8 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false } const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false }
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam }) const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
if (video.isOwned() && !video.hasPrivacyForFederation()) { if (video.isOwned() && !canVideoBeFederated(video)) {
throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation') throw new Error('Cannot resolve thread of video that is not compatible with federation')
} }
let resultComment: MCommentOwnerVideo 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 { Transaction } from 'sequelize'
import { MVideoAP, MVideoAPLight } from '@server/types/models/index.js'
import { sendCreateVideo, sendUpdateVideo } from '../send/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) { export async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) {
const video = videoArg as MVideoAP if (!canVideoBeFederated(videoArg, isNewVideo)) return
if ( const video = await videoArg.lightAPToFullAP(transaction)
// 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)
if (isNewVideo) { if (isNewVideo) {
// Now we'll add the video's meta data to our followers // Now we'll add the video's meta data to our followers
await sendCreateVideo(video, transaction) await sendCreateVideo(video, transaction)
await shareVideoByServerAndChannel(video, transaction)
} else { await Promise.all([
await sendUpdateVideo(video, transaction) shareByServer(video, transaction),
} shareByVideoChannel(video, transaction)
])
} else {
await sendUpdateVideo(video, transaction)
} }
} }
export { export function canVideoBeFederated (video: MVideoWithBlacklistRights, isNewVideo = false) {
federateVideoIfNeeded // 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 { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { logger } from '@server/helpers/logger.js' import { logger } from '@server/helpers/logger.js'
import { JobQueue } from '@server/lib/job-queue/index.js' import { JobQueue } from '@server/lib/job-queue/index.js'
import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders/index.js' import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders/index.js'
import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models/index.js' import {
import { APObjectId } from '@peertube/peertube-models' MVideoAccountLightBlacklistAllFiles,
MVideoImmutable,
MVideoThumbnail,
MVideoThumbnailBlacklist
} from '@server/types/models/index.js'
import { getAPId } from '../activity.js' import { getAPId } from '../activity.js'
import { refreshVideoIfNeeded } from './refresh.js' import { refreshVideoIfNeeded } from './refresh.js'
import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared/index.js' import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared/index.js'
@ -24,23 +29,25 @@ type GetVideoParamAll = {
type GetVideoParamImmutable = { type GetVideoParamImmutable = {
videoObject: APObjectId videoObject: APObjectId
syncParam?: SyncParam syncParam?: SyncParam
fetchType: 'only-immutable-attributes' fetchType: 'unsafe-only-immutable-attributes'
allowRefresh: false allowRefresh: false
} }
type GetVideoParamOther = { type GetVideoParamOther = {
videoObject: APObjectId videoObject: APObjectId
syncParam?: SyncParam syncParam?: SyncParam
fetchType?: 'all' | 'only-video' fetchType?: 'all' | 'only-video-and-blacklist'
allowRefresh?: boolean allowRefresh?: boolean
} }
export function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles> export function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
export function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable> 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 ( export async function getOrCreateAPVideo (
options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> { ): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist | MVideoImmutable> {
// Default params // Default params
const syncParam = options.syncParam || { rates: true, shares: true, comments: true, refreshVideo: false } const syncParam = options.syncParam || { rates: true, shares: true, comments: true, refreshVideo: false }
const fetchType = options.fetchType || 'all' const fetchType = options.fetchType || 'all'
@ -52,7 +59,7 @@ export async function getOrCreateAPVideo (
if (videoFromDatabase) { if (videoFromDatabase) {
if (allowRefresh === true) { 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) 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: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
export function maybeGetOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable> 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) { export async function maybeGetOrCreateAPVideo (options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther) {
try { try {
const result = await getOrCreateAPVideo(options as any) const result = await getOrCreateAPVideo(options as any)

View File

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

View File

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

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