Compare commits

...

7 Commits

Author SHA1 Message Date
Chocobozzz f23f3ebc7c
Add AP compact test 2024-04-25 09:43:13 +02:00
Chocobozzz f28a007be5
Compact json-ld AP objects 2024-04-25 09:32:03 +02:00
Chocobozzz 4e09837aa0
Fix player subtitles on iOS 2024-04-24 15:57:24 +02:00
Chocobozzz fde6b32ca5
Update translations 2024-04-24 14:57:00 +02:00
spf 63654cb475
Translated using Weblate (French (France) (fr_FR))
Currently translated at 93.7% (2218 of 2367 strings)

Translation: PeerTube/angular
Translate-URL: https://weblate.framasoft.org/projects/peertube/angular/fr_FR/
2024-04-24 14:52:22 +02:00
Puryx dcd3a0ee46
Translated using Weblate (Romanian)
Currently translated at 19.1% (262 of 1367 strings)

Translation: PeerTube/angular
Translate-URL: https://weblate.framasoft.org/projects/peertube/angular/ro/
2024-04-24 14:52:22 +02:00
Puryx db32f70799
Translated using Weblate (Romanian)
Currently translated at 18.8% (258 of 1367 strings)

Translation: PeerTube/angular
Translate-URL: https://weblate.framasoft.org/projects/peertube/angular/ro/
2024-04-24 14:52:22 +02:00
70 changed files with 1883 additions and 3657 deletions

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,113 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"Update": "as:Create"
}
],
"id": "https://example.com/users/bob/statuses/107928807471117876/activity",
"type": "Update",
"actor": "https://example.com/users/bob",
"published": "2022-03-09T21:55:07Z",
"to": "https://www.w3.org/ns/activitystreams#Public",
"cc": "https://example.com/users/bob/followers",
"object": {
"@context": {
"id": {
"@id": "as:attributedTo",
"@type": "@id"
},
"Person": "as:Note",
"following": {
"@id": "as:cc",
"@type": "@id"
},
"followers": {
"@id": "as:cc",
"@type": "@id"
},
"inbox": {
"@id": "as:cc",
"@type": "@id"
},
"sharedInbox": {
"@id": "as:cc",
"@type": "@id"
},
"outbox": {
"@id": "as:cc",
"@type": "@id"
},
"preferredUsername": "@type",
"bob": "as:Note",
"name": "@type",
"BEING TAKEN OVER": "as:Note",
"summary": "@type",
"THIS ACCOUNT IS BEING TAKEN OVER BY AN ATTACKER": "as:Note",
"url": {
"@id": "as:attributedTo",
"@type": "@id"
},
"publicKey": {
"@id": "as:replies",
"@type": "@id"
},
"ostatus": "http://ostatus.org#"
},
"id": "https://example.com/users/bob",
"type": "Person",
"following": "https://example.com/users/bob/followers",
"followers": "https://example.com/users/bob/followers",
"inbox": "https://example.com/users/bob/followers",
"sharedInbox": "https://example.com/users/bob/followers",
"outbox": "https://example.com/users/bob/followers",
"preferredUsername": "bob",
"name": "BEING TAKEN OVER",
"summary": "THIS ACCOUNT IS BEING TAKEN OVER BY AN ATTACKER",
"url": "https://example.com/users/bob",
"published": "2022-03-09T21:55:07Z",
"publicKey": {
"@context": {
"id": "@id",
"owner": {
"@reverse": "as:replies",
"@type": "@id"
},
"publicKeyPem": "@type",
"-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAL2hdo3culcPqz6y7AT0rlE5hgiNceL4\n28VkreQP2rSecXgeMZnjeW42GExS73F71pGMkx7b9svVK4IfPTlMN2ECAwEAAQ==\n-----END PUBLIC KEY-----\n": "as:Collection"
},
"id": "https://example.com/users/bob/statuses/107928807471117876/replies",
"owner": "https://example.com/users/bob/statuses/107928807471117876",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAL2hdo3culcPqz6y7AT0rlE5hgiNceL4\n28VkreQP2rSecXgeMZnjeW42GExS73F71pGMkx7b9svVK4IfPTlMN2ECAwEAAQ==\n-----END PUBLIC KEY-----\n",
"as:first": {
"type": "CollectionPage",
"items": [],
"next": "https://example.com/users/bob/statuses/107928807471117876/replies?only_other_accounts=true&page=true",
"partOf": "https://example.com/users/bob/statuses/107928807471117876/replies"
}
},
"@id": "https://example.com/users/bob/statuses/107928807471117876",
"ostatus:atomUri": "https://example.com/users/bob/statuses/107928807471117876",
"ostatus:conversation": "tag:example.com,2022-03-09:objectId=15:objectType=Conversation",
"as:content": [
"<p>hello world</p>",
{
"@value": "<p>hello world</p>",
"@language": "en"
}
],
"as:sensitive": false,
"as:to": {
"@id": "https://www.w3.org/ns/activitystreams#Public"
},
"as:url": {
"@id": "https://example.com/@bob/107928807471117876"
}
},
"signature": {
"type": "RsaSignature2017",
"creator": "https://example.com/users/bob#main-key",
"created": "2022-03-09T21:57:25Z",
"signatureValue": "WculK0LelTQ0MvGwU9TPoq5pFzFfGYRDCJqjZ232/Udj4CHqDTGOSw5UTDLShqBOyycCkbZGrQwXG+dpyDpQLSe1UVPZ5TPQtc/9XtI57WlS2nMNpdvRuxGnnb2btPdesXZ7n3pCxo0zjaXrJMe0mqQh5QJO22mahb4bDwwmfTHgbD3nmkD+fBfGi+UV2qWwqr+jlV4L4JqNkh0gWljF5KTePLRRZCuWiQ/FAt7c67636cdIPf7fR+usjuZltTQyLZKEGuK8VUn2Gkfsx5qns7Vcjvlz1JqlAjyO8HPBbzTTHzUG2nUOIgC3PojCSWv6mNTmRGoLZzOscCAYQA6cKw=="
}
}

View File

@ -0,0 +1,6 @@
{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuuYyoyfsRkYnXRotMsId\nW3euBDDfiv9oVqOxUVC7bhel8KednIMrMCRWFAkgJhbrlzbIkjVr68o1MP9qLcn7\nCmH/BXHp7yhuFTr4byjdJKpwB+/i2jNEsvDH5jR8WTAeTCe0x/QHg21V3F7dSI5m\nCCZ/1dSIyOXLRTWVlfDlm3rE4ntlCo+US3/7oSWbg/4/4qEnt1HC32kvklgScxua\n4LR5ATdoXa5bFoopPWhul7MJ6NyWCyQyScUuGdlj8EN4kmKQJvphKHrI9fvhgOuG\nTvhTR1S5InA4azSSchY0tXEEw/VNxraeX0KPjbgr6DPcwhPd/m0nhVDq0zVyVBBD\nMwIDAQAB\n-----END PUBLIC KEY-----\n",
"privateKey": "-----BEGIN PRIVATE KEY-----\nMIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAvaF2jdy6Vw+rPrLs\nBPSuUTmGCI1x4vjbxWSt5A/atJ5xeB4xmeN5bjYYTFLvcXvWkYyTHtv2y9Urgh89\nOUw3YQIDAQABAkAWF4BrSILA78dgd5G9hg/k0JHH30qcSae42GDVx+8PyY5LTW/k\n2luohqd2aFbVl/64eV8wU4FaTqhuPRAXJKYRAiEA45eyOxOpnCMlO4OuTEItmDE0\n8i5pdasWI+YbFxAJVI0CIQDVTMP43JcgjtGxU7s6eTYGTH1T1LHi8MxZj0q33/C7\nJQIgFJYcIQveQ6lKLN/0XCGATkvlJiLclzAqiIS/3o4syeECIQCHrtpmvyPfkRow\n3BuYmaxVG2kJ3538x8KmIfGcv/ZphQIgXOULuGECR0nnOFwcZ9arqIWp7BE825da\nskc6vNULKBA=\n-----END PRIVATE KEY-----"
}

View File

@ -3,7 +3,7 @@
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import { signAndContextify } from '@peertube/peertube-server/core/helpers/activity-pub-utils.js'
import { isHTTPSignatureVerified, parseHTTPSignature } from '@peertube/peertube-server/core/helpers/peertube-crypto.js'
import { isJsonLDSignatureVerified, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
import { compactJSONLDAndCheckSignature, signJsonLDObject } from '@peertube/peertube-server/core/helpers/peertube-jsonld.js'
import { expect } from 'chai'
import { readJsonSync } from 'fs-extra/esm'
import cloneDeep from 'lodash-es/cloneDeep.js'
@ -24,6 +24,10 @@ function fakeFilter () {
return (data: any) => Promise.resolve(data)
}
function fakeExpressReq (body: any) {
return { body }
}
describe('Test activity pub helpers', function () {
describe('When checking the Linked Signature', function () {
@ -33,7 +37,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
expect(result).to.be.false
})
@ -43,7 +47,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/bad-public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
expect(result).to.be.false
})
@ -53,7 +57,7 @@ describe('Test activity pub helpers', function () {
const publicKey = readJsonSync(buildAbsoluteFixturePath('./ap-json/mastodon/public-key.json')).publicKey
const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, body)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(body))
expect(result).to.be.true
})
@ -72,11 +76,24 @@ 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
})
it('Should compact JSONLD input when checking JSONLD signature', async function () {
const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys-updated.json'))
const signedBody = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-updated.json'))
const fromActor = { publicKey: keys.publicKey }
const req = { body: signedBody }
const result = await compactJSONLDAndCheckSignature(fromActor as any, req)
expect(req.body.type).to.equal('Create')
expect(result).to.be.true
})
it('Should succeed with a valid PeerTube signature', async function () {
const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
const body = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
@ -91,7 +108,7 @@ describe('Test activity pub helpers', function () {
})
const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
const result = await compactJSONLDAndCheckSignature(fromActor as any, fakeExpressReq(signedBody))
expect(result).to.be.true
})

View File

@ -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,7 +186,10 @@ 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({
@ -205,15 +224,19 @@ 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'
}
}),
View: buildContext({
@ -233,13 +256,46 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
Rate: buildContext(),
Chapters: buildContext({
name: 'sc:name',
hasPart: 'sc:hasPart',
endOffset: 'sc:endOffset',
startOffset: 'sc:startOffset'
})
}
let allContext: (string | ContextValue)[]
export function getAllContext () {
if (allContext) return allContext
const processed = new Set<string>()
allContext = []
let staticContext: ContextValue = {}
for (const v of Object.values(contextStore)) {
for (const item of v) {
if (typeof item === 'string') {
if (!processed.has(item)) {
allContext.push(item)
}
processed.add(item)
} else {
for (const subKey of Object.keys(item)) {
if (!processed.has(subKey)) {
staticContext = { ...staticContext, [subKey]: item[subKey] }
}
processed.add(subKey)
}
}
}
}
allContext = [ ...allContext, staticContext ]
return allContext
}
async function getContextData (type: ContextType, contextFilter: ContextFilter) {
const contextData = contextFilter
? await contextFilter(contextStore[type])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -774,7 +774,6 @@ const ACTIVITY_PUB = {
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
],
ACCEPT_HEADER: 'application/activity+json, application/ld+json',
PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
COLLECTION_ITEMS_PER_PAGE: 10,
FETCH_PAGE_LIMIT: 2000,
MAX_RECURSION_COMMENTS: 100,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { NextFunction, Request, Response } from 'express'
import { ActivityDelete, ActivityPubSignature, HttpStatusCode } from '@peertube/peertube-models'
import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor.js'
import { getAPId } from '@server/lib/activitypub/activity.js'
import { wrapWithSpanAndContext } from '@server/lib/opentelemetry/tracing.js'
import { ActivityDelete, ActivityPubSignature, HttpStatusCode } from '@peertube/peertube-models'
import { NextFunction, Request, Response } from 'express'
import { logger } from '../helpers/logger.js'
import { isHTTPSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto.js'
import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants.js'
@ -18,7 +18,7 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
// Forwarded activity
const bodyActor = req.body.actor
const bodyActorId = getAPId(bodyActor)
if (bodyActorId && bodyActorId !== actor.url) {
if (bodyActorId && bodyActorId !== actor.url || bodyActorId === actor.url) {
const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
if (jsonLDSignatureChecked !== true) return
}
@ -54,9 +54,8 @@ function executeIfActivityPub (req: Request, res: Response, next: NextFunction)
// ---------------------------------------------------------------------------
export {
checkSignature,
executeIfActivityPub,
checkHttpSignature
checkHttpSignature, checkSignature,
executeIfActivityPub
}
// ---------------------------------------------------------------------------
@ -123,7 +122,7 @@ async function checkHttpSignature (req: Request, res: Response) {
async function checkJsonLDSignature (req: Request, res: Response) {
// Lazy load the module as it's quite big with json.ld dependency
const { isJsonLDSignatureVerified } = await import('../helpers/peertube-jsonld.js')
const { compactJSONLDAndCheckSignature } = await import('../helpers/peertube-jsonld.js')
return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => {
const signatureObject: ActivityPubSignature = req.body.signature
@ -141,7 +140,7 @@ async function checkJsonLDSignature (req: Request, res: Response) {
logger.debug('Checking JsonLD signature of actor %s...', creator)
const actor = await getOrCreateAPActor(creator)
const verified = await isJsonLDSignatureVerified(actor, req.body)
const verified = await compactJSONLDAndCheckSignature(actor, req)
if (verified !== true) {
logger.warn('Signature not verified.', req.body)