v6.1.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:
commit
29ac4fc084
118 changed files with 19921 additions and 9930 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,6 +1,6 @@
|
|||
# Changelog
|
||||
|
||||
## v6.1.0-rc.1
|
||||
## v6.1.0
|
||||
|
||||
### IMPORTANT NOTES
|
||||
|
||||
|
@ -11,6 +11,11 @@
|
|||
* Views use a *Session ID* generated by the web browser instead of using the request IP (former behavior can be restored in YAML config)
|
||||
* The goal of this change is to get closer to how other video platforms like Mux, Vimeo, or Instagram work
|
||||
|
||||
### SECURITY
|
||||
|
||||
* Compact ActivityPub JSON-LD objects before using them to prevent incorrect access control @tesaguri
|
||||
* Protect ActivityPub information related to private/internal/blocked videos
|
||||
|
||||
### Admin config (non-exhaustive)
|
||||
|
||||
* **Breaking changes**:
|
||||
|
@ -127,6 +132,9 @@
|
|||
* Fix view endpoint crash on geoip update failure
|
||||
* Fix setting video subtitle from URL query
|
||||
* Fix selecting "Display all languages/categories/licences" in videos search resulting in an empty search
|
||||
* Fix followers/following counter of local ActivityPub actors
|
||||
* Fix notification button link on mobile
|
||||
* Fix player subtitles on iOS
|
||||
|
||||
|
||||
## v6.0.4
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "6.1.0-rc.1",
|
||||
"version": "6.1.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
|
|
@ -216,7 +216,10 @@ export class VideoListComponent extends RestTable <Video> implements OnInit {
|
|||
|
||||
getFilesSize (video: Video) {
|
||||
let total = getAllFiles(video).reduce((p, f) => p += f.size, 0)
|
||||
total += video.videoSource?.size || 0
|
||||
|
||||
if (video.videoSource?.fileDownloadUrl) {
|
||||
total += video.videoSource.size || 0
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
|
|
@ -309,7 +309,7 @@ export class VideoStatsComponent implements OnInit {
|
|||
{
|
||||
label: $localize`Views`,
|
||||
value: this.numberFormatter.transform(this.video.views),
|
||||
help: $localize`A view means that someone watched the video for at least 30 seconds`
|
||||
help: $localize`A view means that someone watched the video for several seconds (10 seconds by default)`
|
||||
},
|
||||
{
|
||||
label: $localize`Likes`,
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
import { Subject, Subscription } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { Component, EventEmitter, Output, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { NavigationEnd, Router } from '@angular/router'
|
||||
import { Notifier, PeerTubeSocket, ScreenService } from '@app/core'
|
||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { UserNotificationsComponent } from '@app/shared/standalone-notifications/user-notifications.component'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'
|
||||
import { Notifier, PeerTubeSocket, ScreenService } from '@app/core'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { LoaderComponent } from '@app/shared/shared-main/loaders/loader.component'
|
||||
import { UserNotificationService } from '@app/shared/shared-main/users/user-notification.service'
|
||||
import { UserNotificationsComponent } from '@app/shared/standalone-notifications/user-notifications.component'
|
||||
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
@Component({
|
||||
selector: 'my-notification',
|
||||
templateUrl: './notification.component.html',
|
||||
styleUrls: [ './notification.component.scss' ],
|
||||
standalone: true,
|
||||
imports: [ CommonModule, NgbPopoverModule, UserNotificationsComponent, GlobalIconComponent, LoaderComponent ]
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgbPopoverModule,
|
||||
UserNotificationsComponent,
|
||||
GlobalIconComponent,
|
||||
LoaderComponent,
|
||||
RouterLink,
|
||||
RouterLinkActive
|
||||
]
|
||||
})
|
||||
export class NotificationComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('popover', { static: true }) popover: NgbPopover
|
||||
|
|
|
@ -361,7 +361,10 @@ export class PeerTubePlayer {
|
|||
|
||||
getVideojsOptions (): videojs.PlayerOptions {
|
||||
const html5 = {
|
||||
preloadTextTracks: false
|
||||
preloadTextTracks: false,
|
||||
// Prevent a bug on iOS where the text tracks added by peertube plugin are removed on play
|
||||
// See https://github.com/Chocobozzz/PeerTube/issues/6351
|
||||
nativeTextTracks: false
|
||||
}
|
||||
|
||||
const plugins: VideoJSPluginOptions = {
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<my-app role="main">
|
||||
<my-app>
|
||||
</my-app>
|
||||
|
||||
</body>
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file source-language="en-US" datatype="plaintext" original="ng2.template" target-language="ro">
|
||||
<body>
|
||||
|
@ -106,33 +106,33 @@
|
|||
<context context-type="linenumber">5</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.first-aria" datatype="html">
|
||||
<trans-unit id="ngb.pagination.first-aria" datatype="html" xml:space="preserve">
|
||||
<source>First</source>
|
||||
<target/>
|
||||
<target state="translated">Primul</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
|
||||
<context context-type="linenumber">14</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
|
||||
<trans-unit id="ngb.pagination.previous-aria" datatype="html" xml:space="preserve">
|
||||
<source>Previous</source>
|
||||
<target/>
|
||||
<target state="translated">Anterior</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
|
||||
<context context-type="linenumber">24</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.next-aria" datatype="html">
|
||||
<trans-unit id="ngb.pagination.next-aria" datatype="html" xml:space="preserve">
|
||||
<source>Next</source>
|
||||
<target/>
|
||||
<target state="translated">Următorul</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
|
||||
<context context-type="linenumber">44</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.pagination.last-aria" datatype="html">
|
||||
<trans-unit id="ngb.pagination.last-aria" datatype="html" xml:space="preserve">
|
||||
<source>Last</source>
|
||||
<target/>
|
||||
<target state="translated">Ultimul</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/pagination/pagination.d.ts</context>
|
||||
<context context-type="linenumber">53</context>
|
||||
|
@ -146,9 +146,9 @@
|
|||
<context context-type="linenumber">7</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
|
||||
<trans-unit id="ngb.timepicker.increment-hours" datatype="html" xml:space="preserve">
|
||||
<source>Increment hours</source>
|
||||
<target/>
|
||||
<target state="translated">Adaugă ore</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
|
||||
<context context-type="linenumber">9</context>
|
||||
|
@ -170,17 +170,17 @@
|
|||
<context context-type="linenumber">15</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
|
||||
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html" xml:space="preserve">
|
||||
<source>Decrement hours</source>
|
||||
<target/>
|
||||
<target state="translated">Scade ore</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
|
||||
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html" xml:space="preserve">
|
||||
<source>Increment minutes</source>
|
||||
<target/>
|
||||
<target state="translated">Adaugă minute</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
|
||||
<context context-type="linenumber">32</context>
|
||||
|
@ -202,17 +202,17 @@
|
|||
<context context-type="linenumber">37</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
|
||||
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html" xml:space="preserve">
|
||||
<source>Decrement minutes</source>
|
||||
<target/>
|
||||
<target state="translated">Scade minute</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
|
||||
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html" xml:space="preserve">
|
||||
<source>Increment seconds</source>
|
||||
<target/>
|
||||
<target state="translated">Adaugă secunde</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
|
||||
<context context-type="linenumber">54</context>
|
||||
|
@ -234,9 +234,9 @@
|
|||
<context context-type="linenumber">59</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
|
||||
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html" xml:space="preserve">
|
||||
<source>Decrement seconds</source>
|
||||
<target/>
|
||||
<target state="translated">Scade secunde</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">node_modules/@ng-bootstrap/ng-bootstrap/timepicker/timepicker.d.ts</context>
|
||||
<context context-type="linenumber">67</context>
|
||||
|
@ -298,17 +298,17 @@
|
|||
<context context-type="linenumber">32</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="066903c4bc7d397c799979d64ce8c450792eb664" datatype="html">
|
||||
<trans-unit id="066903c4bc7d397c799979d64ce8c450792eb664" datatype="html" xml:space="preserve">
|
||||
<source>Your video <x id="START_LINK" ctype="x-a" equiv-text="<a>"/><x id="INTERPOLATION" equiv-text="{{ notification.videoBlacklist.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> has been blacklisted </source>
|
||||
<target/>
|
||||
<target state="translated">Videoul tău <x id="START_LINK" ctype="x-a" equiv-text="<a>"/><x id="INTERPOLATION" equiv-text="{{ notification.videoBlacklist.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> a fost blocat. </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context>
|
||||
<context context-type="linenumber">40</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="b5e2152dd5b4222093fcec9c8289f12308a598e7" datatype="html">
|
||||
<trans-unit id="b5e2152dd5b4222093fcec9c8289f12308a598e7" datatype="html" xml:space="preserve">
|
||||
<source><x id="START_LINK" ctype="x-a" equiv-text="<a>"/>A new video abuse<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> has been created on video <x id="START_LINK_1" ctype="x-a" equiv-text="<a>"/><x id="INTERPOLATION" equiv-text="{{ notification.videoAbuse.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> </source>
|
||||
<target/>
|
||||
<target state="translated"><x id="START_LINK" ctype="x-a" equiv-text="<a>"/>A new video abuse<x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> has been created on video <x id="START_LINK_1" ctype="x-a" equiv-text="<a>"/><x id="INTERPOLATION" equiv-text="{{ notification.videoAbuse.video.name }}"/><x id="CLOSE_LINK" ctype="x-a" equiv-text="</a>"/> </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/users/user-notifications.component.html</context>
|
||||
<context context-type="linenumber">48</context>
|
||||
|
@ -438,9 +438,9 @@
|
|||
<context context-type="linenumber">42</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1394835141143590910" datatype="html">
|
||||
<trans-unit id="1394835141143590910" datatype="html" xml:space="preserve">
|
||||
<source>Start at</source>
|
||||
<target/>
|
||||
<target state="translated">Începe la</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context>
|
||||
<context context-type="linenumber">17</context>
|
||||
|
@ -454,9 +454,9 @@
|
|||
<context context-type="linenumber">75</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5964984095397511808" datatype="html">
|
||||
<trans-unit id="5964984095397511808" datatype="html" xml:space="preserve">
|
||||
<source>Stop at</source>
|
||||
<target/>
|
||||
<target state="translated">Oprește la</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/video-playlist/video-add-to-playlist.component.html</context>
|
||||
<context context-type="linenumber">31</context>
|
||||
|
@ -766,17 +766,17 @@
|
|||
<context context-type="linenumber">26</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8558962068274430520" datatype="html">
|
||||
<trans-unit id="8558962068274430520" datatype="html" xml:space="preserve">
|
||||
<source>Unfederate the video</source>
|
||||
<target/>
|
||||
<target state="translated">Decuplați videoclipul</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/video/modals/video-blacklist.component.html</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7539427273132299890" datatype="html">
|
||||
<trans-unit id="7539427273132299890" datatype="html" xml:space="preserve">
|
||||
<source>Unlisted</source>
|
||||
<target/>
|
||||
<target state="translated">Nelistat</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
|
||||
<context context-type="linenumber">6</context>
|
||||
|
@ -802,9 +802,9 @@
|
|||
<context context-type="linenumber">7</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7688104409544625220" datatype="html">
|
||||
<trans-unit id="7688104409544625220" datatype="html" xml:space="preserve">
|
||||
<source>{VAR_PLURAL, plural, =1 {1 view} other {<x id="INTERPOLATION" equiv-text="{{ video.views | myNumberFormatter }}"/> views} }</source>
|
||||
<target/>
|
||||
<target state="translated">{VAR_PLURAL, plural, =1 {1 view} other {<x id="INTERPOLATION" equiv-text="{{ video.views | myNumberFormatter }}"/> views} }</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
|
||||
<context context-type="linenumber">23</context>
|
||||
|
@ -818,9 +818,9 @@
|
|||
<context context-type="linenumber">41</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3514509630940272440" datatype="html">
|
||||
<trans-unit id="3514509630940272440" datatype="html" xml:space="preserve">
|
||||
<source>Sensitive</source>
|
||||
<target/>
|
||||
<target state="translated">Sensitiv</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/video/video-miniature.component.html</context>
|
||||
<context context-type="linenumber">45</context>
|
||||
|
@ -874,9 +874,9 @@
|
|||
<context context-type="linenumber">100</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5263519165976128456" datatype="html">
|
||||
<trans-unit id="5263519165976128456" datatype="html" xml:space="preserve">
|
||||
<source>Edit starts/stops at</source>
|
||||
<target/>
|
||||
<target state="translated">Editarea începe/oprește la</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/video-playlist/video-playlist-element-miniature.component.html</context>
|
||||
<context context-type="linenumber">50</context>
|
||||
|
@ -1182,9 +1182,9 @@
|
|||
<context context-type="linenumber">17</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="c590f63488a5179f20a46930cf5e673f8136673d" datatype="html">
|
||||
<trans-unit id="c590f63488a5179f20a46930cf5e673f8136673d" datatype="html" xml:space="preserve">
|
||||
<source>You can interact with this via any ActivityPub-capable fediverse instance.<x id="LINE_BREAK" ctype="lb" equiv-text="<br/>"/><x id="LINE_BREAK" ctype="lb" equiv-text="<br/>"/> 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="<br/>"/><x id="LINE_BREAK" ctype="lb" equiv-text="<br/>"/>De exemplu cu Mastodon sau Pleroma poți scrie URL-ul curent în cutia de căutare și interacționa cu el acolo. </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/user-subscription/remote-subscribe.component.html</context>
|
||||
<context context-type="linenumber">26</context>
|
||||
|
@ -1198,9 +1198,9 @@
|
|||
<context context-type="linenumber">5</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5975923297757530070" datatype="html">
|
||||
<trans-unit id="5975923297757530070" datatype="html" xml:space="preserve">
|
||||
<source><x id="START_TAG_DIV" ctype="x-div" equiv-text="<div>"/>Default NSFW/sensitive videos policy<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="</div>"/> <x id="START_TAG_DIV_1" ctype="x-div" equiv-text="<div>"/>can be redefined by the users<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="</div>"/> </source>
|
||||
<target/>
|
||||
<target state="translated"><x id="START_TAG_DIV" ctype="x-div" equiv-text="<div>"/>Predefinit NSFW/sensitive videoclipuri regulamentului<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="</div>"/><x id="START_TAG_DIV_1" ctype="x-div" equiv-text="<div>"/>poate fii redefinit de<x id="CLOSE_TAG_DIV" ctype="x-div" equiv-text="</div>"/>. </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
|
||||
<context context-type="linenumber">11</context>
|
||||
|
@ -1266,9 +1266,9 @@
|
|||
<context context-type="linenumber">139</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1502595455339510144" datatype="html">
|
||||
<trans-unit id="1502595455339510144" datatype="html" xml:space="preserve">
|
||||
<source>Unlimited <x id="START_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="<ng-container>"/>(<x id="INTERPOLATION" equiv-text="{{ dailyUserVideoQuota | bytes: 0 }}"/> per day)<x id="CLOSE_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="</ng-container>"/> </source>
|
||||
<target/>
|
||||
<target state="translated">Nelimitat<x id="START_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="<ng-container>"/><x id="INTERPOLATION" equiv-text="{{ dailyUserVideoQuota | bytes: 0 }}"/>pe zi<x id="CLOSE_TAG_NG-CONTAINER" ctype="x-ng-container" equiv-text="</ng-container>"/>/ </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
|
||||
<context context-type="linenumber">59</context>
|
||||
|
@ -1306,9 +1306,9 @@
|
|||
<context context-type="linenumber">77</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7683705529753923369" datatype="html">
|
||||
<trans-unit id="7683705529753923369" datatype="html" xml:space="preserve">
|
||||
<source>Player</source>
|
||||
<target/>
|
||||
<target state="translated">Jucător</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
|
@ -1334,9 +1334,9 @@
|
|||
<context context-type="linenumber">7</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8630916846096019339" datatype="html">
|
||||
<trans-unit id="8630916846096019339" datatype="html" xml:space="preserve">
|
||||
<source>Users can resolve distant content</source>
|
||||
<target/>
|
||||
<target state="translated">Utilizatorii pot accesa conținutul de la distanță</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/shared/instance/instance-features-table.component.html</context>
|
||||
<context context-type="linenumber">100</context>
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -4228,11 +4228,11 @@
|
|||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">281</context>
|
||||
<context context-type="linenumber">284</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">336</context>
|
||||
<context context-type="linenumber">339</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts</context>
|
||||
|
@ -6444,91 +6444,91 @@
|
|||
<source>Are you sure you want to delete this <x id="PH" equiv-text="file.resolution.label"/> file?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">225</context>
|
||||
<context context-type="linenumber">228</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6693349469471580292" datatype="html">
|
||||
<source>Delete file</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">226</context>
|
||||
<context context-type="linenumber">229</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7016764388104297354" datatype="html">
|
||||
<source>File removed.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">232</context>
|
||||
<context context-type="linenumber">235</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="925076027211452339" datatype="html">
|
||||
<source>Are you sure you want to delete the original file of this video?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">241</context>
|
||||
<context context-type="linenumber">244</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3014914668468316940" datatype="html">
|
||||
<source>Delete original file</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">242</context>
|
||||
<context context-type="linenumber">245</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6647462936549531405" datatype="html">
|
||||
<source>Original file removed.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">248</context>
|
||||
<context context-type="linenumber">251</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1314383205093440631" datatype="html">
|
||||
<source>Are you sure you want to delete {count, plural, =1 {this video} other {these <x id="count"/> videos}}?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">277</context>
|
||||
<context context-type="linenumber">280</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5779580280418408097" datatype="html">
|
||||
<source>Deleted {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">289</context>
|
||||
<context context-type="linenumber">292</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="9164541937317586242" datatype="html">
|
||||
<source>Unblocked {count, plural, =1 {1 video} other {<x id="count"/> videos}}.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">307</context>
|
||||
<context context-type="linenumber">310</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6228449077605046873" datatype="html">
|
||||
<source>Are you sure you want to delete {count, plural, =1 {1 HLS streaming playlist} other {<x id="count"/> HLS streaming playlists}}?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">325</context>
|
||||
<context context-type="linenumber">328</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4435640428611044716" datatype="html">
|
||||
<source>Are you sure you want to delete Web Video files of {count, plural, =1 {1 video} other {<x id="count"/> videos}}?</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">331</context>
|
||||
<context context-type="linenumber">334</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1571742433738679426" datatype="html">
|
||||
<source>Files were removed.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">342</context>
|
||||
<context context-type="linenumber">345</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7054344823477412274" datatype="html">
|
||||
<source>Transcoding jobs created.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/+admin/overview/videos/video-list.component.ts</context>
|
||||
<context context-type="linenumber">354</context>
|
||||
<context context-type="linenumber">357</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2591467977473302125" datatype="html">
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -14,51 +14,51 @@
|
|||
"Copy magnet URI": "Copiar lo magnet URI",
|
||||
"Total downloaded: ": "Total telecargat : ",
|
||||
"Total uploaded: ": "Total enviat : ",
|
||||
"From servers: ": "From servers: ",
|
||||
"From peers: ": "From peers: ",
|
||||
"From servers: ": "Dels servidors : ",
|
||||
"From peers: ": "Dels pars : ",
|
||||
"Normal mode": "Normal mode",
|
||||
"Stats for nerds": "Stats for nerds",
|
||||
"Theater mode": "Theater mode",
|
||||
"Video UUID": "Video UUID",
|
||||
"Viewport / Frames": "Viewport / Frames",
|
||||
"Resolution": "Resolution",
|
||||
"Stats for nerds": "Estatisticas pels nerds",
|
||||
"Theater mode": "Mòde cinèma",
|
||||
"Video UUID": "UUID de la vidèo",
|
||||
"Viewport / Frames": "Fenèstra d’afichatge / Quadres",
|
||||
"Resolution": "Resolucion",
|
||||
"Volume": "Volume",
|
||||
"Codecs": "Codecs",
|
||||
"Color": "Color",
|
||||
"Go back to the live": "Go back to the live",
|
||||
"Connection Speed": "Connection Speed",
|
||||
"Network Activity": "Network Activity",
|
||||
"Total Transfered": "Total Transfered",
|
||||
"Download Breakdown": "Download Breakdown",
|
||||
"Go back to the live": "Tornar al dirèct",
|
||||
"Connection Speed": "Velocitat de la connexion",
|
||||
"Network Activity": "Activitat ret",
|
||||
"Total Transfered": "Total Transferit",
|
||||
"Download Breakdown": "Reparticion dels telecargaments",
|
||||
"Buffer Progress": "Buffer Progress",
|
||||
"Buffer State": "Buffer State",
|
||||
"Live Latency": "Live Latency",
|
||||
"Buffer State": "Estat memòria tampon",
|
||||
"Live Latency": "Laténcia en viu",
|
||||
"P2P": "P2P",
|
||||
"{1} seconds": "{1} seconds",
|
||||
"enabled": "enabled",
|
||||
"{1} seconds": "{1} segondas",
|
||||
"enabled": "activat",
|
||||
"Playlist: {1}": "Playlist: {1}",
|
||||
"disabled": "disabled",
|
||||
"disabled": "desactivat",
|
||||
" off": " desactivats",
|
||||
"Player mode": "Player mode",
|
||||
"Play in loop": "Play in loop",
|
||||
"This live has not started yet.": "This live has not started yet.",
|
||||
"This live has ended.": "This live has ended.",
|
||||
"Play in loop": "Lector en bocla",
|
||||
"This live has not started yet.": "Lo dirècte a pas encara començat.",
|
||||
"This live has ended.": "Lo dirèct es acabat.",
|
||||
"The video failed to play, will try to fast forward.": "The video failed to play, will try to fast forward.",
|
||||
"{1} / {2} dropped of {3}": "{1} / {2} dropped of {3}",
|
||||
" (muted)": " (muted)",
|
||||
"{1} from servers · {2} from peers": "{1} from servers · {2} from peers",
|
||||
"Previous video": "Previous video",
|
||||
"Video page (new window)": "Video page (new window)",
|
||||
" (muted)": " (mut)",
|
||||
"{1} from servers · {2} from peers": "{1} dels servidors · {2} dels pars",
|
||||
"Previous video": "Vidèo precedenta",
|
||||
"Video page (new window)": "Pagina vidèo (fenèstra novèla)",
|
||||
"Next video": "Next video",
|
||||
"This video is password protected": "This video is password protected",
|
||||
"You need a password to watch this video.": "You need a password to watch this video.",
|
||||
"Incorrect password, please enter a correct password": "Incorrect password, please enter a correct password",
|
||||
"Cancel": "Cancel",
|
||||
"This video is password protected": "Un senhal protegís aquesta vidèo",
|
||||
"You need a password to watch this video.": "Devètz fornir un senhal per agachar aquesta vidèo.",
|
||||
"Incorrect password, please enter a correct password": "Senhal incorrècte, mercés de picar un senhal corrècte",
|
||||
"Cancel": "Anullar",
|
||||
"Up Next": "Up Next",
|
||||
"Autoplay is suspended": "Autoplay is suspended",
|
||||
"Autoplay is suspended": "La lectura automatica es suspenduda",
|
||||
"{1} (from edge: {2})": "{1} (from edge: {2})",
|
||||
"Disable subtitles": "Disable subtitles",
|
||||
"Enable {1} subtitle": "Enable {1} subtitle",
|
||||
"Disable subtitles": "Desactivar los sostítols",
|
||||
"Enable {1} subtitle": "Activar {1} sostítol",
|
||||
"Audio Player": "Lector àudio",
|
||||
"Video Player": "Lector vidèo",
|
||||
"Play": "Lectura",
|
||||
|
|
|
@ -272,5 +272,5 @@
|
|||
"Traditional Chinese": "Traditional Chinese",
|
||||
"Misc": "Divèrs",
|
||||
"Normal mode": "Normal mode",
|
||||
"Theater mode": "Theater mode"
|
||||
"Theater mode": "Mòde cinèma"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "peertube",
|
||||
"description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.",
|
||||
"version": "6.1.0-rc.1",
|
||||
"version": "6.1.0",
|
||||
"private": true,
|
||||
"licence": "AGPL-3.0",
|
||||
"engines": {
|
||||
|
@ -237,7 +237,7 @@
|
|||
"pngjs": "^7.0.0",
|
||||
"proxy": "^2.1.1",
|
||||
"socket.io-client": "^4.5.4",
|
||||
"supertest": "^6.0.1",
|
||||
"supertest": "^7.0.0",
|
||||
"swagger-cli": "^4.0.2",
|
||||
"tsc-watch": "^6.0.0",
|
||||
"tsx": "^4.7.1",
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
@ -3,12 +3,12 @@
|
|||
import { expect } from 'chai'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { sortObjectComparator } from '@peertube/peertube-core-utils'
|
||||
import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
|
||||
import {
|
||||
BlacklistCommand,
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow, PeerTubeServer,
|
||||
doubleFollow, makeActivityPubGetRequest, PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultChannelAvatar,
|
||||
waitJobs
|
||||
|
@ -298,6 +298,13 @@ describe('Test video blacklist', function () {
|
|||
expect(video4Blacklisted.unfederated).to.be.true
|
||||
})
|
||||
|
||||
it('Should not have AP comments/announces/likes/dislikes', async function () {
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/comments`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/announces`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/likes`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
await makeActivityPubGetRequest(servers[0].url, `/videos/watch/${video3UUID}/dislikes`, HttpStatusCode.UNAUTHORIZED_401)
|
||||
})
|
||||
|
||||
it('Should remove the video from blacklist and refederate the video', async function () {
|
||||
await command.remove({ videoId: video4UUID })
|
||||
|
||||
|
|
|
@ -165,15 +165,17 @@ describe('Test video source management', function () {
|
|||
expect(data[1].videoSource.fileDownloadUrl).to.exist
|
||||
|
||||
expect(data[2].videoSource).to.exist
|
||||
|
||||
expect(data[2].videoSource.fileDownloadUrl).to.not.exist
|
||||
|
||||
expect(data[2].videoSource.createdAt).to.exist
|
||||
expect(data[2].videoSource.fps).to.be.null
|
||||
expect(data[2].videoSource.height).to.be.null
|
||||
expect(data[2].videoSource.width).to.be.null
|
||||
expect(data[2].videoSource.resolution.id).to.be.null
|
||||
expect(data[2].videoSource.resolution.label).to.be.null
|
||||
expect(data[2].videoSource.size).to.be.null
|
||||
expect(data[2].videoSource.metadata).to.be.null
|
||||
expect(data[2].videoSource.fps).to.to.exist
|
||||
expect(data[2].videoSource.height).to.to.exist
|
||||
expect(data[2].videoSource.width).to.to.exist
|
||||
expect(data[2].videoSource.resolution.id).to.to.exist
|
||||
expect(data[2].videoSource.resolution.label).to.to.exist
|
||||
expect(data[2].videoSource.size).to.to.exist
|
||||
expect(data[2].videoSource.metadata).to.to.exist
|
||||
})
|
||||
|
||||
it('Should delete all videos and do not have original files anymore', async function () {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { signAndContextify } from '@peertube/peertube-server/core/helpers/activity-pub-utils.js'
|
||||
import { isHTTPSignatureVerified, parseHTTPSignature } from '@peertube/peertube-server/core/helpers/peertube-crypto.js'
|
||||
import { isJsonLDSignatureVerified, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
|
||||
import { compactJSONLDAndCheckSignature, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
|
||||
import { expect } from 'chai'
|
||||
import { readJsonSync } from 'fs-extra/esm'
|
||||
import cloneDeep from 'lodash-es/cloneDeep.js'
|
||||
|
@ -24,6 +24,10 @@ function fakeFilter () {
|
|||
return (data: any) => Promise.resolve(data)
|
||||
}
|
||||
|
||||
function fakeExpressReq (body: any) {
|
||||
return { body }
|
||||
}
|
||||
|
||||
describe('Test activity pub helpers', function () {
|
||||
|
||||
describe('When checking the Linked Signature', function () {
|
||||
|
@ -33,7 +37,7 @@ describe('Test activity pub helpers', function () {
|
|||
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
|
||||
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
|
||||
|
||||
const result = await isJsonLDSignatureVerified(fromActor as any, body)
|
||||
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
|
@ -43,7 +47,7 @@ describe('Test activity pub helpers', function () {
|
|||
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey
|
||||
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
|
||||
|
||||
const result = await isJsonLDSignatureVerified(fromActor as any, body)
|
||||
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
|
@ -53,7 +57,7 @@ describe('Test activity pub helpers', function () {
|
|||
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
|
||||
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
|
||||
|
||||
const result = await isJsonLDSignatureVerified(fromActor as any, body)
|
||||
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
|
||||
|
||||
expect(result).to.be.true
|
||||
})
|
||||
|
@ -72,7 +76,7 @@ describe('Test activity pub helpers', function () {
|
|||
})
|
||||
|
||||
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
|
||||
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
|
||||
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(signedBody))
|
||||
|
||||
expect(result).to.be.false
|
||||
})
|
||||
|
@ -91,7 +95,7 @@ describe('Test activity pub helpers', function () {
|
|||
})
|
||||
|
||||
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
|
||||
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
|
||||
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(signedBody))
|
||||
|
||||
expect(result).to.be.true
|
||||
})
|
||||
|
|
|
@ -120,7 +120,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
|
|||
activityPubClientRouter.get('/videos/watch/:id/announces',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoAnnouncesController)
|
||||
)
|
||||
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
|
||||
|
@ -132,19 +132,19 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
|
|||
activityPubClientRouter.get('/videos/watch/:id/likes',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoLikesController)
|
||||
)
|
||||
activityPubClientRouter.get('/videos/watch/:id/dislikes',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoDislikesController)
|
||||
)
|
||||
activityPubClientRouter.get('/videos/watch/:id/comments',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoCommentsController)
|
||||
)
|
||||
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
|
||||
|
@ -175,7 +175,7 @@ activityPubClientRouter.get('/videos/watch/:id/chapters',
|
|||
activityPubRateLimiter,
|
||||
apVideoChaptersSetCacheKey,
|
||||
chaptersCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video')),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(videoChaptersController)
|
||||
)
|
||||
|
||||
|
@ -330,7 +330,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
|
|||
}
|
||||
|
||||
async function videoAnnouncesController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyImmutableVideo
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
|
@ -347,7 +347,7 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
|
|||
}
|
||||
|
||||
async function videoLikesController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyImmutableVideo
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
|
@ -357,7 +357,7 @@ async function videoLikesController (req: express.Request, res: express.Response
|
|||
}
|
||||
|
||||
async function videoDislikesController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyImmutableVideo
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
|
@ -367,7 +367,7 @@ async function videoDislikesController (req: express.Request, res: express.Respo
|
|||
}
|
||||
|
||||
async function videoCommentsController (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyImmutableVideo
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
if (redirectIfNotOwned(video.url, res)) return
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { replaceChapters } from '@server/lib/video-chapters.js'
|
|||
const videoChaptersRouter = express.Router()
|
||||
|
||||
videoChaptersRouter.get('/:id/chapters',
|
||||
asyncMiddleware(videosCustomGetValidator('only-video')),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(listVideoChapters)
|
||||
)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import express from 'express'
|
||||
import { HttpStatusCode, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, VideoChangeOwnershipStatus } from '@peertube/peertube-models'
|
||||
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
|
||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
|
@ -113,7 +114,7 @@ function acceptOwnership (req: express.Request, res: express.Response) {
|
|||
const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
|
||||
targetVideoUpdated.VideoChannel = channel
|
||||
|
||||
if (targetVideoUpdated.hasPrivacyForFederation() && targetVideoUpdated.state === VideoState.PUBLISHED) {
|
||||
if (canVideoBeFederated(targetVideoUpdated)) {
|
||||
await changeVideoChannelShare(targetVideoUpdated, oldVideoChannel, t)
|
||||
await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor)
|
||||
}
|
||||
|
|
|
@ -68,12 +68,8 @@ async function deleteVideoLatestSourceFile (req: express.Request, res: express.R
|
|||
await video.removeOriginalFile(videoSource)
|
||||
|
||||
videoSource.keptOriginalFilename = null
|
||||
videoSource.fps = null
|
||||
videoSource.resolution = null
|
||||
videoSource.width = null
|
||||
videoSource.height = null
|
||||
videoSource.metadata = null
|
||||
videoSource.size = null
|
||||
videoSource.storage = null
|
||||
|
||||
await videoSource.save()
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
|
|
|
@ -7,7 +7,7 @@ const tokenRouter = express.Router()
|
|||
|
||||
tokenRouter.post('/:id/token',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(videosCustomGetValidator('only-video')),
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
videoFileTokenValidator,
|
||||
generateToken
|
||||
)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import express, { UploadFiles } from 'express'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ThumbnailType, VideoPrivacy, VideoPrivacyType, VideoUpdate } from '@peertube/peertube-models'
|
||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||
import { changeVideoChannelShare } from '@server/lib/activitypub/share.js'
|
||||
import { isNewVideoPrivacyForFederation, isPrivacyForFederation } from '@server/lib/activitypub/videos/federate.js'
|
||||
import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js'
|
||||
import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
|
||||
import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js'
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { setVideoPrivacy } from '@server/lib/video-privacy.js'
|
||||
import { setVideoTags } from '@server/lib/video.js'
|
||||
|
@ -11,7 +13,9 @@ import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
|||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { FilteredModelAttributes } from '@server/types/index.js'
|
||||
import { MVideoFullLight, MVideoThumbnail } from '@server/types/models/index.js'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
|
||||
import express, { UploadFiles } from 'express'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { VideoAuditView, auditLoggerFactory, getAuditIdFromRes } from '../../../helpers/audit-logger.js'
|
||||
import { resetSequelizeInstance } from '../../../helpers/database-utils.js'
|
||||
import { createReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
|
@ -22,9 +26,6 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist.js'
|
|||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares/index.js'
|
||||
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { replaceChaptersFromDescriptionIfNeeded } from '@server/lib/video-chapters.js'
|
||||
import { addVideoJobsAfterUpdate } from '@server/lib/video-jobs.js'
|
||||
import { updateLocalVideoMiniatureFromExisting } from '@server/lib/thumbnail.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video')
|
||||
const auditLogger = auditLoggerFactory('videos')
|
||||
|
@ -53,7 +54,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
|||
const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON())
|
||||
const videoInfoToUpdate: VideoUpdate = req.body
|
||||
|
||||
const hadPrivacyForFederation = videoFromReq.hasPrivacyForFederation()
|
||||
const hadPrivacyForFederation = isPrivacyForFederation(videoFromReq.privacy)
|
||||
const oldPrivacy = videoFromReq.privacy
|
||||
|
||||
const thumbnails = await buildVideoThumbnailsFromReq(videoFromReq, req.files)
|
||||
|
@ -192,7 +193,7 @@ async function updateVideoPrivacy (options: {
|
|||
transaction: Transaction
|
||||
}) {
|
||||
const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
|
||||
const isNewVideoForFederation = videoInstance.isNewVideoForFederation(videoInfoToUpdate.privacy)
|
||||
const isNewVideoForFederation = isNewVideoPrivacyForFederation(videoInstance.privacy, videoInfoToUpdate.privacy)
|
||||
|
||||
const newPrivacy = forceNumber(videoInfoToUpdate.privacy) as VideoPrivacyType
|
||||
setVideoPrivacy(videoInstance, newPrivacy)
|
||||
|
@ -208,7 +209,7 @@ async function updateVideoPrivacy (options: {
|
|||
}
|
||||
|
||||
// Unfederate the video if the new privacy is not compatible with federation
|
||||
if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
|
||||
if (hadPrivacyForFederation && !isPrivacyForFederation(videoInstance.privacy)) {
|
||||
await VideoModel.sendDelete(videoInstance, { transaction })
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { ContextType } from '@peertube/peertube-models'
|
||||
import { ACTIVITY_PUB, REMOTE_SCHEME } from '@server/initializers/constants.js'
|
||||
import { isArray } from './custom-validators/misc.js'
|
||||
import { buildDigest } from './peertube-crypto.js'
|
||||
import type { signJsonLDObject } from './peertube-jsonld.js'
|
||||
import { doJSONRequest } from './requests.js'
|
||||
import { isArray } from './custom-validators/misc.js'
|
||||
|
||||
export type ContextFilter = <T> (arg: T) => Promise<T>
|
||||
|
||||
|
@ -49,6 +49,18 @@ export async function getApplicationActorOfHost (host: string) {
|
|||
return found?.href || undefined
|
||||
}
|
||||
|
||||
export function getAPPublicValue () {
|
||||
return 'https://www.w3.org/ns/activitystreams#Public'
|
||||
}
|
||||
|
||||
export function hasAPPublic (toOrCC: string[]) {
|
||||
if (!isArray(toOrCC)) return false
|
||||
|
||||
const publicValue = getAPPublicValue()
|
||||
|
||||
return toOrCC.some(f => f === 'as:Public' || publicValue)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -58,7 +70,6 @@ type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string
|
|||
const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = {
|
||||
Video: buildContext({
|
||||
Hashtag: 'as:Hashtag',
|
||||
uuid: 'sc:identifier',
|
||||
category: 'sc:category',
|
||||
licence: 'sc:license',
|
||||
subtitleLanguage: 'sc:subtitleLanguage',
|
||||
|
@ -99,6 +110,11 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
|
|||
'@id': 'pt:aspectRatio'
|
||||
},
|
||||
|
||||
uuid: {
|
||||
'@type': 'sc:identifier',
|
||||
'@id': 'pt:uuid'
|
||||
},
|
||||
|
||||
originallyPublishedAt: 'sc:datePublished',
|
||||
|
||||
uploadDate: 'sc:uploadDate',
|
||||
|
@ -170,12 +186,23 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
|
|||
'@type': 'sc:Number',
|
||||
'@id': 'pt:stopTimestamp'
|
||||
},
|
||||
uuid: 'sc:identifier'
|
||||
uuid: {
|
||||
'@type': 'sc:identifier',
|
||||
'@id': 'pt:uuid'
|
||||
}
|
||||
}),
|
||||
|
||||
CacheFile: buildContext({
|
||||
expires: 'sc:expires',
|
||||
CacheFile: 'pt:CacheFile'
|
||||
CacheFile: 'pt:CacheFile',
|
||||
size: {
|
||||
'@type': 'sc:Number',
|
||||
'@id': 'pt:size'
|
||||
},
|
||||
fps: {
|
||||
'@type': 'sc:Number',
|
||||
'@id': 'pt:fps'
|
||||
}
|
||||
}),
|
||||
|
||||
Flag: buildContext({
|
||||
|
@ -205,15 +232,21 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
|
|||
'@type': 'sc:Number',
|
||||
'@id': 'pt:startTimestamp'
|
||||
},
|
||||
stopTimestamp: {
|
||||
endTimestamp: {
|
||||
'@type': 'sc:Number',
|
||||
'@id': 'pt:stopTimestamp'
|
||||
'@id': 'pt:endTimestamp'
|
||||
},
|
||||
watchSection: {
|
||||
'@type': 'sc:Number',
|
||||
'@id': 'pt:stopTimestamp'
|
||||
uuid: {
|
||||
'@type': 'sc:identifier',
|
||||
'@id': 'pt:uuid'
|
||||
},
|
||||
uuid: 'sc:identifier'
|
||||
actionStatus: 'sc:actionStatus',
|
||||
watchSections: {
|
||||
'@type': '@id',
|
||||
'@id': 'pt:watchSections'
|
||||
},
|
||||
addressRegion: 'sc:addressRegion',
|
||||
addressCountry: 'sc:addressCountry'
|
||||
}),
|
||||
|
||||
View: buildContext({
|
||||
|
@ -233,13 +266,46 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
|
|||
Rate: buildContext(),
|
||||
|
||||
Chapters: buildContext({
|
||||
name: 'sc:name',
|
||||
hasPart: 'sc:hasPart',
|
||||
endOffset: 'sc:endOffset',
|
||||
startOffset: 'sc:startOffset'
|
||||
})
|
||||
}
|
||||
|
||||
let allContext: (string | ContextValue)[]
|
||||
export function getAllContext () {
|
||||
if (allContext) return allContext
|
||||
|
||||
const processed = new Set<string>()
|
||||
allContext = []
|
||||
|
||||
let staticContext: ContextValue = {}
|
||||
|
||||
for (const v of Object.values(contextStore)) {
|
||||
for (const item of v) {
|
||||
if (typeof item === 'string') {
|
||||
if (!processed.has(item)) {
|
||||
allContext.push(item)
|
||||
}
|
||||
|
||||
processed.add(item)
|
||||
} else {
|
||||
for (const subKey of Object.keys(item)) {
|
||||
if (!processed.has(subKey)) {
|
||||
staticContext = { ...staticContext, [subKey]: item[subKey] }
|
||||
}
|
||||
|
||||
processed.add(subKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allContext = [ ...allContext, staticContext ]
|
||||
|
||||
return allContext
|
||||
}
|
||||
|
||||
async function getContextData (type: ContextType, contextFilter: ContextFilter) {
|
||||
const contextData = contextFilter
|
||||
? await contextFilter(contextStore[type])
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import jsonld from 'jsonld'
|
||||
|
||||
const CACHE = {
|
||||
const STATIC_CACHE = {
|
||||
'https://w3id.org/security/v1': {
|
||||
'@context': {
|
||||
id: '@id',
|
||||
|
@ -53,19 +53,29 @@ const CACHE = {
|
|||
}
|
||||
}
|
||||
|
||||
const localCache = new Map<string, any>()
|
||||
|
||||
const nodeDocumentLoader = (jsonld as any).documentLoaders.node();
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
(jsonld as any).documentLoader = (url) => {
|
||||
if (url in CACHE) {
|
||||
return Promise.resolve({
|
||||
(jsonld as any).documentLoader = async (url: string) => {
|
||||
if (url in STATIC_CACHE) {
|
||||
return {
|
||||
contextUrl: null,
|
||||
document: CACHE[url],
|
||||
document: STATIC_CACHE[url],
|
||||
documentUrl: url
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nodeDocumentLoader(url)
|
||||
if (localCache.has(url)) return localCache.get(url)
|
||||
|
||||
const remoteDoc = await nodeDocumentLoader(url)
|
||||
|
||||
if (localCache.size < 100) {
|
||||
localCache.set(url, remoteDoc)
|
||||
}
|
||||
|
||||
return remoteDoc
|
||||
}
|
||||
|
||||
export { jsonld }
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
import { CacheFileObject } from '@peertube/peertube-models'
|
||||
import { exists, isDateValid } from '../misc.js'
|
||||
import { MIMETYPES } from '@server/initializers/constants.js'
|
||||
import validator from 'validator'
|
||||
import { isDateValid } from '../misc.js'
|
||||
import { isActivityPubUrlValid } from './misc.js'
|
||||
import { isRemoteVideoUrlValid } from './videos.js'
|
||||
|
||||
function isCacheFileObjectValid (object: CacheFileObject) {
|
||||
return exists(object) &&
|
||||
object.type === 'CacheFile' &&
|
||||
(object.expires === null || isDateValid(object.expires)) &&
|
||||
export function isCacheFileObjectValid (object: CacheFileObject) {
|
||||
if (!object || object.type !== 'CacheFile') return false
|
||||
|
||||
return (!object.expires || isDateValid(object.expires)) &&
|
||||
isActivityPubUrlValid(object.object) &&
|
||||
(isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isCacheFileObjectValid
|
||||
(isRedundancyUrlVideoValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -24,3 +19,15 @@ function isPlaylistRedundancyUrlValid (url: any) {
|
|||
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
|
||||
isActivityPubUrlValid(url.href)
|
||||
}
|
||||
|
||||
// TODO: compat with < 6.1, use isRemoteVideoUrlValid instead in 7.0
|
||||
function isRedundancyUrlVideoValid (url: any) {
|
||||
const size = url.size || url['_:size']
|
||||
const fps = url.fps || url['_fps']
|
||||
|
||||
return MIMETYPES.AP_VIDEO.MIMETYPE_EXT[url.mediaType] &&
|
||||
isActivityPubUrlValid(url.href) &&
|
||||
validator.default.isInt(url.height + '', { min: 0 }) &&
|
||||
validator.default.isInt(size + '', { min: 0 }) &&
|
||||
(!fps || validator.default.isInt(fps + '', { min: -1 }))
|
||||
}
|
||||
|
|
|
@ -1,29 +1,25 @@
|
|||
import validator from 'validator'
|
||||
import { PlaylistElementObject, PlaylistObject } from '@peertube/peertube-models'
|
||||
import validator from 'validator'
|
||||
import { exists, isDateValid, isUUIDValid } from '../misc.js'
|
||||
import { isVideoPlaylistNameValid } from '../video-playlists.js'
|
||||
import { isActivityPubUrlValid } from './misc.js'
|
||||
|
||||
function isPlaylistObjectValid (object: PlaylistObject) {
|
||||
return exists(object) &&
|
||||
object.type === 'Playlist' &&
|
||||
validator.default.isInt(object.totalItems + '') &&
|
||||
export function isPlaylistObjectValid (object: PlaylistObject) {
|
||||
if (!object || object.type !== 'Playlist') return false
|
||||
|
||||
// TODO: compat with < 6.1, remove in 7.0
|
||||
if (!object.uuid && object['identifier']) object.uuid = object['identifier']
|
||||
|
||||
return validator.default.isInt(object.totalItems + '') &&
|
||||
isVideoPlaylistNameValid(object.name) &&
|
||||
isUUIDValid(object.uuid) &&
|
||||
isDateValid(object.published) &&
|
||||
isDateValid(object.updated)
|
||||
}
|
||||
|
||||
function isPlaylistElementObjectValid (object: PlaylistElementObject) {
|
||||
export function isPlaylistElementObjectValid (object: PlaylistElementObject) {
|
||||
return exists(object) &&
|
||||
object.type === 'PlaylistElement' &&
|
||||
validator.default.isInt(object.position + '') &&
|
||||
isActivityPubUrlValid(object.url)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
isPlaylistObjectValid,
|
||||
isPlaylistElementObjectValid
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
|
||||
import validator from 'validator'
|
||||
import { ACTIVITY_PUB } from '../../../initializers/constants.js'
|
||||
import { exists, isArray, isDateValid } from '../misc.js'
|
||||
import { isActivityPubUrlValid } from './misc.js'
|
||||
|
||||
|
@ -23,10 +23,7 @@ function sanitizeAndCheckVideoCommentObject (comment: any) {
|
|||
isDateValid(comment.published) &&
|
||||
isActivityPubUrlValid(comment.url) &&
|
||||
isArray(comment.to) &&
|
||||
(
|
||||
comment.to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ||
|
||||
comment.cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1
|
||||
) // Only accept public comments
|
||||
(hasAPPublic(comment.to) || hasAPPublic(comment.cc)) // Only accept public comments
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -27,7 +27,7 @@ function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
|
|||
sanitizeAndCheckVideoTorrentObject(activity.object)
|
||||
}
|
||||
|
||||
function sanitizeAndCheckVideoTorrentObject (video: any) {
|
||||
function sanitizeAndCheckVideoTorrentObject (video: VideoObject) {
|
||||
if (!video || video.type !== 'Video') return false
|
||||
|
||||
if (!setValidRemoteTags(video)) {
|
||||
|
@ -59,6 +59,9 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
|
|||
return false
|
||||
}
|
||||
|
||||
// TODO: compat with < 6.1, remove in 7.0
|
||||
if (!video.uuid && video['identifier']) video.uuid = video['identifier']
|
||||
|
||||
// Default attributes
|
||||
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
|
||||
if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { WatchActionObject } from '@peertube/peertube-models'
|
||||
import { exists, isDateValid, isUUIDValid } from '../misc.js'
|
||||
import { isDateValid, isUUIDValid } from '../misc.js'
|
||||
import { isVideoTimeValid } from '../video-view.js'
|
||||
import { isActivityPubVideoDurationValid, isObjectValid } from './misc.js'
|
||||
|
||||
function isWatchActionObjectValid (action: WatchActionObject) {
|
||||
return exists(action) &&
|
||||
action.type === 'WatchAction' &&
|
||||
isObjectValid(action.id) &&
|
||||
if (!action || action.type !== 'WatchAction') return false
|
||||
|
||||
// TODO: compat with < 6.1, remove in 7.0
|
||||
if (!action.uuid && action['identifier']) action.uuid = action['identifier']
|
||||
|
||||
if (action['_:actionStatus'] && !action.actionStatus) action.actionStatus = action['_:actionStatus']
|
||||
if (action['_:watchSections'] && !action.watchSections) action.watchSections = arrayify(action['_:watchSections'])
|
||||
|
||||
return isObjectValid(action.id) &&
|
||||
isActivityPubVideoDurationValid(action.duration) &&
|
||||
isDateValid(action.startTime) &&
|
||||
isDateValid(action.endTime) &&
|
||||
isLocationValid(action.location) &&
|
||||
isUUIDValid(action.uuid) &&
|
||||
isObjectValid(action.object) &&
|
||||
isWatchSectionsValid(action.watchSections)
|
||||
areWatchSectionsValid(action.watchSections)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -34,8 +41,11 @@ function isLocationValid (location: any) {
|
|||
return true
|
||||
}
|
||||
|
||||
function isWatchSectionsValid (sections: WatchActionObject['watchSections']) {
|
||||
function areWatchSectionsValid (sections: WatchActionObject['watchSections']) {
|
||||
return Array.isArray(sections) && sections.every(s => {
|
||||
// TODO: compat with < 6.1, remove in 7.0
|
||||
if (s['_:endTimestamp'] && !s.endTimestamp) s.endTimestamp = s['_:endTimestamp']
|
||||
|
||||
return isVideoTimeValid(s.startTimestamp) && isVideoTimeValid(s.endTimestamp)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,51 @@
|
|||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { sha256 } from '@peertube/peertube-node-utils'
|
||||
import { createSign, createVerify } from 'crypto'
|
||||
import cloneDeep from 'lodash-es/cloneDeep.js'
|
||||
import { MActor } from '../types/models/index.js'
|
||||
import { getAllContext } from './activity-pub-utils.js'
|
||||
import { jsonld } from './custom-jsonld-signature.js'
|
||||
import { isArray } from './custom-validators/misc.js'
|
||||
import { logger } from './logger.js'
|
||||
import { assertIsInWorkerThread } from './threads.js'
|
||||
import { jsonld } from './custom-jsonld-signature.js'
|
||||
|
||||
export function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
|
||||
if (signedDocument.signature.type === 'RsaSignature2017') {
|
||||
return isJsonLDRSA2017Verified(fromActor, signedDocument)
|
||||
type ExpressRequest = { body: any }
|
||||
|
||||
export function compactJSONLDAndCheckSignature (fromActor: MActor, req: ExpressRequest): Promise<boolean> {
|
||||
if (req.body.signature.type === 'RsaSignature2017') {
|
||||
return compactJSONLDAndCheckRSA2017Signature(fromActor, req)
|
||||
}
|
||||
|
||||
logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument)
|
||||
logger.warn('Unknown JSON LD signature %s.', req.body.signature.type, req.body)
|
||||
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
// Backward compatibility with "other" implementations
|
||||
export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) {
|
||||
export async function compactJSONLDAndCheckRSA2017Signature (fromActor: MActor, req: ExpressRequest) {
|
||||
const compacted = await jsonldCompact(omit(req.body, [ 'signature' ]))
|
||||
|
||||
fixCompacted(req.body, compacted)
|
||||
|
||||
req.body = { ...compacted, signature: req.body.signature }
|
||||
|
||||
if (compacted['@include']) {
|
||||
logger.warn('JSON-LD @include is not supported')
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: compat with < 6.1, remove in 7.0
|
||||
let safe = true
|
||||
if (
|
||||
(compacted.type === 'Create' && (compacted?.object?.type === 'WatchAction' || compacted?.object?.type === 'CacheFile')) ||
|
||||
(compacted.type === 'Undo' && compacted?.object?.type === 'Create' && compacted?.object?.object.type === 'CacheFile')
|
||||
) {
|
||||
safe = false
|
||||
}
|
||||
|
||||
const [ documentHash, optionsHash ] = await Promise.all([
|
||||
createDocWithoutSignatureHash(signedDocument),
|
||||
createSignatureHash(signedDocument.signature)
|
||||
hashObject(compacted, safe),
|
||||
createSignatureHash(req.body.signature, safe)
|
||||
])
|
||||
|
||||
const toVerify = optionsHash + documentHash
|
||||
|
@ -28,7 +53,39 @@ export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument
|
|||
const verify = createVerify('RSA-SHA256')
|
||||
verify.update(toVerify, 'utf8')
|
||||
|
||||
return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
|
||||
return verify.verify(fromActor.publicKey, req.body.signature.signatureValue, 'base64')
|
||||
}
|
||||
|
||||
function fixCompacted (original: any, compacted: any) {
|
||||
if (!original || !compacted) return
|
||||
|
||||
for (const [ k, v ] of Object.entries(original)) {
|
||||
if (k === '@context' || k === 'signature') continue
|
||||
if (v === undefined || v === null) continue
|
||||
|
||||
const cv = compacted[k]
|
||||
if (cv === undefined || cv === null) continue
|
||||
|
||||
if (typeof v === 'string') {
|
||||
if (v === 'https://www.w3.org/ns/activitystreams#Public' && cv === 'as:Public') {
|
||||
compacted[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if (isArray(v) && !isArray(cv)) {
|
||||
compacted[k] = [ cv ]
|
||||
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
if (v[i] === 'https://www.w3.org/ns/activitystreams#Public' && cv[i] === 'as:Public') {
|
||||
compacted[k][i] = v[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof v === 'object') {
|
||||
fixCompacted(original[k], compacted[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function signJsonLDObject <T> (options: {
|
||||
|
@ -66,35 +123,40 @@ export async function signJsonLDObject <T> (options: {
|
|||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function hashObject (obj: any): Promise<any> {
|
||||
const res = await (jsonld as any).promises.normalize(obj, {
|
||||
safe: false,
|
||||
algorithm: 'URDNA2015',
|
||||
format: 'application/n-quads'
|
||||
})
|
||||
async function hashObject (obj: any, safe: boolean): Promise<any> {
|
||||
const res = await jsonldNormalize(obj, safe)
|
||||
|
||||
return sha256(res)
|
||||
}
|
||||
|
||||
function createSignatureHash (signature: any) {
|
||||
const signatureCopy = cloneDeep(signature)
|
||||
Object.assign(signatureCopy, {
|
||||
function jsonldCompact (obj: any) {
|
||||
return (jsonld as any).promises.compact(obj, getAllContext())
|
||||
}
|
||||
|
||||
function jsonldNormalize (obj: any, safe: boolean) {
|
||||
return (jsonld as any).promises.normalize(obj, {
|
||||
safe,
|
||||
algorithm: 'URDNA2015',
|
||||
format: 'application/n-quads'
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createSignatureHash (signature: any, safe = true) {
|
||||
return hashObject({
|
||||
'@context': [
|
||||
'https://w3id.org/security/v1',
|
||||
{ RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
|
||||
]
|
||||
})
|
||||
],
|
||||
|
||||
delete signatureCopy.type
|
||||
delete signatureCopy.id
|
||||
delete signatureCopy.signatureValue
|
||||
|
||||
return hashObject(signatureCopy)
|
||||
...omit(signature, [ 'type', 'id', 'signatureValue' ])
|
||||
}, safe)
|
||||
}
|
||||
|
||||
function createDocWithoutSignatureHash (doc: any) {
|
||||
const docWithoutSignature = cloneDeep(doc)
|
||||
delete docWithoutSignature.signature
|
||||
|
||||
return hashObject(docWithoutSignature)
|
||||
return hashObject(docWithoutSignature, true)
|
||||
}
|
||||
|
|
|
@ -1,51 +1,28 @@
|
|||
import { Response } from 'express'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models'
|
||||
import { VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models/index.js'
|
||||
import { Response } from 'express'
|
||||
|
||||
function getVideoWithAttributes (res: Response) {
|
||||
export function getVideoWithAttributes (res: Response) {
|
||||
return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo
|
||||
}
|
||||
|
||||
function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
|
||||
export function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
|
||||
return isStreamingPlaylist(videoOrPlaylist)
|
||||
? videoOrPlaylist.Video
|
||||
: videoOrPlaylist
|
||||
}
|
||||
|
||||
function isPrivacyForFederation (privacy: VideoPrivacyType) {
|
||||
const castedPrivacy = forceNumber(privacy)
|
||||
|
||||
return castedPrivacy === VideoPrivacy.PUBLIC ||
|
||||
(CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED)
|
||||
}
|
||||
|
||||
function isStateForFederation (state: VideoStateType) {
|
||||
const castedState = forceNumber(state)
|
||||
|
||||
return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED
|
||||
}
|
||||
|
||||
function getPrivaciesForFederation () {
|
||||
export function getPrivaciesForFederation () {
|
||||
return (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true)
|
||||
? [ { privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.UNLISTED } ]
|
||||
: [ { privacy: VideoPrivacy.PUBLIC } ]
|
||||
}
|
||||
|
||||
function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) {
|
||||
export function getExtFromMimetype (mimeTypes: { [id: string]: string | string[] }, mimeType: string) {
|
||||
const value = mimeTypes[mimeType]
|
||||
|
||||
if (Array.isArray(value)) return value[0]
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export {
|
||||
getVideoWithAttributes,
|
||||
extractVideo,
|
||||
getExtFromMimetype,
|
||||
isStateForFederation,
|
||||
isPrivacyForFederation,
|
||||
getPrivaciesForFederation
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ import { cpus } from 'os'
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 830
|
||||
const LAST_MIGRATION_VERSION = 835
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -775,7 +775,6 @@ const ACTIVITY_PUB = {
|
|||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
],
|
||||
ACCEPT_HEADER: 'application/activity+json, application/ld+json',
|
||||
PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
COLLECTION_ITEMS_PER_PAGE: 10,
|
||||
FETCH_PAGE_LIMIT: 2000,
|
||||
MAX_RECURSION_COMMENTS: 100,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
import { ActivityAudience } from '@peertube/peertube-models'
|
||||
import { ACTIVITY_PUB } from '../../initializers/constants.js'
|
||||
import { getAPPublicValue } from '@server/helpers/activity-pub-utils.js'
|
||||
import { MActorFollowersUrl } from '../../types/models/index.js'
|
||||
|
||||
function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
|
||||
export function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
|
||||
return buildAudience([ actorSender.followersUrl ], isPublic)
|
||||
}
|
||||
|
||||
function buildAudience (followerUrls: string[], isPublic = true) {
|
||||
export function buildAudience (followerUrls: string[], isPublic = true) {
|
||||
let to: string[] = []
|
||||
let cc: string[] = []
|
||||
|
||||
if (isPublic) {
|
||||
to = [ ACTIVITY_PUB.PUBLIC ]
|
||||
to = [ getAPPublicValue() ]
|
||||
cc = followerUrls
|
||||
} else { // Unlisted
|
||||
to = []
|
||||
|
@ -21,14 +21,6 @@ function buildAudience (followerUrls: string[], isPublic = true) {
|
|||
return { to, cc }
|
||||
}
|
||||
|
||||
function audiencify<T> (object: T, audience: ActivityAudience) {
|
||||
export function audiencify<T> (object: T, audience: ActivityAudience) {
|
||||
return { ...audience, ...object }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
buildAudience,
|
||||
getAudience,
|
||||
audiencify
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Transaction } from 'sequelize'
|
|||
import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models/index.js'
|
||||
import { CacheFileObject, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy.js'
|
||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||
|
||||
async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
|
||||
const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
|
||||
|
@ -65,11 +66,15 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
|
|||
}
|
||||
|
||||
const url = cacheFileObject.url
|
||||
const urlFPS = exists(url.fps) // TODO: compat with < 6.1, remove in 7.0
|
||||
? url.fps
|
||||
: url['_:fps']
|
||||
|
||||
const videoFile = video.VideoFiles.find(f => {
|
||||
return f.resolution === url.height && f.fps === url.fps
|
||||
return f.resolution === url.height && f.fps === urlFPS
|
||||
})
|
||||
|
||||
if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`)
|
||||
if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${urlFPS} of video ${video.url}`)
|
||||
|
||||
return {
|
||||
expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Activity } from '@peertube/peertube-models'
|
|||
import { StatsManager } from '../stat-manager.js'
|
||||
import { processActivities } from './process/index.js'
|
||||
|
||||
class InboxManager {
|
||||
export class InboxManager {
|
||||
|
||||
private static instance: InboxManager
|
||||
private readonly inboxQueue: PQueue
|
||||
|
@ -39,9 +39,3 @@ class InboxManager {
|
|||
return this.instance || (this.instance = new this())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
InboxManager
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ async function buildElementsDBAttributes (elementUrls: string[], playlist: MVide
|
|||
try {
|
||||
const { elementObject } = await fetchRemotePlaylistElement(elementUrl)
|
||||
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video' })
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video-and-blacklist' })
|
||||
|
||||
elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video))
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { ACTIVITY_PUB } from '@server/initializers/constants.js'
|
||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
|
||||
import { MVideoId, MVideoPlaylistId } from '@server/types/models/index.js'
|
||||
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
||||
import { PlaylistElementObject, PlaylistObject, VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
||||
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
||||
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
|
||||
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
|
||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { MVideoId, MVideoPlaylistId } from '@server/types/models/index.js'
|
||||
|
||||
function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) {
|
||||
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
|
||||
export function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) {
|
||||
const privacy = hasAPPublic(to)
|
||||
? VideoPlaylistPrivacy.PUBLIC
|
||||
: VideoPlaylistPrivacy.UNLISTED
|
||||
|
||||
|
@ -23,7 +23,11 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: strin
|
|||
} as AttributesOnly<VideoPlaylistModel>
|
||||
}
|
||||
|
||||
function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) {
|
||||
export function playlistElementObjectToDBAttributes (
|
||||
elementObject: PlaylistElementObject,
|
||||
videoPlaylist: MVideoPlaylistId,
|
||||
video: MVideoId
|
||||
) {
|
||||
return {
|
||||
position: elementObject.position,
|
||||
url: elementObject.id,
|
||||
|
@ -33,8 +37,3 @@ function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObje
|
|||
videoId: video.id
|
||||
} as AttributesOnly<VideoPlaylistElementModel>
|
||||
}
|
||||
|
||||
export {
|
||||
playlistObjectToDBAttributes,
|
||||
playlistElementObjectToDBAttributes
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { createOrUpdateLocalVideoViewer } from '../local-video-viewer.js'
|
|||
import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
|
||||
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
|
||||
import { resolveThread } from '../video-comments.js'
|
||||
import { getOrCreateAPVideo } from '../videos/index.js'
|
||||
import { canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js'
|
||||
|
||||
async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -87,6 +87,11 @@ async function processCreateCacheFile (
|
|||
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
|
||||
|
||||
if (video.isOwned() && !canVideoBeFederated(video)) {
|
||||
logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`)
|
||||
return
|
||||
}
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
return createOrUpdateCacheFile(cacheFile, video, byActor, t)
|
||||
})
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { ActivityDislike } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { retryTransactionWrapper } from '../../../helpers/database-utils.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
|
||||
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
|
||||
import { MActorSignature } from '../../../types/models/index.js'
|
||||
import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
|
||||
import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
|
||||
|
||||
async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -21,14 +22,19 @@ export {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function processDislike (activity: ActivityDislike, byActor: MActorSignature) {
|
||||
const dislikeObject = activity.object
|
||||
const videoUrl = activity.object
|
||||
const byAccount = byActor.Account
|
||||
|
||||
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
|
||||
|
||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' })
|
||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
||||
if (!onlyVideo?.isOwned()) return
|
||||
|
||||
if (!canVideoBeFederated(onlyVideo)) {
|
||||
logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`)
|
||||
return
|
||||
}
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const video = await VideoModel.loadFull(onlyVideo.id, t)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ActivityLike } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { retryTransactionWrapper } from '../../../helpers/database-utils.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
|
@ -6,7 +7,7 @@ import { getAPId } from '../../../lib/activitypub/activity.js'
|
|||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
|
||||
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
|
||||
import { MActorSignature } from '../../../types/models/index.js'
|
||||
import { federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
|
||||
import { canVideoBeFederated, federateVideoIfNeeded, maybeGetOrCreateAPVideo } from '../videos/index.js'
|
||||
|
||||
async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -28,9 +29,14 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
|
|||
const byAccount = byActor.Account
|
||||
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
|
||||
|
||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' })
|
||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
||||
if (!onlyVideo?.isOwned()) return
|
||||
|
||||
if (!canVideoBeFederated(onlyVideo)) {
|
||||
logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`)
|
||||
return
|
||||
}
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const video = await VideoModel.loadFull(onlyVideo.id, t)
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { APActorUpdater } from '../actors/updater.js'
|
|||
import { createOrUpdateCacheFile } from '../cache-file.js'
|
||||
import { createOrUpdateVideoPlaylist } from '../playlists/index.js'
|
||||
import { forwardVideoRelatedActivity } from '../send/shared/send-utils.js'
|
||||
import { APVideoUpdater, getOrCreateAPVideo } from '../videos/index.js'
|
||||
import { APVideoUpdater, canVideoBeFederated, getOrCreateAPVideo } from '../videos/index.js'
|
||||
|
||||
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -93,6 +93,11 @@ async function processUpdateCacheFile (
|
|||
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
|
||||
|
||||
if (video.isOwned() && !canVideoBeFederated(video)) {
|
||||
logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`)
|
||||
return
|
||||
}
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
|
||||
})
|
||||
|
|
|
@ -24,7 +24,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
|
|||
|
||||
const { video } = await getOrCreateAPVideo({
|
||||
videoObject,
|
||||
fetchType: 'only-video',
|
||||
fetchType: 'only-video-and-blacklist',
|
||||
allowRefresh: false
|
||||
})
|
||||
|
||||
|
@ -32,8 +32,8 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
|
|||
video,
|
||||
viewerId: activity.id,
|
||||
|
||||
viewerExpires: activity.expires
|
||||
? new Date(activity.expires)
|
||||
viewerExpires: getExpires(activity)
|
||||
? new Date(getExpires(activity))
|
||||
: undefined,
|
||||
viewerResultCounter: getViewerResultCounter(activity)
|
||||
})
|
||||
|
@ -49,10 +49,15 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
|
|||
function getViewerResultCounter (activity: ActivityView) {
|
||||
const result = activity.result
|
||||
|
||||
if (!activity.expires || result?.interactionType !== 'WatchAction' || result?.type !== 'InteractionCounter') return undefined
|
||||
if (!getExpires(activity) || result?.interactionType !== 'WatchAction' || result?.type !== 'InteractionCounter') return undefined
|
||||
|
||||
const counter = parseInt(result.userInteractionCount + '')
|
||||
if (isNaN(counter)) return undefined
|
||||
|
||||
return counter
|
||||
}
|
||||
|
||||
// TODO: compat with < 6.1, remove in 7.0
|
||||
function getExpires (activity: ActivityView) {
|
||||
return activity.expires || activity['expiration'] as string
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Act
|
|||
View: processViewActivity
|
||||
}
|
||||
|
||||
async function processActivities (
|
||||
export async function processActivities (
|
||||
activities: Activity[],
|
||||
options: {
|
||||
signatureActor?: MActorSignature
|
||||
|
@ -86,7 +86,3 @@ async function processActivities (
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
processActivities
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { Transaction } from 'sequelize'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import {
|
||||
ActivityAudience,
|
||||
ActivityCreate,
|
||||
|
@ -9,19 +7,24 @@ import {
|
|||
VideoPlaylistPrivacy,
|
||||
VideoPrivacy
|
||||
} from '@peertube/peertube-models'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
import { VideoCommentModel } from '../../../models/video/video-comment.js'
|
||||
import {
|
||||
MActorLight,
|
||||
MCommentOwnerVideo,
|
||||
MLocalVideoViewerWithWatchSections,
|
||||
MVideoAccountLight,
|
||||
MVideoAP,
|
||||
MVideoAccountLight,
|
||||
MVideoPlaylistFull,
|
||||
MVideoRedundancyFileVideo,
|
||||
MVideoRedundancyStreamingPlaylistVideo
|
||||
} from '../../../types/models/index.js'
|
||||
import { audiencify, getAudience } from '../audience.js'
|
||||
import { canVideoBeFederated } from '../videos/federate.js'
|
||||
import {
|
||||
broadcastToActors,
|
||||
broadcastToFollowers,
|
||||
|
@ -32,12 +35,11 @@ import {
|
|||
sendVideoRelatedActivity,
|
||||
unicastTo
|
||||
} from './shared/index.js'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
|
||||
const lTags = loggerTagsFactory('ap', 'create')
|
||||
|
||||
async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
|
||||
if (!video.hasPrivacyForFederation()) return undefined
|
||||
export async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
|
||||
if (!canVideoBeFederated(video)) return undefined
|
||||
|
||||
logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
|
||||
|
||||
|
@ -56,7 +58,7 @@ async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
|
|||
})
|
||||
}
|
||||
|
||||
async function sendCreateCacheFile (
|
||||
export async function sendCreateCacheFile (
|
||||
byActor: MActorLight,
|
||||
video: MVideoAccountLight,
|
||||
fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
|
||||
|
@ -72,7 +74,7 @@ async function sendCreateCacheFile (
|
|||
})
|
||||
}
|
||||
|
||||
async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) {
|
||||
export async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) {
|
||||
logger.info('Creating job to send create watch action %s.', stats.url, lTags(stats.uuid))
|
||||
|
||||
const byActor = await getServerActor()
|
||||
|
@ -84,7 +86,7 @@ async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections,
|
|||
return sendVideoActivityToOrigin(activityBuilder, { byActor, video: stats.Video, transaction, contextType: 'WatchAction' })
|
||||
}
|
||||
|
||||
async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) {
|
||||
export async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) {
|
||||
if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
|
||||
|
||||
logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
|
||||
|
@ -109,11 +111,20 @@ async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transactio
|
|||
})
|
||||
}
|
||||
|
||||
async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) {
|
||||
logger.info('Creating job to send comment %s.', comment.url)
|
||||
|
||||
export async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) {
|
||||
const isOrigin = comment.Video.isOwned()
|
||||
|
||||
if (isOrigin) {
|
||||
const videoWithBlacklist = await VideoModel.loadWithBlacklist(comment.Video.id)
|
||||
|
||||
if (!canVideoBeFederated(videoWithBlacklist)) {
|
||||
logger.debug(`Do not send comment ${comment.url} on a video that cannot be federated`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Creating job to send comment %s.', comment.url)
|
||||
|
||||
const byActor = comment.Account.Actor
|
||||
const videoAccount = await AccountModel.load(comment.Video.VideoChannel.Account.id, transaction)
|
||||
|
||||
|
@ -179,7 +190,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction:
|
|||
})
|
||||
}
|
||||
|
||||
function buildCreateActivity <T extends ActivityCreateObject> (
|
||||
export function buildCreateActivity <T extends ActivityCreateObject> (
|
||||
url: string,
|
||||
byActor: MActorLight,
|
||||
object: T,
|
||||
|
@ -201,16 +212,7 @@ function buildCreateActivity <T extends ActivityCreateObject> (
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
sendCreateVideo,
|
||||
buildCreateActivity,
|
||||
sendCreateVideoComment,
|
||||
sendCreateVideoPlaylist,
|
||||
sendCreateCacheFile,
|
||||
sendCreateWatchAction
|
||||
}
|
||||
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function sendVideoRelatedCreateActivity (options: {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Transaction } from 'sequelize'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { AccountModel } from '../../../models/account/account.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { VideoShareModel } from '../../../models/video/video-share.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import {
|
||||
MAccountDefault,
|
||||
MActor,
|
||||
|
@ -16,11 +16,12 @@ import {
|
|||
} from '../../../types/models/index.js'
|
||||
import { audiencify, getAudience } from '../audience.js'
|
||||
import { getUpdateActivityPubUrl } from '../url.js'
|
||||
import { canVideoBeFederated } from '../videos/federate.js'
|
||||
import { getActorsInvolvedInVideo } from './shared/index.js'
|
||||
import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils.js'
|
||||
|
||||
async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) {
|
||||
if (!videoArg.hasPrivacyForFederation()) return undefined
|
||||
export async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) {
|
||||
if (!canVideoBeFederated(videoArg)) return undefined
|
||||
|
||||
const video = await videoArg.lightAPToFullAP(transaction)
|
||||
|
||||
|
@ -47,7 +48,7 @@ async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transactio
|
|||
})
|
||||
}
|
||||
|
||||
async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) {
|
||||
export async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) {
|
||||
const byActor = accountOrChannel.Actor
|
||||
|
||||
logger.info('Creating job to update actor %s.', byActor.url)
|
||||
|
@ -77,7 +78,7 @@ async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefa
|
|||
})
|
||||
}
|
||||
|
||||
async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) {
|
||||
export async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) {
|
||||
logger.info('Creating job to update cache file %s.', redundancyModel.url)
|
||||
|
||||
const associatedVideo = redundancyModel.getVideo()
|
||||
|
@ -98,7 +99,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide
|
|||
return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' })
|
||||
}
|
||||
|
||||
async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) {
|
||||
export async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) {
|
||||
if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
|
||||
|
||||
const byActor = videoPlaylist.OwnerAccount.Actor
|
||||
|
@ -127,14 +128,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, trans
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
sendUpdateActor,
|
||||
sendUpdateVideo,
|
||||
sendUpdateCacheFile,
|
||||
sendUpdateVideoPlaylist
|
||||
}
|
||||
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildUpdateActivity (
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import { Transaction } from 'sequelize'
|
||||
import { ACTIVITY_PUB } from '@server/initializers/constants.js'
|
||||
import { ActorModel } from '@server/models/actor/actor.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { VideoShareModel } from '@server/models/video/video-share.js'
|
||||
import { MActorFollowersUrl, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models/index.js'
|
||||
import { ActivityAudience } from '@peertube/peertube-models'
|
||||
import { getAPPublicValue } from '@server/helpers/activity-pub-utils.js'
|
||||
import { ActorModel } from '@server/models/actor/actor.js'
|
||||
import { VideoShareModel } from '@server/models/video/video-share.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MActorFollowersUrl, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models/index.js'
|
||||
import { Transaction } from 'sequelize'
|
||||
|
||||
function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
|
||||
export function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
|
||||
return {
|
||||
to: [ accountActor.url ],
|
||||
cc: actorsInvolvedInVideo.map(a => a.followersUrl)
|
||||
}
|
||||
}
|
||||
|
||||
function getVideoCommentAudience (
|
||||
export function getVideoCommentAudience (
|
||||
videoComment: MCommentOwnerVideo,
|
||||
threadParentComments: MCommentOwner[],
|
||||
actorsInvolvedInVideo: MActorFollowersUrl[],
|
||||
isOrigin = false
|
||||
): ActivityAudience {
|
||||
const to = [ ACTIVITY_PUB.PUBLIC ]
|
||||
const to = [ getAPPublicValue() ]
|
||||
const cc: string[] = []
|
||||
|
||||
// Owner of the video we comment
|
||||
|
@ -43,14 +43,14 @@ function getVideoCommentAudience (
|
|||
}
|
||||
}
|
||||
|
||||
function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
|
||||
export function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
|
||||
return {
|
||||
to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
|
||||
to: [ getAPPublicValue() ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
|
||||
cc: []
|
||||
}
|
||||
}
|
||||
|
||||
async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
|
||||
export async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
|
||||
const actors = await VideoShareModel.listActorIdsAndFollowerUrlsByShare(video.id, t)
|
||||
|
||||
const alreadyLoadedActor = (video as VideoModel).VideoChannel?.Account?.Actor
|
||||
|
@ -63,12 +63,3 @@ async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
|
|||
|
||||
return actors
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
getOriginVideoAudience,
|
||||
getActorsInvolvedInVideo,
|
||||
getAudienceFromFollowersOf,
|
||||
getVideoCommentAudience
|
||||
}
|
||||
|
|
|
@ -258,7 +258,6 @@ function unicastTo (options: {
|
|||
export {
|
||||
broadcastToFollowers,
|
||||
unicastTo,
|
||||
forwardActivity,
|
||||
broadcastToActors,
|
||||
sendVideoActivityToOrigin,
|
||||
forwardVideoRelatedActivity,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import Bluebird from 'bluebird'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { logger, loggerTagsFactory } from '../../helpers/logger.js'
|
||||
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants.js'
|
||||
import { VideoShareModel } from '../../models/video/video-share.js'
|
||||
|
@ -12,16 +12,7 @@ import { checkUrlsSameHost, getLocalVideoAnnounceActivityPubUrl } from './url.js
|
|||
|
||||
const lTags = loggerTagsFactory('share')
|
||||
|
||||
async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
|
||||
if (!video.hasPrivacyForFederation()) return undefined
|
||||
|
||||
return Promise.all([
|
||||
shareByServer(video, t),
|
||||
shareByVideoChannel(video, t)
|
||||
])
|
||||
}
|
||||
|
||||
async function changeVideoChannelShare (
|
||||
export async function changeVideoChannelShare (
|
||||
video: MVideoAccountLight,
|
||||
oldVideoChannel: MChannelActorLight,
|
||||
t: Transaction
|
||||
|
@ -36,7 +27,7 @@ async function changeVideoChannelShare (
|
|||
await shareByVideoChannel(video, t)
|
||||
}
|
||||
|
||||
async function addVideoShares (shareUrls: string[], video: MVideoId) {
|
||||
export async function addVideoShares (shareUrls: string[], video: MVideoId) {
|
||||
await Bluebird.map(shareUrls, async shareUrl => {
|
||||
try {
|
||||
await addVideoShare(shareUrl, video)
|
||||
|
@ -46,12 +37,44 @@ async function addVideoShares (shareUrls: string[], video: MVideoId) {
|
|||
}, { concurrency: CRAWL_REQUEST_CONCURRENCY })
|
||||
}
|
||||
|
||||
export {
|
||||
changeVideoChannelShare,
|
||||
addVideoShares,
|
||||
shareVideoByServerAndChannel
|
||||
export async function shareByServer (video: MVideo, t: Transaction) {
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const serverShareUrl = getLocalVideoAnnounceActivityPubUrl(serverActor, video)
|
||||
const [ serverShare ] = await VideoShareModel.findOrCreate({
|
||||
defaults: {
|
||||
actorId: serverActor.id,
|
||||
videoId: video.id,
|
||||
url: serverShareUrl
|
||||
},
|
||||
where: {
|
||||
url: serverShareUrl
|
||||
},
|
||||
transaction: t
|
||||
})
|
||||
|
||||
return sendVideoAnnounce(serverActor, serverShare, video, t)
|
||||
}
|
||||
|
||||
export async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) {
|
||||
const videoChannelShareUrl = getLocalVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
|
||||
const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
|
||||
defaults: {
|
||||
actorId: video.VideoChannel.actorId,
|
||||
videoId: video.id,
|
||||
url: videoChannelShareUrl
|
||||
},
|
||||
where: {
|
||||
url: videoChannelShareUrl
|
||||
},
|
||||
transaction: t
|
||||
})
|
||||
|
||||
return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addVideoShare (shareUrl: string, video: MVideoId) {
|
||||
|
@ -74,42 +97,6 @@ async function addVideoShare (shareUrl: string, video: MVideoId) {
|
|||
await VideoShareModel.upsert(entry)
|
||||
}
|
||||
|
||||
async function shareByServer (video: MVideo, t: Transaction) {
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const serverShareUrl = getLocalVideoAnnounceActivityPubUrl(serverActor, video)
|
||||
const [ serverShare ] = await VideoShareModel.findOrCreate({
|
||||
defaults: {
|
||||
actorId: serverActor.id,
|
||||
videoId: video.id,
|
||||
url: serverShareUrl
|
||||
},
|
||||
where: {
|
||||
url: serverShareUrl
|
||||
},
|
||||
transaction: t
|
||||
})
|
||||
|
||||
return sendVideoAnnounce(serverActor, serverShare, video, t)
|
||||
}
|
||||
|
||||
async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) {
|
||||
const videoChannelShareUrl = getLocalVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
|
||||
const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
|
||||
defaults: {
|
||||
actorId: video.VideoChannel.actorId,
|
||||
videoId: video.id,
|
||||
url: videoChannelShareUrl
|
||||
},
|
||||
where: {
|
||||
url: videoChannelShareUrl
|
||||
},
|
||||
transaction: t
|
||||
})
|
||||
|
||||
return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
|
||||
}
|
||||
|
||||
async function undoShareByVideoChannel (video: MVideo, oldVideoChannel: MChannelActorLight, t: Transaction) {
|
||||
// Load old share
|
||||
const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t)
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Hooks } from '../plugins/hooks.js'
|
|||
import { fetchAP } from './activity.js'
|
||||
import { getOrCreateAPActor } from './actors/index.js'
|
||||
import { checkUrlsSameHost } from './url.js'
|
||||
import { getOrCreateAPVideo } from './videos/index.js'
|
||||
import { canVideoBeFederated, getOrCreateAPVideo } from './videos/index.js'
|
||||
|
||||
type ResolveThreadParams = {
|
||||
url: string
|
||||
|
@ -92,8 +92,8 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
|
|||
const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false }
|
||||
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
|
||||
|
||||
if (video.isOwned() && !video.hasPrivacyForFederation()) {
|
||||
throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation')
|
||||
if (video.isOwned() && !canVideoBeFederated(video)) {
|
||||
throw new Error('Cannot resolve thread of video that is not compatible with federation')
|
||||
}
|
||||
|
||||
let resultComment: MCommentOwnerVideo
|
||||
|
|
|
@ -1,29 +1,53 @@
|
|||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { VideoPrivacy, VideoPrivacyType, VideoState, VideoStateType } from '@peertube/peertube-models'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { MVideoAPLight, MVideoWithBlacklistRights } from '@server/types/models/index.js'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { MVideoAP, MVideoAPLight } from '@server/types/models/index.js'
|
||||
import { sendCreateVideo, sendUpdateVideo } from '../send/index.js'
|
||||
import { shareVideoByServerAndChannel } from '../share.js'
|
||||
import { shareByServer, shareByVideoChannel } from '../share.js'
|
||||
|
||||
async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) {
|
||||
const video = videoArg as MVideoAP
|
||||
export async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) {
|
||||
if (!canVideoBeFederated(videoArg, isNewVideo)) return
|
||||
|
||||
if (
|
||||
// Check this is not a blacklisted video, or unfederated blacklisted video
|
||||
(video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
|
||||
// Check the video is public/unlisted and published
|
||||
video.hasPrivacyForFederation() && video.hasStateForFederation()
|
||||
) {
|
||||
const video = await videoArg.lightAPToFullAP(transaction)
|
||||
const video = await videoArg.lightAPToFullAP(transaction)
|
||||
|
||||
if (isNewVideo) {
|
||||
// Now we'll add the video's meta data to our followers
|
||||
await sendCreateVideo(video, transaction)
|
||||
await shareVideoByServerAndChannel(video, transaction)
|
||||
} else {
|
||||
await sendUpdateVideo(video, transaction)
|
||||
}
|
||||
if (isNewVideo) {
|
||||
// Now we'll add the video's meta data to our followers
|
||||
await sendCreateVideo(video, transaction)
|
||||
|
||||
await Promise.all([
|
||||
shareByServer(video, transaction),
|
||||
shareByVideoChannel(video, transaction)
|
||||
])
|
||||
} else {
|
||||
await sendUpdateVideo(video, transaction)
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
federateVideoIfNeeded
|
||||
export function canVideoBeFederated (video: MVideoWithBlacklistRights, isNewVideo = false) {
|
||||
// Check this is not a blacklisted video
|
||||
if (video.isBlacklisted() === true) {
|
||||
if (isNewVideo === false) return false
|
||||
if (video.VideoBlacklist.unfederated === true) return false
|
||||
}
|
||||
|
||||
// Check the video is public/unlisted and published
|
||||
return isPrivacyForFederation(video.privacy) && isStateForFederation(video.state)
|
||||
}
|
||||
|
||||
export function isNewVideoPrivacyForFederation (currentPrivacy: VideoPrivacyType, newPrivacy: VideoPrivacyType) {
|
||||
return !isPrivacyForFederation(currentPrivacy) && isPrivacyForFederation(newPrivacy)
|
||||
}
|
||||
|
||||
export function isPrivacyForFederation (privacy: VideoPrivacyType) {
|
||||
const castedPrivacy = forceNumber(privacy)
|
||||
|
||||
return castedPrivacy === VideoPrivacy.PUBLIC ||
|
||||
(CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED)
|
||||
}
|
||||
|
||||
export function isStateForFederation (state: VideoStateType) {
|
||||
const castedState = forceNumber(state)
|
||||
|
||||
return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { APObjectId } from '@peertube/peertube-models'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders/index.js'
|
||||
import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models/index.js'
|
||||
import { APObjectId } from '@peertube/peertube-models'
|
||||
import {
|
||||
MVideoAccountLightBlacklistAllFiles,
|
||||
MVideoImmutable,
|
||||
MVideoThumbnail,
|
||||
MVideoThumbnailBlacklist
|
||||
} from '@server/types/models/index.js'
|
||||
import { getAPId } from '../activity.js'
|
||||
import { refreshVideoIfNeeded } from './refresh.js'
|
||||
import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared/index.js'
|
||||
|
@ -24,23 +29,25 @@ type GetVideoParamAll = {
|
|||
type GetVideoParamImmutable = {
|
||||
videoObject: APObjectId
|
||||
syncParam?: SyncParam
|
||||
fetchType: 'only-immutable-attributes'
|
||||
fetchType: 'unsafe-only-immutable-attributes'
|
||||
allowRefresh: false
|
||||
}
|
||||
|
||||
type GetVideoParamOther = {
|
||||
videoObject: APObjectId
|
||||
syncParam?: SyncParam
|
||||
fetchType?: 'all' | 'only-video'
|
||||
fetchType?: 'all' | 'only-video-and-blacklist'
|
||||
allowRefresh?: boolean
|
||||
}
|
||||
|
||||
export function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
|
||||
export function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
|
||||
export function getOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
|
||||
export function getOrCreateAPVideo (
|
||||
options: GetVideoParamOther
|
||||
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist>
|
||||
export async function getOrCreateAPVideo (
|
||||
options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
|
||||
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
|
||||
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist | MVideoImmutable> {
|
||||
// Default params
|
||||
const syncParam = options.syncParam || { rates: true, shares: true, comments: true, refreshVideo: false }
|
||||
const fetchType = options.fetchType || 'all'
|
||||
|
@ -52,7 +59,7 @@ export async function getOrCreateAPVideo (
|
|||
|
||||
if (videoFromDatabase) {
|
||||
if (allowRefresh === true) {
|
||||
// Typings ensure allowRefresh === false in only-immutable-attributes fetch type
|
||||
// Typings ensure allowRefresh === false in unsafe-only-immutable-attributes fetch type
|
||||
videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam)
|
||||
}
|
||||
|
||||
|
@ -87,7 +94,9 @@ export async function getOrCreateAPVideo (
|
|||
|
||||
export function maybeGetOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
|
||||
export function maybeGetOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
|
||||
export function maybeGetOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
|
||||
export function maybeGetOrCreateAPVideo (
|
||||
options: GetVideoParamOther
|
||||
): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist>
|
||||
export async function maybeGetOrCreateAPVideo (options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther) {
|
||||
try {
|
||||
const result = await getOrCreateAPVideo(options as any)
|
||||
|
|
|
@ -11,13 +11,14 @@ import {
|
|||
VideoPrivacy,
|
||||
VideoStreamingPlaylistType
|
||||
} from '@peertube/peertube-models'
|
||||
import { hasAPPublic } from '@server/helpers/activity-pub-utils.js'
|
||||
import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos.js'
|
||||
import { isArray } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { generateImageFilename } from '@server/helpers/image-utils.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { getExtFromMimetype } from '@server/helpers/video.js'
|
||||
import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants.js'
|
||||
import { MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants.js'
|
||||
import { generateTorrentFileName } from '@server/lib/paths.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
|
@ -191,7 +192,7 @@ export function getStoryboardAttributeFromObject (video: MVideoId, videoObject:
|
|||
}
|
||||
|
||||
export function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
|
||||
const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
|
||||
const privacy = hasAPPublic(to)
|
||||
? VideoPrivacy.PUBLIC
|
||||
: VideoPrivacy.UNLISTED
|
||||
|
||||
|
|
|
@ -6,57 +6,57 @@ import {
|
|||
MVideoFullLight,
|
||||
MVideoId,
|
||||
MVideoImmutable,
|
||||
MVideoThumbnail
|
||||
MVideoThumbnailBlacklist
|
||||
} from '@server/types/models/index.js'
|
||||
import { getOrCreateAPVideo } from '../activitypub/videos/get.js'
|
||||
|
||||
type VideoLoadType = 'for-api' | 'all' | 'only-video' | 'id' | 'none' | 'only-immutable-attributes'
|
||||
type VideoLoadType = 'for-api' | 'all' | 'only-video-and-blacklist' | 'id' | 'none' | 'unsafe-only-immutable-attributes'
|
||||
|
||||
function loadVideo (id: number | string, fetchType: 'for-api', userId?: number): Promise<MVideoFormattableDetails>
|
||||
function loadVideo (id: number | string, fetchType: 'all', userId?: number): Promise<MVideoFullLight>
|
||||
function loadVideo (id: number | string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable>
|
||||
function loadVideo (id: number | string, fetchType: 'only-video', userId?: number): Promise<MVideoThumbnail>
|
||||
function loadVideo (id: number | string, fetchType: 'unsafe-only-immutable-attributes'): Promise<MVideoImmutable>
|
||||
function loadVideo (id: number | string, fetchType: 'only-video-and-blacklist', userId?: number): Promise<MVideoThumbnailBlacklist>
|
||||
function loadVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Promise<MVideoId>
|
||||
function loadVideo (
|
||||
id: number | string,
|
||||
fetchType: VideoLoadType,
|
||||
userId?: number
|
||||
): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable>
|
||||
): Promise<MVideoFullLight | MVideoThumbnailBlacklist | MVideoId | MVideoImmutable>
|
||||
function loadVideo (
|
||||
id: number | string,
|
||||
fetchType: VideoLoadType,
|
||||
userId?: number
|
||||
): Promise<MVideoFullLight | MVideoThumbnail | MVideoId | MVideoImmutable> {
|
||||
): Promise<MVideoFullLight | MVideoThumbnailBlacklist | MVideoId | MVideoImmutable> {
|
||||
|
||||
if (fetchType === 'for-api') return VideoModel.loadForGetAPI({ id, userId })
|
||||
|
||||
if (fetchType === 'all') return VideoModel.loadFull(id, undefined, userId)
|
||||
|
||||
if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
|
||||
if (fetchType === 'unsafe-only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
|
||||
|
||||
if (fetchType === 'only-video') return VideoModel.load(id)
|
||||
if (fetchType === 'only-video-and-blacklist') return VideoModel.loadWithBlacklist(id)
|
||||
|
||||
if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
|
||||
}
|
||||
|
||||
type VideoLoadByUrlType = 'all' | 'only-video' | 'only-immutable-attributes'
|
||||
type VideoLoadByUrlType = 'all' | 'only-video-and-blacklist' | 'unsafe-only-immutable-attributes'
|
||||
|
||||
function loadVideoByUrl (url: string, fetchType: 'all'): Promise<MVideoAccountLightBlacklistAllFiles>
|
||||
function loadVideoByUrl (url: string, fetchType: 'only-immutable-attributes'): Promise<MVideoImmutable>
|
||||
function loadVideoByUrl (url: string, fetchType: 'only-video'): Promise<MVideoThumbnail>
|
||||
function loadVideoByUrl (url: string, fetchType: 'unsafe-only-immutable-attributes'): Promise<MVideoImmutable>
|
||||
function loadVideoByUrl (url: string, fetchType: 'only-video-and-blacklist'): Promise<MVideoThumbnailBlacklist>
|
||||
function loadVideoByUrl (
|
||||
url: string,
|
||||
fetchType: VideoLoadByUrlType
|
||||
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable>
|
||||
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist | MVideoImmutable>
|
||||
function loadVideoByUrl (
|
||||
url: string,
|
||||
fetchType: VideoLoadByUrlType
|
||||
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
|
||||
): Promise<MVideoAccountLightBlacklistAllFiles | MVideoThumbnailBlacklist | MVideoImmutable> {
|
||||
if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccountAndFiles(url)
|
||||
|
||||
if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url)
|
||||
if (fetchType === 'unsafe-only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url)
|
||||
|
||||
if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
|
||||
if (fetchType === 'only-video-and-blacklist') return VideoModel.loadByUrlWithBlacklist(url)
|
||||
}
|
||||
|
||||
async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) {
|
||||
|
@ -64,7 +64,7 @@ async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) {
|
|||
try {
|
||||
const res = await getOrCreateAPVideo({
|
||||
videoObject: videoUrl,
|
||||
fetchType: 'only-immutable-attributes',
|
||||
fetchType: 'unsafe-only-immutable-attributes',
|
||||
allowRefresh: false
|
||||
})
|
||||
|
||||
|
@ -78,10 +78,8 @@ async function loadOrCreateVideoIfAllowedForUser (videoUrl: string) {
|
|||
}
|
||||
|
||||
export {
|
||||
type VideoLoadType,
|
||||
type VideoLoadByUrlType,
|
||||
|
||||
loadVideo,
|
||||
loadOrCreateVideoIfAllowedForUser, loadVideo,
|
||||
loadVideoByUrl,
|
||||
loadOrCreateVideoIfAllowedForUser
|
||||
type VideoLoadByUrlType,
|
||||
type VideoLoadType
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue