Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc
Who moderates the instance? What is the policy regarding sensitive content? Political videos? etc
- Auto block
+ @if (videoBlock.type === 2) {
+ Auto block
+ }
- NSFW
+
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index 708775a7a..6a46e02a3 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -19,6 +19,7 @@ import { AutoColspanDirective } from '../../../shared/shared-main/common/auto-co
import { EmbedComponent } from '../../../shared/shared-main/video/embed.component'
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
import { VideoCellComponent } from '../../../shared/shared-tables/video-cell.component'
+import { VideoNSFWBadgeComponent } from '../../../shared/shared-video/video-nsfw-badge.component'
@Component({
selector: 'my-video-block-list',
@@ -36,7 +37,8 @@ import { VideoCellComponent } from '../../../shared/shared-tables/video-cell.com
VideoCellComponent,
AutoColspanDirective,
EmbedComponent,
- PTDatePipe
+ PTDatePipe,
+ VideoNSFWBadgeComponent
]
})
export class VideoBlockListComponent extends RestTable implements OnInit {
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index cfecc2d4a..6f4fd04b5 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -74,7 +74,7 @@
- NSFW
+
{{ video.state.label }}
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index 82bbc0486..bf64887f5 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -13,7 +13,7 @@ import { VideoBlockComponent } from '@app/shared/shared-moderation/video-block.c
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { getAllFiles } from '@peertube/peertube-core-utils'
-import { FileStorage, UserRight, VideoFile, VideoState, VideoStreamingPlaylistType } from '@peertube/peertube-models'
+import { FileStorage, NSFWFlag, UserRight, VideoFile, VideoState, VideoStreamingPlaylistType } from '@peertube/peertube-models'
import { videoRequiresFileToken } from '@root-helpers/video'
import { SharedModule, SortMeta } from 'primeng/api'
import { TableModule, TableRowExpandEvent } from 'primeng/table'
@@ -31,6 +31,7 @@ import {
VideoActionsDisplayType,
VideoActionsDropdownComponent
} from '../../../shared/shared-video-miniature/video-actions-dropdown.component'
+import { VideoNSFWBadgeComponent } from '../../../shared/shared-video/video-nsfw-badge.component'
import { VideoPrivacyBadgeComponent } from '../../../shared/shared-video/video-privacy-badge.component'
import { VideoAdminService } from './video-admin.service'
@@ -58,7 +59,8 @@ import { VideoAdminService } from './video-admin.service'
PTDatePipe,
RouterLink,
BytesPipe,
- VideoPrivacyBadgeComponent
+ VideoPrivacyBadgeComponent,
+ VideoNSFWBadgeComponent
]
})
export class VideoListComponent extends RestTable implements OnInit {
@@ -305,7 +307,11 @@ export class VideoListComponent extends RestTable implements OnInit {
this.videoAdminService.getAdminVideos({
pagination: this.pagination,
sort: this.sort,
- nsfw: 'both', // Always list NSFW video, overriding instance/user setting
+
+ // Always list NSFW video, overriding instance/user setting
+ nsfw: 'both',
+ nsfwFlagsExcluded: NSFWFlag.NONE,
+
search: this.search
}).pipe(finalize(() => this.loading = false))
.subscribe({
diff --git a/client/src/app/+admin/plugins/shared/plugin-card.component.scss b/client/src/app/+admin/plugins/shared/plugin-card.component.scss
index 81d015291..cf93ea1bf 100644
--- a/client/src/app/+admin/plugins/shared/plugin-card.component.scss
+++ b/client/src/app/+admin/plugins/shared/plugin-card.component.scss
@@ -28,7 +28,7 @@
color: pvar(--fg-400);
&[iconName=npm] {
- @include fill-svg-color(pvar(--fg-400));
+ @include fill-path-svg-color(pvar(--fg-400));
}
}
}
diff --git a/client/src/app/+my-library/my-history/my-history.component.ts b/client/src/app/+my-library/my-history/my-history.component.ts
index 8369299ce..596ec63ce 100644
--- a/client/src/app/+my-library/my-history/my-history.component.ts
+++ b/client/src/app/+my-library/my-history/my-history.component.ts
@@ -57,9 +57,7 @@ export class MyHistoryComponent implements OnInit, DisableForReuseHook {
date: true,
views: true,
by: true,
- privacyLabel: false,
- privacyText: true,
- blacklistInfo: true
+ privacyLabel: false
}
getVideosObservableFunction = this.getVideosObservable.bind(this)
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html
index 9c88e8ece..4cc873574 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.html
+++ b/client/src/app/+my-library/my-videos/my-videos.component.html
@@ -145,7 +145,7 @@
- NSFW
+
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts
index 48d161eab..7f74656e3 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.ts
+++ b/client/src/app/+my-library/my-videos/my-videos.component.ts
@@ -49,6 +49,7 @@ import {
VideoActionsDisplayType,
VideoActionsDropdownComponent
} from '../../shared/shared-video-miniature/video-actions-dropdown.component'
+import { VideoNSFWBadgeComponent } from '../../shared/shared-video/video-nsfw-badge.component'
import { VideoPrivacyBadgeComponent } from '../../shared/shared-video/video-privacy-badge.component'
import { VideoStateBadgeComponent } from '../../shared/shared-video/video-state-badge.component'
import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
@@ -95,7 +96,8 @@ type QueryParams = {
ChannelToggleComponent,
AutoColspanDirective,
SelectCheckboxComponent,
- PTDatePipe
+ PTDatePipe,
+ VideoNSFWBadgeComponent
]
})
export class MyVideosComponent extends RestTable implements OnInit, OnDestroy {
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html
index e97a177b1..de7855929 100644
--- a/client/src/app/+search/search-filters.component.html
+++ b/client/src/app/+search/search-filters.component.html
@@ -35,25 +35,6 @@
-
-
Published date
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index 911651ffe..019ed49ff 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -74,9 +74,7 @@ export class SearchComponent implements OnInit, OnDestroy {
views: true,
by: true,
avatar: true,
- privacyLabel: false,
- privacyText: false,
- blacklistInfo: false
+ privacyLabel: false
}
errorMessage: string
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 464d078e7..a4134448d 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -1,4 +1,4 @@
-import { NgIf } from '@angular/common'
+import { CommonModule } from '@angular/common'
import { AfterViewInit, Component, OnDestroy, OnInit, inject, viewChild } from '@angular/core'
import { ComponentPaginationLight, DisableForReuseHook, HooksService, ScreenService } from '@app/core'
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
@@ -12,7 +12,7 @@ import { VideosListComponent } from '../../shared/shared-video-miniature/videos-
@Component({
selector: 'my-video-channel-videos',
templateUrl: './video-channel-videos.component.html',
- imports: [ NgIf, VideosListComponent ]
+ imports: [ CommonModule, VideosListComponent ]
})
export class VideoChannelVideosComponent implements OnInit, AfterViewInit, OnDestroy, DisableForReuseHook {
private screenService = inject(ScreenService)
@@ -65,7 +65,7 @@ export class VideoChannelVideosComponent implements OnInit, AfterViewInit, OnDes
skipCount: true
}
- return this.videoService.getVideoChannelVideos(params)
+ return this.videoService.listChannelVideos(params)
}
getSyndicationItems () {
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index bbe58c579..50c68f1ac 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -176,7 +176,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
}
private loadChannelVideosCount () {
- this.videoService.getVideoChannelVideos({
+ this.videoService.listChannelVideos({
videoChannel: this.videoChannel,
videoPagination: {
currentPage: 1,
diff --git a/client/src/app/+video-list/videos-list-all.component.ts b/client/src/app/+video-list/videos-list-all.component.ts
index f58e29c02..948055c59 100644
--- a/client/src/app/+video-list/videos-list-all.component.ts
+++ b/client/src/app/+video-list/videos-list-all.component.ts
@@ -58,7 +58,7 @@ export class VideosListAllComponent implements OnInit, OnDestroy, DisableForReus
}
return this.hooks.wrapObsFun(
- this.videoService.getVideos.bind(this.videoService),
+ this.videoService.listVideos.bind(this.videoService),
params,
'common',
'filter:api.browse-videos.videos.list.params',
diff --git a/client/src/app/+video-watch/shared/action-buttons/video-rate.component.scss b/client/src/app/+video-watch/shared/action-buttons/video-rate.component.scss
index 30add70c2..8e694616d 100644
--- a/client/src/app/+video-watch/shared/action-buttons/video-rate.component.scss
+++ b/client/src/app/+video-watch/shared/action-buttons/video-rate.component.scss
@@ -14,6 +14,6 @@
my-global-icon {
color: pvar(--active-icon-color) !important;
- @include fill-svg-color(pvar(--active-icon-bg));
+ @include fill-path-svg-color(pvar(--active-icon-bg));
}
}
diff --git a/client/src/app/+video-watch/shared/recommendations/video-recommendation.service.ts b/client/src/app/+video-watch/shared/recommendations/video-recommendation.service.ts
index 75e529e79..c6fee2cc0 100644
--- a/client/src/app/+video-watch/shared/recommendations/video-recommendation.service.ts
+++ b/client/src/app/+video-watch/shared/recommendations/video-recommendation.service.ts
@@ -59,15 +59,10 @@ export class VideoRecommendationService {
return this.userService.getAnonymousOrLoggedUser()
.pipe(
switchMap(user => {
- const nsfw = user.nsfwPolicy
- ? this.videos.nsfwPolicyToParam(user.nsfwPolicy)
- : undefined
-
- const defaultSubscription = this.videos.getVideos({
+ const defaultSubscription = this.videos.listVideos({
skipCount: true,
videoPagination: pagination,
- sort: '-publishedAt',
- nsfw
+ sort: '-publishedAt'
}).pipe(map(v => v.data))
const searchIndexConfig = this.config.search.searchIndex
@@ -83,7 +78,6 @@ export class VideoRecommendationService {
tagsOneOf: currentVideo.tags.join(','),
sort: '-publishedAt',
searchTarget: 'local',
- nsfw,
excludeAlreadyWatched: user.id
? true
: undefined
diff --git a/client/src/app/+video-watch/video-watch.component.html b/client/src/app/+video-watch/video-watch.component.html
index 65b0f59b2..0fb189f3a 100644
--- a/client/src/app/+video-watch/video-watch.component.html
+++ b/client/src/app/+video-watch/video-watch.component.html
@@ -32,7 +32,7 @@
-
+
@@ -110,7 +110,7 @@
class="border-top pt-3"
[video]="video"
[videoPassword]="videoPassword"
- [user]="user"
+ [user]="authUser"
(timestampClicked)="handleTimestampClicked($event)"
>
diff --git a/client/src/app/+video-watch/video-watch.component.ts b/client/src/app/+video-watch/video-watch.component.ts
index 60e23490b..35f28ec9d 100644
--- a/client/src/app/+video-watch/video-watch.component.ts
+++ b/client/src/app/+video-watch/video-watch.component.ts
@@ -185,7 +185,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private hotkeys: Hotkey[] = []
- get user () {
+ get authUser () {
return this.authService.getUser()
}
@@ -276,11 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
isUserOwner () {
- return this.video.isLocal === true && this.video.account.name === this.user?.username
- }
-
- isVideoBlur (video: Video) {
- return video.isVideoNSFWForUser(this.user, this.serverConfig)
+ return this.video.isLocal === true && this.video.account.name === this.authUser?.username
}
isChannelDisplayNameGeneric () {
@@ -526,7 +522,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.transcriptionWidgetOpened = false
}
- if (this.isVideoBlur(this.video)) {
+ if (this.video.isVideoNSFWHiddenForUser(loggedInOrAnonymousUser, this.serverConfig)) {
const res = await this.confirmService.confirm(
$localize`This video contains mature or explicit content. Are you sure you want to watch it?`,
$localize`Mature or explicit content`
@@ -556,7 +552,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
const videoState = this.video.state.id
if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) {
- this.updatePlayerOnNoLive()
+ this.updatePlayerOnNoLive({ hasPlayed: false })
return
}
@@ -573,7 +569,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
urlOptions: this.getUrlOptions(),
loggedInOrAnonymousUser,
forceAutoplay,
- user: this.user
+ user: this.authUser
}
const loadOptions = await this.hooks.wrapFun(
@@ -649,31 +645,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
})
}
- private isAutoplay () {
+ private isAutoplay (loggedInOrAnonymousUser: User) {
// We'll jump to the thread id, so do not play the video
if (this.route.snapshot.params['threadId']) return false
- if (this.user) return this.user.autoPlayVideo
+ // Prevent autoplay if we need to warn the user
+ if (this.video.isVideoNSFWWarnedForUser(loggedInOrAnonymousUser, this.serverConfig)) return false
- if (this.anonymousUser) return this.anonymousUser.autoPlayVideo
+ if (loggedInOrAnonymousUser) return loggedInOrAnonymousUser.autoPlayVideo
throw new Error('Cannot guess autoplay because user and anonymousUser are not defined')
}
- private isAutoPlayNext () {
- return (
- (this.user?.autoPlayNextVideo) ||
- this.anonymousUser.autoPlayNextVideo
- )
- }
-
- private isPlaylistAutoPlayNext () {
- return (
- (this.user?.autoPlayNextVideoPlaylist) ||
- this.anonymousUser.autoPlayNextVideoPlaylist
- )
- }
-
private buildPeerTubePlayerConstructorOptions (options: {
urlOptions: URLOptions
}): PeerTubePlayerConstructorOptions {
@@ -821,11 +804,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return {
mode,
- autoplay: this.isAutoplay(),
+ autoplay: this.isAutoplay(loggedInOrAnonymousUser),
forceAutoplay,
duration: this.video.duration,
- poster: video.previewUrl,
p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled),
startTime,
@@ -846,9 +828,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
videoFileToken: () => videoFileToken,
requiresUserAuth: videoRequiresUserAuth(video, videoPassword),
requiresPassword: video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED &&
- !video.canBypassPassword(this.user),
+ !video.canBypassPassword(this.authUser),
videoPassword: () => videoPassword,
+ poster: video.isVideoNSFWBlurForUser(loggedInOrAnonymousUser, this.serverConfig)
+ ? null
+ : video.previewUrl,
+
+ nsfwWarning: video.isVideoNSFWWarnedForUser(loggedInOrAnonymousUser, this.serverConfig)
+ ? {
+ flags: video.nsfwFlags,
+ summary: video.nsfwSummary
+ }
+ : undefined,
+
videoCaptions: playerCaptions,
videoChapters,
storyboard,
@@ -877,9 +870,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
upnext: {
isEnabled: () => {
- if (this.playlist) return this.isPlaylistAutoPlayNext()
+ if (this.playlist) return loggedInOrAnonymousUser?.autoPlayNextVideoPlaylist
- return this.isAutoPlayNext()
+ return loggedInOrAnonymousUser?.autoPlayNextVideo
},
isSuspended: (player: videojs.Player) => {
@@ -943,10 +936,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.video.viewers = newViewers
}
- private updatePlayerOnNoLive () {
+ private updatePlayerOnNoLive ({ hasPlayed }: { hasPlayed: boolean }) {
this.peertubePlayer.unload()
this.peertubePlayer.disable()
- this.peertubePlayer.setPoster(this.video.previewPath)
+
+ if (hasPlayed || !this.video.isVideoNSFWBlurForUser(this.authUser || this.anonymousUser, this.serverConfig)) {
+ this.peertubePlayer.setPoster(this.video.previewPath)
+ }
}
private buildHotkeysHelp (video: Video) {
@@ -1039,6 +1035,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.video.state.id = VideoState.LIVE_ENDED
- this.updatePlayerOnNoLive()
+ this.updatePlayerOnNoLive({ hasPlayed: true })
}
}
diff --git a/client/src/app/+videos-publish-manage/shared-manage/common/video-edit.model.ts b/client/src/app/+videos-publish-manage/shared-manage/common/video-edit.model.ts
index 032758825..b518184fa 100644
--- a/client/src/app/+videos-publish-manage/shared-manage/common/video-edit.model.ts
+++ b/client/src/app/+videos-publish-manage/shared-manage/common/video-edit.model.ts
@@ -5,6 +5,7 @@ import {
LiveVideo,
LiveVideoCreate,
LiveVideoUpdate,
+ NSFWFlag,
VideoCaption,
VideoChapter,
VideoCreate,
@@ -31,12 +32,19 @@ const debugLogger = debug('peertube:video-manage:video-edit')
export type VideoEditPrivacyType = VideoPrivacyType | typeof VideoEdit.SPECIAL_SCHEDULED_PRIVACY
type CommonUpdateForm =
- & Omit
+ & Omit<
+ VideoUpdate,
+ 'privacy' | 'videoPasswords' | 'thumbnailfile' | 'scheduleUpdate' | 'commentsEnabled' | 'originallyPublishedAt' | 'nsfwFlags'
+ >
& {
schedulePublicationAt?: Date
originallyPublishedAt?: Date
privacy?: VideoEditPrivacyType
videoPassword?: string
+
+ nsfwFlagViolent?: boolean
+ nsfwFlagSex?: boolean
+ nsfwFlagShocking?: boolean
}
type LiveUpdateForm = Omit & {
@@ -82,6 +90,8 @@ type UpdateFromAPIOptions = {
| 'description'
| 'tags'
| 'nsfw'
+ | 'nsfwFlags'
+ | 'nsfwSummary'
| 'waitTranscoding'
| 'support'
| 'commentsPolicy'
@@ -317,6 +327,8 @@ export class VideoEdit {
description: video.description ?? '',
tags: video.tags ?? [],
nsfw: video.nsfw ?? null,
+ nsfwSummary: video.nsfwSummary ?? null,
+ nsfwFlags: video.nsfwFlags ?? NSFWFlag.NONE,
waitTranscoding: video.waitTranscoding ?? null,
support: video.support ?? '',
commentsPolicy: video.commentsPolicy?.id ?? null,
@@ -430,7 +442,6 @@ export class VideoEdit {
if (values.language !== undefined) this.common.language = values.language
if (values.description !== undefined) this.common.description = values.description
if (values.tags !== undefined) this.common.tags = values.tags
- if (values.nsfw !== undefined) this.common.nsfw = values.nsfw
if (values.waitTranscoding !== undefined) this.common.waitTranscoding = values.waitTranscoding
if (values.support !== undefined) this.common.support = values.support
if (values.commentsPolicy !== undefined) this.common.commentsPolicy = values.commentsPolicy
@@ -438,6 +449,41 @@ export class VideoEdit {
if (values.previewfile !== undefined) this.common.previewfile = values.previewfile
if (values.pluginData !== undefined) this.common.pluginData = values.pluginData
+ // ---------------------------------------------------------------------------
+ // NSFW
+ // ---------------------------------------------------------------------------
+
+ if (values.nsfw !== undefined) this.common.nsfw = values.nsfw
+
+ if (this.common.nsfw) {
+ if (values.nsfwFlagSex !== undefined) {
+ this.common.nsfwFlags = values.nsfwFlagSex
+ ? this.common.nsfwFlags | NSFWFlag.EXPLICIT_SEX
+ : this.common.nsfwFlags & ~NSFWFlag.EXPLICIT_SEX
+ }
+
+ if (values.nsfwFlagShocking !== undefined) {
+ this.common.nsfwFlags = values.nsfwFlagShocking
+ ? this.common.nsfwFlags | NSFWFlag.SHOCKING_DISTURBING
+ : this.common.nsfwFlags & ~NSFWFlag.SHOCKING_DISTURBING
+ }
+
+ if (values.nsfwFlagViolent !== undefined) {
+ this.common.nsfwFlags = values.nsfwFlagViolent
+ ? this.common.nsfwFlags | NSFWFlag.VIOLENT
+ : this.common.nsfwFlags & ~NSFWFlag.VIOLENT
+ }
+
+ if (values.nsfwSummary !== undefined) {
+ this.common.nsfwSummary = values.nsfwSummary
+ }
+ } else {
+ this.common.nsfwSummary = null
+ this.common.nsfwFlags = NSFWFlag.NONE
+ }
+
+ // ---------------------------------------------------------------------------
+
if (values.videoPassword !== undefined) {
this.common.videoPasswords = values.privacy === VideoPrivacy.PASSWORD_PROTECTED && values.videoPassword
? [ values.videoPassword ]
@@ -483,7 +529,13 @@ export class VideoEdit {
support: this.common.support,
name: this.common.name,
tags: this.common.tags,
+
nsfw: this.common.nsfw,
+ nsfwFlagSex: (this.common.nsfwFlags & NSFWFlag.EXPLICIT_SEX) === NSFWFlag.EXPLICIT_SEX,
+ nsfwFlagShocking: (this.common.nsfwFlags & NSFWFlag.SHOCKING_DISTURBING) === NSFWFlag.SHOCKING_DISTURBING,
+ nsfwFlagViolent: (this.common.nsfwFlags & NSFWFlag.VIOLENT) === NSFWFlag.VIOLENT,
+ nsfwSummary: this.common.nsfwSummary,
+
commentsPolicy: this.common.commentsPolicy,
waitTranscoding: this.common.waitTranscoding,
channelId: this.common.channelId,
@@ -550,6 +602,8 @@ export class VideoEdit {
tags: this.common.tags,
nsfw: this.common.nsfw,
+ nsfwFlags: this.common.nsfwFlags,
+ nsfwSummary: this.common.nsfwSummary || null,
waitTranscoding: this.common.waitTranscoding,
commentsPolicy: this.common.commentsPolicy,
downloadEnabled: this.common.downloadEnabled,
diff --git a/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.html b/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.html
index 4b22afb64..864f69adc 100644
--- a/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.html
+++ b/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.html
@@ -9,27 +9,54 @@
diff --git a/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.ts b/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.ts
index 6a5d5c447..db32d3df1 100644
--- a/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.ts
+++ b/client/src/app/+videos-publish-manage/shared-manage/moderation/video-moderation.component.ts
@@ -1,16 +1,17 @@
-import { NgIf } from '@angular/common'
+import { CommonModule } from '@angular/common'
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterLink } from '@angular/router'
import { ServerService } from '@app/core'
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
+import { VIDEO_NSFW_SUMMARY_VALIDATOR } from '@app/shared/form-validators/video-validators'
import { FormReactiveErrors, FormReactiveService, FormReactiveValidationMessages } from '@app/shared/shared-forms/form-reactive.service'
import { HTMLServerConfig, VideoCommentPolicyType, VideoConstant } from '@peertube/peertube-models'
import debug from 'debug'
import { CalendarModule } from 'primeng/calendar'
import { Subscription } from 'rxjs'
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
-import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
+import { SelectRadioComponent } from '../../../shared/shared-forms/select/select-radio.component'
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
import { VideoManageController } from '../video-manage-controller.service'
@@ -19,6 +20,12 @@ const debugLogger = debug('peertube:video-manage')
type Form = {
nsfw: FormControl
+
+ nsfwFlagViolent: FormControl
+ nsfwFlagShocking: FormControl
+ nsfwFlagSex: FormControl
+ nsfwSummary: FormControl
+
commentPolicies: FormControl
}
@@ -29,15 +36,15 @@ type Form = {
],
templateUrl: './video-moderation.component.html',
imports: [
+ CommonModule,
RouterLink,
FormsModule,
ReactiveFormsModule,
- NgIf,
PeerTubeTemplateDirective,
- SelectOptionsComponent,
CalendarModule,
PeertubeCheckboxComponent,
- GlobalIconComponent
+ GlobalIconComponent,
+ SelectRadioComponent
]
})
export class VideoModerationComponent implements OnInit, OnDestroy {
@@ -71,7 +78,14 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
const videoEdit = this.manageController.getStore().videoEdit
const defaultValues = videoEdit.toCommonFormPatch()
- const obj: BuildFormArgument = { nsfw: null, commentsPolicy: null }
+ const obj: BuildFormArgument = {
+ commentsPolicy: null,
+ nsfw: null,
+ nsfwFlagViolent: null,
+ nsfwFlagShocking: null,
+ nsfwFlagSex: null,
+ nsfwSummary: VIDEO_NSFW_SUMMARY_VALIDATOR
+ }
const {
form,
@@ -97,5 +111,35 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
this.updatedSub = this.manageController.getUpdatedObs().subscribe(() => {
this.form.patchValue(videoEdit.toCommonFormPatch())
})
+
+ this.updateNSFWControls(videoEdit.toCommonFormPatch().nsfw)
+ this.trackNSFWChange()
+ }
+
+ private trackNSFWChange () {
+ this.form.controls.nsfw
+ .valueChanges
+ .subscribe(newNSFW => this.updateNSFWControls(newNSFW))
+ }
+
+ private updateNSFWControls (nsfw: boolean) {
+ const controls = [
+ this.form.controls.nsfwFlagViolent,
+ this.form.controls.nsfwFlagShocking,
+ this.form.controls.nsfwFlagSex,
+ this.form.controls.nsfwSummary
+ ]
+
+ if (!nsfw) {
+ for (const control of controls) {
+ control.disable()
+ }
+
+ return
+ }
+
+ for (const control of controls) {
+ control.enable()
+ }
}
}
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts
index 9f05627ed..964eac917 100644
--- a/client/src/app/core/theme/theme.service.ts
+++ b/client/src/app/core/theme/theme.service.ts
@@ -1,11 +1,9 @@
import { Injectable, inject } from '@angular/core'
-import { sortBy } from '@peertube/peertube-core-utils'
import { HTMLServerConfig, ServerConfig, ServerConfigTheme } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
import { capitalizeFirstLetter } from '@root-helpers/string'
+import { ThemeManager } from '@root-helpers/theme-manager'
import { UserLocalStorageKeys } from '@root-helpers/users'
-import { getLuminance, parse, toHSLA } from 'color-bits'
-import debug from 'debug'
import { environment } from '../../../environments/environment'
import { AuthService } from '../auth'
import { PluginService } from '../plugins/plugin.service'
@@ -13,8 +11,6 @@ import { ServerService } from '../server'
import { UserService } from '../users/user.service'
import { LocalStorageService } from '../wrappers/storage.service'
-const debugLogger = debug('peertube:theme')
-
@Injectable()
export class ThemeService {
private auth = inject(AuthService)
@@ -23,7 +19,6 @@ export class ThemeService {
private server = inject(ServerService)
private localStorageService = inject(LocalStorageService)
- private oldInjectedProperties: string[] = []
private oldThemeName: string
private internalThemes: string[] = []
@@ -34,6 +29,8 @@ export class ThemeService {
private serverConfig: HTMLServerConfig
+ private themeManager = new ThemeManager()
+
initialize () {
this.serverConfig = this.server.getHTMLConfig()
this.internalThemes = this.serverConfig.theme.builtIn.map(t => t.name)
@@ -80,30 +77,19 @@ export class ThemeService {
logger.info(`Injecting ${this.themes.length} themes.`)
- const head = this.getHeadElement()
-
for (const theme of this.themes) {
// Already added this theme?
if (fromLocalStorage === false && this.themeFromLocalStorage && this.themeFromLocalStorage.name === theme.name) continue
- for (const css of theme.css) {
- const link = document.createElement('link')
+ const links = this.themeManager.injectTheme(theme, environment.apiUrl)
- const href = environment.apiUrl + `/themes/${theme.name}/${theme.version}/css/${css}`
- link.setAttribute('href', href)
- link.setAttribute('rel', 'alternate stylesheet')
- link.setAttribute('type', 'text/css')
- link.setAttribute('title', theme.name)
- link.setAttribute('disabled', '')
-
- if (fromLocalStorage === true) this.themeDOMLinksFromLocalStorage.push(link)
-
- head.appendChild(link)
+ if (fromLocalStorage === true) {
+ this.themeDOMLinksFromLocalStorage = [ ...this.themeDOMLinksFromLocalStorage, ...links ]
}
}
}
- private getCurrentTheme () {
+ private getCurrentThemeName () {
if (this.themeFromLocalStorage) return this.themeFromLocalStorage.name
const theme = this.auth.isLoggedIn()
@@ -123,43 +109,24 @@ export class ThemeService {
return instanceTheme
}
- private loadThemeStyle (name: string) {
- const links = document.getElementsByTagName('link')
-
- for (let i = 0; i < links.length; i++) {
- const link = links[i]
- if (link.getAttribute('rel').includes('style') && link.getAttribute('title')) {
- link.disabled = link.getAttribute('title') !== name
-
- if (!link.disabled) {
- link.onload = () => this.injectColorPalette()
- } else {
- link.onload = undefined
- }
- }
- }
-
- document.body.dataset.ptTheme = name
- }
-
private updateCurrentTheme () {
- const currentTheme = this.getCurrentTheme()
- if (this.oldThemeName === currentTheme) return
+ const currentThemeName = this.getCurrentThemeName()
+ if (this.oldThemeName === currentThemeName) return
if (this.oldThemeName) this.removeThemePlugins(this.oldThemeName)
- logger.info(`Enabling ${currentTheme} theme.`)
+ logger.info(`Enabling ${currentThemeName} theme.`)
- this.loadThemeStyle(currentTheme)
+ this.themeManager.loadThemeStyle(currentThemeName)
- const theme = this.getTheme(currentTheme)
+ const theme = this.getTheme(currentThemeName)
- if (this.internalThemes.includes(currentTheme)) {
- logger.info(`Enabling internal theme ${currentTheme}`)
+ if (this.internalThemes.includes(currentThemeName)) {
+ logger.info(`Enabling internal theme ${currentThemeName}`)
- this.localStorageService.setItem(UserLocalStorageKeys.LAST_ACTIVE_THEME, JSON.stringify({ name: currentTheme }), false)
+ this.localStorageService.setItem(UserLocalStorageKeys.LAST_ACTIVE_THEME, JSON.stringify({ name: currentThemeName }), false)
} else if (theme) {
- logger.info(`Adding scripts of theme ${currentTheme}`)
+ logger.info(`Adding scripts of theme ${currentThemeName}`)
this.pluginService.addPlugin(theme, true)
@@ -170,161 +137,9 @@ export class ThemeService {
this.localStorageService.removeItem(UserLocalStorageKeys.LAST_ACTIVE_THEME, false)
}
- this.injectCoreColorPalette()
+ this.themeManager.injectCoreColorPalette()
- this.oldThemeName = currentTheme
- }
-
- private injectCoreColorPalette (iteration = 0) {
- if (iteration > 10) {
- logger.error('Cannot inject core color palette: too many iterations')
- return
- }
-
- if (!this.canInjectCoreColorPalette()) {
- return setTimeout(() => this.injectCoreColorPalette(iteration + 1))
- }
-
- return this.injectColorPalette()
- }
-
- private canInjectCoreColorPalette () {
- const computedStyle = getComputedStyle(document.body)
- const isDark = computedStyle.getPropertyValue('--is-dark')
-
- return isDark === '0' || isDark === '1'
- }
-
- private injectColorPalette () {
- console.log(`Injecting color palette`)
-
- const rootStyle = document.body.style
- const computedStyle = getComputedStyle(document.body)
-
- // FIXME: Remove previously injected properties
- for (const property of this.oldInjectedProperties) {
- rootStyle.removeProperty(property)
- }
-
- this.oldInjectedProperties = []
-
- const isGlobalDarkTheme = () => {
- return this.isDarkTheme({
- fg: computedStyle.getPropertyValue('--fg') || computedStyle.getPropertyValue('--mainForegroundColor'),
- bg: computedStyle.getPropertyValue('--bg') || computedStyle.getPropertyValue('--mainBackgroundColor'),
- isDarkVar: computedStyle.getPropertyValue('--is-dark')
- })
- }
-
- const isMenuDarkTheme = () => {
- return this.isDarkTheme({
- fg: computedStyle.getPropertyValue('--menu-fg'),
- bg: computedStyle.getPropertyValue('--menu-bg'),
- isDarkVar: computedStyle.getPropertyValue('--is-menu-dark')
- })
- }
-
- const toProcess = [
- { prefix: 'primary', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
- { prefix: 'on-primary', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
- { prefix: 'bg-secondary', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
- { prefix: 'fg', invertIfDark: true, fallbacks: { '--fg-300': '--greyForegroundColor' }, step: 5, darkTheme: isGlobalDarkTheme },
-
- { prefix: 'input-bg', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
-
- { prefix: 'menu-fg', invertIfDark: true, step: 5, darkTheme: isMenuDarkTheme },
- { prefix: 'menu-bg', invertIfDark: true, step: 5, darkTheme: isMenuDarkTheme }
- ] as { prefix: string, invertIfDark: boolean, step: number, darkTheme: () => boolean, fallbacks?: Record }[]
-
- for (const { prefix, invertIfDark, step, darkTheme, fallbacks = {} } of toProcess) {
- const mainColor = computedStyle.getPropertyValue('--' + prefix)
-
- const darkInverter = invertIfDark && darkTheme()
- ? -1
- : 1
-
- if (!mainColor) {
- console.error(`Cannot create palette of nonexistent "--${prefix}" CSS body variable`)
- continue
- }
-
- // Use trim for some web browsers: https://github.com/Chocobozzz/PeerTube/issues/6952
- const mainColorHSL = toHSLA(parse(mainColor.trim()))
- debugLogger(`Theme main variable ${mainColor} -> ${this.toHSLStr(mainColorHSL)}`)
-
- // Inject in alphabetical order for easy debug
- const toInject: { id: number, key: string, value: string }[] = [
- { id: 500, key: `--${prefix}-500`, value: this.toHSLStr(mainColorHSL) }
- ]
-
- for (const j of [ -1, 1 ]) {
- let lastColorHSL = { ...mainColorHSL }
-
- for (let i = 1; i <= 9; i++) {
- const suffix = 500 + (50 * i * j)
- const key = `--${prefix}-${suffix}`
-
- const existingValue = computedStyle.getPropertyValue(key)
- if (!existingValue || existingValue === '0') {
- const newLuminance = this.buildNewLuminance(lastColorHSL, j * step, darkInverter)
- const newColorHSL = { ...lastColorHSL, l: newLuminance }
-
- const newColorStr = this.toHSLStr(newColorHSL)
-
- const value = fallbacks[key]
- ? `var(${fallbacks[key]}, ${newColorStr})`
- : newColorStr
-
- toInject.push({ id: suffix, key, value })
-
- lastColorHSL = newColorHSL
-
- debugLogger(`Injected theme palette ${key} -> ${value}`)
- } else {
- lastColorHSL = toHSLA(parse(existingValue))
- }
- }
- }
-
- for (const { key, value } of sortBy(toInject, 'id')) {
- rootStyle.setProperty(key, value)
- this.oldInjectedProperties.push(key)
- }
- }
-
- document.body.dataset.bsTheme = isGlobalDarkTheme()
- ? 'dark'
- : ''
- }
-
- private buildNewLuminance (base: { l: number }, factor: number, darkInverter: number) {
- return Math.max(Math.min(100, Math.round(base.l + (factor * -1 * darkInverter))), 0)
- }
-
- private toHSLStr (c: { h: number, s: number, l: number, a: number }) {
- return `hsl(${Math.round(c.h)} ${Math.round(c.s)}% ${Math.round(c.l)}% / ${Math.round(c.a)})`
- }
-
- private isDarkTheme (options: {
- fg: string
- bg: string
- isDarkVar: string
- }) {
- const { fg, bg, isDarkVar } = options
-
- if (isDarkVar === '1') {
- return true
- } else if (fg && bg) {
- try {
- if (getLuminance(parse(bg)) < getLuminance(parse(fg))) {
- return true
- }
- } catch (err) {
- console.error('Cannot parse deprecated CSS variables', err)
- }
- }
-
- return false
+ this.oldThemeName = currentThemeName
}
private listenUserTheme () {
@@ -381,9 +196,8 @@ export class ThemeService {
this.removeThemePlugins(this.themeFromLocalStorage.name)
this.oldThemeName = undefined
- const head = this.getHeadElement()
for (const htmlLinkElement of this.themeDOMLinksFromLocalStorage) {
- head.removeChild(htmlLinkElement)
+ this.themeManager.removeThemeLink(htmlLinkElement)
}
this.themeFromLocalStorage = undefined
@@ -391,10 +205,6 @@ export class ThemeService {
}
}
- private getHeadElement () {
- return document.getElementsByTagName('head')[0]
- }
-
private getTheme (name: string) {
return this.themes.find(t => t.name === name)
}
diff --git a/client/src/app/core/users/user-local-storage.service.ts b/client/src/app/core/users/user-local-storage.service.ts
index bfa4b4be0..51e5fd16f 100644
--- a/client/src/app/core/users/user-local-storage.service.ts
+++ b/client/src/app/core/users/user-local-storage.service.ts
@@ -1,11 +1,11 @@
-import { filter, throttleTime } from 'rxjs'
import { Injectable, inject } from '@angular/core'
import { AuthService, AuthStatus } from '@app/core/auth'
import { objectKeysTyped } from '@peertube/peertube-core-utils'
import { NSFWPolicyType, UserRoleType, UserUpdateMe } from '@peertube/peertube-models'
-import { getBoolOrDefault } from '@root-helpers/local-storage-utils'
+import { getBoolOrDefault, getNumberOrDefault } from '@root-helpers/local-storage-utils'
import { logger } from '@root-helpers/logger'
import { OAuthUserTokens, UserLocalStorageKeys } from '@root-helpers/users'
+import { filter, throttleTime } from 'rxjs'
import { ServerService } from '../server'
import { LocalStorageService } from '../wrappers/storage.service'
@@ -110,6 +110,11 @@ export class UserLocalStorageService {
return {
nsfwPolicy: this.localStorageService.getItem(UserLocalStorageKeys.NSFW_POLICY) || defaultNSFWPolicy,
+ nsfwFlagsDisplayed: getNumberOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.NSFW_FLAGS_DISPLAYED), undefined),
+ nsfwFlagsWarned: getNumberOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.NSFW_FLAGS_WARNED), undefined),
+ nsfwFlagsBlurred: getNumberOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.NSFW_FLAGS_BLURRED), undefined),
+ nsfwFlagsHidden: getNumberOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.NSFW_FLAGS_HIDDEN), undefined),
+
p2pEnabled: getBoolOrDefault(this.localStorageService.getItem(UserLocalStorageKeys.P2P_ENABLED), defaultP2PEnabled),
theme: this.localStorageService.getItem(UserLocalStorageKeys.THEME) || 'instance-default',
videoLanguages,
@@ -123,6 +128,10 @@ export class UserLocalStorageService {
setUserInfo (profile: UserUpdateMe) {
const localStorageKeys = {
nsfwPolicy: UserLocalStorageKeys.NSFW_POLICY,
+ nsfwFlagsDisplayed: UserLocalStorageKeys.NSFW_FLAGS_DISPLAYED,
+ nsfwFlagsHidden: UserLocalStorageKeys.NSFW_FLAGS_HIDDEN,
+ nsfwFlagsWarned: UserLocalStorageKeys.NSFW_FLAGS_WARNED,
+ nsfwFlagsBlurred: UserLocalStorageKeys.NSFW_FLAGS_BLURRED,
p2pEnabled: UserLocalStorageKeys.P2P_ENABLED,
autoPlayVideo: UserLocalStorageKeys.AUTO_PLAY_VIDEO,
autoPlayNextVideo: UserLocalStorageKeys.AUTO_PLAY_NEXT_VIDEO,
@@ -131,7 +140,7 @@ export class UserLocalStorageService {
videoLanguages: UserLocalStorageKeys.VIDEO_LANGUAGES
}
- const obj: [string, string | boolean | string[]][] = objectKeysTyped(localStorageKeys)
+ const obj: [string, string | boolean | number | string[]][] = objectKeysTyped(localStorageKeys)
.filter(key => key in profile)
.map(key => [ localStorageKeys[key], profile[key] ])
@@ -155,6 +164,10 @@ export class UserLocalStorageService {
flushUserInfo () {
this.localStorageService.removeItem(UserLocalStorageKeys.NSFW_POLICY)
+ this.localStorageService.removeItem(UserLocalStorageKeys.NSFW_FLAGS_DISPLAYED)
+ this.localStorageService.removeItem(UserLocalStorageKeys.NSFW_FLAGS_WARNED)
+ this.localStorageService.removeItem(UserLocalStorageKeys.NSFW_FLAGS_BLURRED)
+ this.localStorageService.removeItem(UserLocalStorageKeys.NSFW_FLAGS_HIDDEN)
this.localStorageService.removeItem(UserLocalStorageKeys.P2P_ENABLED)
this.localStorageService.removeItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO)
this.localStorageService.removeItem(UserLocalStorageKeys.AUTO_PLAY_VIDEO_PLAYLIST)
@@ -165,6 +178,10 @@ export class UserLocalStorageService {
listenUserInfoChange () {
return this.localStorageService.watch([
UserLocalStorageKeys.NSFW_POLICY,
+ UserLocalStorageKeys.NSFW_FLAGS_DISPLAYED,
+ UserLocalStorageKeys.NSFW_FLAGS_WARNED,
+ UserLocalStorageKeys.NSFW_FLAGS_BLURRED,
+ UserLocalStorageKeys.NSFW_FLAGS_HIDDEN,
UserLocalStorageKeys.P2P_ENABLED,
UserLocalStorageKeys.AUTO_PLAY_VIDEO,
UserLocalStorageKeys.AUTO_PLAY_NEXT_VIDEO,
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts
index f98e05106..79b45349c 100644
--- a/client/src/app/core/users/user.model.ts
+++ b/client/src/app/core/users/user.model.ts
@@ -22,7 +22,12 @@ export class User implements UserServerModel {
emailVerified: boolean
emailPublic: boolean
+
nsfwPolicy: NSFWPolicyType
+ nsfwFlagsDisplayed: number
+ nsfwFlagsHidden: number
+ nsfwFlagsWarned: number
+ nsfwFlagsBlurred: number
adminFlags?: UserAdminFlagType
@@ -89,7 +94,7 @@ export class User implements UserServerModel {
patch (obj: UserServerModel) {
for (const key of objectKeysTyped(obj)) {
- (this as any)[key] = obj[key]
+ ;(this as any)[key] = obj[key]
}
if (obj.account !== undefined) {
diff --git a/client/src/app/menu/quick-settings-modal.component.ts b/client/src/app/menu/quick-settings-modal.component.ts
index 3c7ecab4e..a97d05f72 100644
--- a/client/src/app/menu/quick-settings-modal.component.ts
+++ b/client/src/app/menu/quick-settings-modal.component.ts
@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'
import { Component, OnDestroy, OnInit, inject, output, viewChild } from '@angular/core'
-import { ActivatedRoute, Router } from '@angular/router'
-import { AuthService, AuthStatus, LocalStorageService, User, UserService } from '@app/core'
+import { ActivatedRoute } from '@angular/router'
+import { AuthService, AuthStatus, LocalStorageService, PeerTubeRouterService, User, UserService } from '@app/core'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
@@ -30,7 +30,7 @@ export class QuickSettingsModalComponent implements OnInit, OnDestroy {
private authService = inject(AuthService)
private localStorageService = inject(LocalStorageService)
private route = inject(ActivatedRoute)
- private router = inject(Router)
+ private peertubeRouter = inject(PeerTubeRouterService)
private static readonly QUERY_MODAL_NAME = 'quick-settings'
@@ -99,6 +99,6 @@ export class QuickSettingsModalComponent implements OnInit, OnDestroy {
? QuickSettingsModalComponent.QUERY_MODAL_NAME
: null
- this.router.navigate([], { queryParams: { modal }, queryParamsHandling: 'merge' })
+ this.peertubeRouter.silentNavigate([], { modal }, this.route)
}
}
diff --git a/client/src/app/shared/form-validators/video-validators.ts b/client/src/app/shared/form-validators/video-validators.ts
index bf18ba630..86a46dbcc 100644
--- a/client/src/app/shared/form-validators/video-validators.ts
+++ b/client/src/app/shared/form-validators/video-validators.ts
@@ -86,6 +86,14 @@ export const VIDEO_SUPPORT_VALIDATOR: BuildFormValidator = {
}
}
+export const VIDEO_NSFW_SUMMARY_VALIDATOR: BuildFormValidator = {
+ VALIDATORS: [ Validators.minLength(3), Validators.maxLength(250) ],
+ MESSAGES: {
+ minlength: $localize`Video support must be at least 3 characters long.`,
+ maxlength: $localize`Video support cannot be more than 250 characters long.`
+ }
+}
+
export const VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR: BuildFormValidator = {
VALIDATORS: [], // Required is set dynamically
MESSAGES: {
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
index d63cd96f0..4c870e9f0 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
@@ -92,7 +92,7 @@ export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, O
.pipe(
map(user => user.nsfwPolicy),
switchMap(nsfwPolicy => {
- return this.videoService.getVideoChannelVideos({ ...videoOptions, nsfw: this.videoService.nsfwPolicyToParam(nsfwPolicy) })
+ return this.videoService.listChannelVideos({ ...videoOptions, nsfw: this.videoService.nsfwPolicyToParam(nsfwPolicy) })
})
)
}
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts
index c4c56e836..86e2bcd76 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts
@@ -35,9 +35,7 @@ export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent,
views: true,
by: true,
avatar: true,
- privacyLabel: false,
- privacyText: false,
- blacklistInfo: false
+ privacyLabel: false
}
ngOnInit () {
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.html
index c9569b727..ce068d305 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.html
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.html
@@ -1,6 +1,6 @@
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts
index 67ef8a213..2f3f20ec2 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts
@@ -1,6 +1,6 @@
import { NgIf } from '@angular/common'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, inject, input, model, output } from '@angular/core'
-import { AuthService, Notifier } from '@app/core'
+import { AuthService, Notifier, User, UserService } from '@app/core'
import { Video } from '@app/shared/shared-main/video/video.model'
import { FindInBulkService } from '@app/shared/shared-search/find-in-bulk.service'
import { objectKeysTyped } from '@peertube/peertube-core-utils'
@@ -23,6 +23,7 @@ export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnI
private auth = inject(AuthService)
private findInBulk = inject(FindInBulkService)
private notifier = inject(Notifier)
+ private userService = inject(UserService)
private cd = inject(ChangeDetectorRef)
readonly uuid = input(undefined)
@@ -36,14 +37,10 @@ export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnI
views: true,
by: true,
avatar: true,
- privacyLabel: false,
- privacyText: false,
- blacklistInfo: false
+ privacyLabel: false
}
- getUser () {
- return this.auth.getUser()
- }
+ user: User
ngOnInit () {
if (this.onlyDisplayTitle()) {
@@ -52,6 +49,9 @@ export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnI
}
}
+ this.userService.getAnonymousOrLoggedUser()
+ .subscribe(user => this.user = user)
+
if (this.video()) return
this.findInBulk.getVideo(this.uuid())
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.html b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.html
index 868bda387..f4d3d3f23 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.html
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.html
@@ -3,7 +3,7 @@
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
index f94e2af4c..6a7e60f04 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts
@@ -1,6 +1,6 @@
import { NgFor, NgStyle } from '@angular/common'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, inject, input, output } from '@angular/core'
-import { AuthService, Notifier } from '@app/core'
+import { AuthService, Notifier, User, UserService } from '@app/core'
import { Video } from '@app/shared/shared-main/video/video.model'
import { CommonVideoParams, VideoService } from '@app/shared/shared-main/video/video.service'
import { objectKeysTyped } from '@peertube/peertube-core-utils'
@@ -25,6 +25,7 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
private auth = inject(AuthService)
private videoService = inject(VideoService)
private notifier = inject(Notifier)
+ private userService = inject(UserService)
private cd = inject(ChangeDetectorRef)
readonly sort = input
(undefined)
@@ -42,19 +43,14 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
readonly loaded = output()
videos: Video[]
+ user: User
displayOptions: MiniatureDisplayOptions = {
date: false,
views: true,
by: true,
avatar: true,
- privacyLabel: false,
- privacyText: false,
- blacklistInfo: false
- }
-
- getUser () {
- return this.auth.getUser()
+ privacyLabel: false
}
limitRowsStyle () {
@@ -74,7 +70,10 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
}
}
- return this.getVideosObservable()
+ this.userService.getAnonymousOrLoggedUser()
+ .subscribe(user => this.user = user)
+
+ this.getVideosObservable()
.pipe(finalize(() => this.loaded.emit(true)))
.subscribe({
next: data => {
@@ -106,19 +105,19 @@ export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit
const channelHandle = this.channelHandle()
const accountHandle = this.accountHandle()
if (channelHandle) {
- obs = this.videoService.getVideoChannelVideos({
+ obs = this.videoService.listChannelVideos({
...options,
videoChannel: { nameWithHost: channelHandle }
})
} else if (accountHandle) {
- obs = this.videoService.getAccountVideos({
+ obs = this.videoService.listAccountVideos({
...options,
account: { nameWithHost: accountHandle }
})
} else {
- obs = this.videoService.getVideos(options)
+ obs = this.videoService.listVideos(options)
}
return obs.pipe(map(({ data }) => data))
diff --git a/client/src/app/shared/shared-forms/peertube-checkbox.component.html b/client/src/app/shared/shared-forms/peertube-checkbox.component.html
index 5284d066e..cf1f851c4 100644
--- a/client/src/app/shared/shared-forms/peertube-checkbox.component.html
+++ b/client/src/app/shared/shared-forms/peertube-checkbox.component.html
@@ -6,7 +6,7 @@
[(ngModel)]="checked"
(ngModelChange)="onModelChange()"
[id]="inputName()"
- [disabled]="disabled()"
+ [disabled]="disabled"
[attr.aria-describedby]="inputName() + '-description'"
/>
diff --git a/client/src/app/shared/shared-forms/peertube-checkbox.component.ts b/client/src/app/shared/shared-forms/peertube-checkbox.component.ts
index 1e3b01ea6..131576356 100644
--- a/client/src/app/shared/shared-forms/peertube-checkbox.component.ts
+++ b/client/src/app/shared/shared-forms/peertube-checkbox.component.ts
@@ -23,9 +23,9 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor, AfterCon
readonly labelText = input(undefined)
readonly labelInnerHTML = input(undefined)
readonly helpPlacement = input('top auto')
- readonly disabled = model(false)
readonly recommended = input(false)
+ disabled = false
describedby: string
readonly templates = contentChildren(PeerTubeTemplateDirective)
@@ -66,6 +66,6 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor, AfterCon
}
setDisabledState (isDisabled: boolean) {
- this.disabled.set(isDisabled)
+ this.disabled = isDisabled
}
}
diff --git a/client/src/app/shared/shared-forms/select/select-radio.component.html b/client/src/app/shared/shared-forms/select/select-radio.component.html
new file mode 100644
index 000000000..823790639
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/select-radio.component.html
@@ -0,0 +1,34 @@
+
+
{{ label() }}
+
+
+
+ @if (isGroup()) {
+
+ @for (item of items(); track item.id) {
+
+
+ {{ item.label }}
+ }
+
+ } @else {
+ @for (item of items(); track item.id) {
+
+
+
+
{{ item.label }}
+
+
+ {{ item.description}}
+
+
+ }
+ }
+
diff --git a/client/src/app/shared/shared-forms/select/select-radio.component.scss b/client/src/app/shared/shared-forms/select/select-radio.component.scss
new file mode 100644
index 000000000..97df6242c
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/select-radio.component.scss
@@ -0,0 +1,37 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+
+label {
+ display: block;
+
+ &.label-secondary {
+ font-size: 1rem;
+ font-weight: normal;
+ color: pvar(--fg);
+ }
+}
+
+.peertube-radio-container {
+ margin-bottom: 0.25rem;
+}
+
+.peertube-radio-container .form-group-description {
+ margin-bottom: 0;
+}
+
+// Prevent layout shift on bold
+.btn-outline-primary {
+ &::after {
+ display: block;
+ content: attr(data-label);
+ font-weight: bold;
+ height: 0;
+ overflow: hidden;
+ visibility: hidden;
+ }
+}
+
+.btn-check:checked + .btn-outline-primary {
+ font-weight: $font-bold;
+ letter-spacing: 0;
+}
diff --git a/client/src/app/shared/shared-forms/select/select-radio.component.ts b/client/src/app/shared/shared-forms/select/select-radio.component.ts
new file mode 100644
index 000000000..a17962974
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/select-radio.component.ts
@@ -0,0 +1,69 @@
+import { CommonModule } from '@angular/common'
+import { booleanAttribute, Component, forwardRef, inject, input, model } from '@angular/core'
+import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
+import { ScreenService } from '@app/core'
+import { SelectRadioItem } from 'src/types'
+
+@Component({
+ selector: 'my-select-radio',
+
+ templateUrl: './select-radio.component.html',
+ styleUrls: [ './select-radio.component.scss' ],
+
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SelectRadioComponent),
+ multi: true
+ }
+ ],
+ imports: [ FormsModule, CommonModule ]
+})
+export class SelectRadioComponent implements ControlValueAccessor {
+ readonly items = input.required()
+ readonly inputId = input.required()
+
+ readonly label = input()
+ readonly isGroup = input(false, { transform: booleanAttribute })
+ readonly labelSecondary = input(false, { transform: booleanAttribute })
+
+ private readonly screenService = inject(ScreenService)
+
+ readonly value = model('')
+
+ disabled = false
+
+ wroteValue: number | string
+
+ propagateChange = (_: any) => {
+ // empty
+ }
+
+ writeValue (value: string) {
+ this.value.set(value)
+ }
+
+ registerOnChange (fn: (_: any) => void) {
+ this.propagateChange = fn
+ }
+
+ registerOnTouched () {
+ // Unused
+ }
+
+ setDisabledState (isDisabled: boolean) {
+ this.disabled = isDisabled
+ }
+
+ update () {
+ this.propagateChange(this.value())
+ }
+
+ getRadioId (item: SelectRadioItem) {
+ return this.inputId() + '-' + item.id
+ }
+
+ isInMobileView () {
+ return this.screenService.isInMobileView()
+ }
+}
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts
index 5b2aa330a..f30c3de20 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -79,6 +79,7 @@ const icons = {
'ownership-change': require('../../../assets/images/feather/share.svg'),
'p2p': require('../../../assets/images/feather/airplay.svg'),
'play': require('../../../assets/images/feather/play.svg'),
+ 'circle-alert': require('../../../assets/images/feather/circle-alert.svg'),
'playlists': require('../../../assets/images/feather/playlists.svg'),
'refresh': require('../../../assets/images/feather/refresh-cw.svg'),
'repeat': require('../../../assets/images/feather/repeat.svg'),
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html
index d5a4aefcb..90cd662c8 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.html
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.html
@@ -10,7 +10,7 @@
- Default NSFW/sensitive videos policy
+ Default sensitive content policy
can be redefined by the users
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts
index 98e9de00e..19062cf3b 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.ts
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts
@@ -66,7 +66,8 @@ export class InstanceFeaturesTableComponent implements OnInit {
const policy = this.serverConfig().instance.defaultNSFWPolicy
if (policy === 'do_not_list') return $localize`Hidden`
- if (policy === 'blur') return $localize`Blurred with confirmation request`
+ if (policy === 'warn') return $localize`Warn users`
+ if (policy === 'blur') return $localize`Warn users and blur thumbnail`
if (policy === 'display') return $localize`Displayed`
}
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index c0e759807..519c9493b 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -19,6 +19,7 @@ import {
VideoStreamingPlaylist,
VideoStreamingPlaylistType
} from '@peertube/peertube-models'
+import { isVideoNSFWBlurForUser, isVideoNSFWHiddenForUser, isVideoNSFWWarnedForUser } from '@root-helpers/video'
export class Video implements VideoServerModel {
byVideoChannel: string
@@ -68,7 +69,10 @@ export class Video implements VideoServerModel {
likes: number
dislikes: number
+
nsfw: boolean
+ nsfwFlags: number
+ nsfwSummary: string
originInstanceUrl: string
originInstanceHost: string
@@ -176,6 +180,8 @@ export class Video implements VideoServerModel {
this.dislikes = hash.dislikes
this.nsfw = hash.nsfw
+ this.nsfwFlags = hash.nsfwFlags
+ this.nsfwSummary = hash.nsfwSummary
this.account = hash.account
this.channel = hash.channel
@@ -217,15 +223,16 @@ export class Video implements VideoServerModel {
this.comments = hash.comments
}
- isVideoNSFWForUser (user: User, serverConfig: HTMLServerConfig) {
- // Video is not NSFW, skip
- if (this.nsfw === false) return false
+ isVideoNSFWWarnedForUser (user: User, serverConfig: HTMLServerConfig) {
+ return isVideoNSFWWarnedForUser(this, serverConfig, user)
+ }
- // Return user setting if logged in
- if (user) return user.nsfwPolicy !== 'display'
+ isVideoNSFWBlurForUser (user: User, serverConfig: HTMLServerConfig) {
+ return isVideoNSFWBlurForUser(this, serverConfig, user)
+ }
- // Return default instance config
- return serverConfig.instance.defaultNSFWPolicy !== 'display'
+ isVideoNSFWHiddenForUser (user: User, serverConfig: HTMLServerConfig) {
+ return isVideoNSFWHiddenForUser(this, serverConfig, user)
}
isRemovableBy (user: AuthUser) {
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 20214ae49..9edded864 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -18,6 +18,7 @@ import {
FeedFormatType,
FeedType,
FeedType_Type,
+ NSFWFlag,
NSFWPolicyType,
ResultList,
ServerErrorCode,
@@ -30,7 +31,6 @@ import {
VideoDetails as VideoDetailsServerModel,
VideoFile,
VideoFileMetadata,
- VideoIncludeType,
VideoPrivacy,
VideoPrivacyType,
VideosCommonQuery,
@@ -45,26 +45,14 @@ import { from, Observable, of, throwError } from 'rxjs'
import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators'
import { environment } from '../../../../environments/environment'
import { Account } from '../account/account.model'
-import { AccountService } from '../account/account.service'
import { VideoChannel } from '../channel/video-channel.model'
-import { VideoChannelService } from '../channel/video-channel.service'
import { VideoDetails } from './video-details.model'
import { VideoPasswordService } from './video-password.service'
import { Video } from './video.model'
-export type CommonVideoParams = {
+export type CommonVideoParams = Omit & {
videoPagination?: ComponentPaginationLight
sort: VideoSortField | SortMeta
- include?: VideoIncludeType
- isLocal?: boolean
- categoryOneOf?: number[]
- languageOneOf?: string[]
- privacyOneOf?: VideoPrivacyType[]
- isLive?: boolean
- skipCount?: boolean
- nsfw?: BooleanBothQuery
- host?: string
- search?: string
}
@Injectable()
@@ -75,6 +63,7 @@ export class VideoService {
private restService = inject(RestService)
private serverService = inject(ServerService)
private confirmService = inject(ConfirmService)
+ private userService = inject(UserService)
static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos'
static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.'
@@ -113,6 +102,17 @@ export class VideoService {
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
+ removeVideo (idArg: number | number[]) {
+ const ids = arrayify(idArg)
+
+ return from(ids)
+ .pipe(
+ concatMap(id => this.authHttp.delete(`${VideoService.BASE_VIDEO_URL}/${id}`)),
+ toArray(),
+ catchError(err => this.restExtractor.handleError(err))
+ )
+ }
+
listMyVideos (options: {
videoPagination?: ComponentPaginationLight
restPagination?: RestPagination
@@ -154,45 +154,30 @@ export class VideoService {
)
}
- getAccountVideos (
+ listAccountVideos (
options: CommonVideoParams & {
account: Pick
}
): Observable> {
- const { account, ...parameters } = options
-
- let params = new HttpParams()
- params = this.buildCommonVideosParams({ params, ...parameters })
-
- return this.authHttp
- .get>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
- .pipe(
- switchMap(res => this.extractVideos(res)),
- catchError(err => this.restExtractor.handleError(err))
- )
+ return this.listVideos({ ...options, videoChannel: options.account })
}
- getVideoChannelVideos (
- parameters: CommonVideoParams & {
+ listChannelVideos (
+ options: CommonVideoParams & {
videoChannel: Pick
}
): Observable> {
- const { videoChannel } = parameters
-
- let params = new HttpParams()
- params = this.buildCommonVideosParams({ params, ...parameters })
-
- return this.authHttp
- .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
- .pipe(
- switchMap(res => this.extractVideos(res)),
- catchError(err => this.restExtractor.handleError(err))
- )
+ return this.listVideos({ ...options, videoChannel: options.videoChannel })
}
- getVideos (parameters: CommonVideoParams): Observable> {
+ listVideos (
+ options: CommonVideoParams & {
+ videoChannel?: Pick
+ account?: Pick
+ }
+ ): Observable> {
let params = new HttpParams()
- params = this.buildCommonVideosParams({ params, ...parameters })
+ params = this.buildCommonVideosParams({ params, ...options })
return this.authHttp
.get>(VideoService.BASE_VIDEO_URL, { params })
@@ -202,6 +187,87 @@ export class VideoService {
)
}
+ buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
+ const {
+ params,
+ videoPagination,
+ sort,
+ categoryOneOf,
+ languageOneOf,
+ privacyOneOf,
+ skipCount,
+ search,
+ nsfw,
+ nsfwFlagsExcluded,
+ nsfwFlagsIncluded,
+
+ ...otherOptions
+ } = options
+
+ const pagination = videoPagination
+ ? this.restService.componentToRestPagination(videoPagination)
+ : undefined
+
+ let newParams = this.restService.addRestGetParams(params, pagination, this.buildListSort(sort))
+
+ if (skipCount) newParams = newParams.set('skipCount', skipCount + '')
+ if (languageOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
+ if (categoryOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
+ if (privacyOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'privacyOneOf', privacyOneOf)
+ if (search) newParams = newParams.set('search', search)
+
+ newParams = this.buildNSFWParams(newParams, { nsfw, nsfwFlagsExcluded, nsfwFlagsIncluded })
+
+ return this.restService.addObjectParams(newParams, otherOptions)
+ }
+
+ buildNSFWParams (params: HttpParams, options: Pick = {}) {
+ const { nsfw, nsfwFlagsExcluded, nsfwFlagsIncluded } = options
+
+ const anonymous = this.auth.isLoggedIn()
+ ? undefined
+ : this.userService.getAnonymousUser()
+
+ const anonymousFlagsExcluded = anonymous
+ ? anonymous.nsfwFlagsHidden
+ : undefined
+
+ const anonymousFlagsIncluded = anonymous
+ ? anonymous.nsfwFlagsDisplayed | anonymous.nsfwFlagsBlurred | anonymous.nsfwFlagsWarned
+ : undefined
+
+ if (nsfw !== undefined) params = params.set('nsfw', nsfw)
+ else if (anonymous?.nsfwPolicy) params = params.set('nsfw', this.nsfwPolicyToParam(anonymous.nsfwPolicy))
+
+ if (nsfwFlagsExcluded !== undefined) params = params.set('nsfwFlagsExcluded', nsfwFlagsExcluded)
+ else if (anonymousFlagsExcluded !== undefined) params = params.set('nsfwFlagsExcluded', anonymousFlagsExcluded)
+
+ if (nsfwFlagsIncluded !== undefined) params = params.set('nsfwFlagsIncluded', nsfwFlagsIncluded)
+ else if (anonymousFlagsIncluded !== undefined) params = params.set('nsfwFlagsIncluded', anonymousFlagsIncluded)
+
+ return params
+ }
+
+ private buildListSort (sortArg: VideoSortField | SortMeta) {
+ const sort = this.restService.buildSortString(sortArg)
+
+ if (typeof sort === 'string') {
+ // Silently use the best algorithm for logged in users if they chose the hot algorithm
+ if (
+ this.auth.isLoggedIn() &&
+ (sort === 'hot' || sort === '-hot')
+ ) {
+ return sort.replace('hot', 'best')
+ }
+
+ return sort
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // Video feeds
+ // ---------------------------------------------------------------------------
+
buildBaseFeedUrls (params: HttpParams, base = VideoService.BASE_FEEDS_URL) {
const feeds: { type: FeedType_Type, format: FeedFormatType, label: string, url: string }[] = [
{
@@ -278,6 +344,10 @@ export class VideoService {
return this.buildBaseFeedUrls(params, VideoService.BASE_SUBSCRIPTION_FEEDS_URL)
}
+ // ---------------------------------------------------------------------------
+ // Video files
+ // ---------------------------------------------------------------------------
+
getVideoFileMetadata (metadataUrl: string) {
return this.authHttp
.get(metadataUrl)
@@ -286,17 +356,6 @@ export class VideoService {
)
}
- removeVideo (idArg: number | number[]) {
- const ids = arrayify(idArg)
-
- return from(ids)
- .pipe(
- concatMap(id => this.authHttp.delete(`${VideoService.BASE_VIDEO_URL}/${id}`)),
- toArray(),
- catchError(err => this.restExtractor.handleError(err))
- )
- }
-
removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'web-videos') {
return from(videoIds)
.pipe(
@@ -316,6 +375,8 @@ export class VideoService {
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
+ // ---------------------------------------------------------------------------
+
runTranscoding (options: {
videos: Video[]
type: 'hls' | 'web-video'
@@ -475,6 +536,28 @@ export class VideoService {
}
}
+ buildNSFWTooltip (video: Pick) {
+ const flags: string[] = []
+
+ if ((video.nsfwFlags & NSFWFlag.VIOLENT) === NSFWFlag.VIOLENT) {
+ flags.push($localize`violence`)
+ }
+
+ if ((video.nsfwFlags & NSFWFlag.SHOCKING_DISTURBING) === NSFWFlag.SHOCKING_DISTURBING) {
+ flags.push($localize`shocking content`)
+ }
+
+ if ((video.nsfwFlags & NSFWFlag.EXPLICIT_SEX) === NSFWFlag.EXPLICIT_SEX) {
+ flags.push($localize`explicit sex`)
+ }
+
+ if (flags.length === 0) {
+ return $localize`This video contains sensitive content`
+ }
+
+ return $localize`This video contains sensitive content: ${flags.join(' - ')}`
+ }
+
getHighestAvailablePrivacy (serverPrivacies: VideoConstant[]) {
// We do not add a password as this requires additional configuration.
const order = [
@@ -517,64 +600,6 @@ export class VideoService {
return 'videoChannel' as 'videoChannel'
}
- buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) {
- const {
- params,
- videoPagination,
- sort,
- isLocal,
- include,
- categoryOneOf,
- languageOneOf,
- privacyOneOf,
- skipCount,
- isLive,
- nsfw,
- search,
- host,
-
- ...otherOptions
- } = options
-
- const pagination = videoPagination
- ? this.restService.componentToRestPagination(videoPagination)
- : undefined
-
- let newParams = this.restService.addRestGetParams(params, pagination, this.buildListSort(sort))
-
- if (skipCount) newParams = newParams.set('skipCount', skipCount + '')
-
- if (isLocal !== undefined) newParams = newParams.set('isLocal', isLocal)
- if (include !== undefined) newParams = newParams.set('include', include)
- if (isLive !== undefined) newParams = newParams.set('isLive', isLive)
- if (nsfw !== undefined) newParams = newParams.set('nsfw', nsfw)
- if (languageOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
- if (categoryOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
- if (privacyOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'privacyOneOf', privacyOneOf)
- if (search) newParams = newParams.set('search', search)
- if (host) newParams = newParams.set('host', host)
-
- newParams = this.restService.addObjectParams(newParams, otherOptions)
-
- return newParams
- }
-
- private buildListSort (sortArg: VideoSortField | SortMeta) {
- const sort = this.restService.buildSortString(sortArg)
-
- if (typeof sort === 'string') {
- // Silently use the best algorithm for logged in users if they chose the hot algorithm
- if (
- this.auth.isLoggedIn() &&
- (sort === 'hot' || sort === '-hot')
- ) {
- return sort.replace('hot', 'best')
- }
-
- return sort
- }
- }
-
private setVideoRate (id: string, rateType: UserVideoRateType, videoPassword?: string) {
const url = `${VideoService.BASE_VIDEO_URL}/${id}/rate`
const body: UserVideoRateUpdate = {
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts
index b977a4801..43fbb6341 100644
--- a/client/src/app/shared/shared-search/advanced-search.model.ts
+++ b/client/src/app/shared/shared-search/advanced-search.model.ts
@@ -1,6 +1,5 @@
import { splitIntoArray } from '@app/helpers'
import {
- BooleanBothQuery,
BooleanQuery,
SearchTargetType,
VideoChannelsSearchQuery,
@@ -17,8 +16,6 @@ export class AdvancedSearch {
originallyPublishedStartDate: string // ISO 8601
originallyPublishedEndDate: string // ISO 8601
- nsfw: BooleanBothQuery
-
categoryOneOf: string
licenceOneOf: string
@@ -47,7 +44,6 @@ export class AdvancedSearch {
endDate?: string
originallyPublishedStartDate?: string
originallyPublishedEndDate?: string
- nsfw?: BooleanBothQuery
categoryOneOf?: string
licenceOneOf?: string
languageOneOf?: string
@@ -74,7 +70,6 @@ export class AdvancedSearch {
this.originallyPublishedStartDate = options.originallyPublishedStartDate || undefined
this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined
- this.nsfw = options.nsfw || undefined
this.isLive = options.isLive || undefined
this.categoryOneOf = options.categoryOneOf || undefined
@@ -112,7 +107,6 @@ export class AdvancedSearch {
this.endDate = undefined
this.originallyPublishedStartDate = undefined
this.originallyPublishedEndDate = undefined
- this.nsfw = undefined
this.categoryOneOf = undefined
this.licenceOneOf = undefined
this.languageOneOf = undefined
@@ -132,7 +126,6 @@ export class AdvancedSearch {
endDate: this.endDate,
originallyPublishedStartDate: this.originallyPublishedStartDate,
originallyPublishedEndDate: this.originallyPublishedEndDate,
- nsfw: this.nsfw,
categoryOneOf: this.categoryOneOf,
licenceOneOf: this.licenceOneOf,
languageOneOf: this.languageOneOf,
@@ -158,7 +151,6 @@ export class AdvancedSearch {
endDate: this.endDate,
originallyPublishedStartDate: this.originallyPublishedStartDate,
originallyPublishedEndDate: this.originallyPublishedEndDate,
- nsfw: this.nsfw,
categoryOneOf: splitIntoArray(this.categoryOneOf),
licenceOneOf: splitIntoArray(this.licenceOneOf),
languageOneOf: splitIntoArray(this.languageOneOf),
@@ -194,7 +186,6 @@ export class AdvancedSearch {
if (this.isValidValue(this.startDate) || this.isValidValue(this.endDate)) acc++
if (this.isValidValue(this.originallyPublishedStartDate) || this.isValidValue(this.originallyPublishedEndDate)) acc++
- if (this.isValidValue(this.nsfw)) acc++
if (this.isValidValue(this.categoryOneOf)) acc++
if (this.isValidValue(this.licenceOneOf)) acc++
if (this.isValidValue(this.languageOneOf)) acc++
@@ -221,7 +212,6 @@ export class AdvancedSearch {
this.endDate !== undefined ||
this.originallyPublishedStartDate !== undefined ||
this.originallyPublishedEndDate !== undefined ||
- this.nsfw !== undefined ||
this.categoryOneOf !== undefined ||
this.licenceOneOf !== undefined ||
this.languageOneOf !== undefined ||
diff --git a/client/src/app/shared/shared-search/search.service.ts b/client/src/app/shared/shared-search/search.service.ts
index 795899437..153b4bae8 100644
--- a/client/src/app/shared/shared-search/search.service.ts
+++ b/client/src/app/shared/shared-search/search.service.ts
@@ -57,9 +57,12 @@ export class SearchService {
if (advancedSearch) {
const advancedSearchObject = advancedSearch.toVideosAPIObject()
+
params = this.restService.addObjectParams(params, advancedSearchObject)
}
+ params = this.videoService.buildNSFWParams(params, {})
+
return this.authHttp
.get>(url, { params })
.pipe(
diff --git a/client/src/app/shared/shared-tables/video-cell.component.html b/client/src/app/shared/shared-tables/video-cell.component.html
index 16fc28810..4dbc97f41 100644
--- a/client/src/app/shared/shared-tables/video-cell.component.html
+++ b/client/src/app/shared/shared-tables/video-cell.component.html
@@ -1,12 +1,12 @@
-
+
{{ video().name }}
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html
index aad0a6243..afe79c8ed 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html
@@ -9,7 +9,7 @@
}
-
+
-
-
+
+
@if (video().isLive) {
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
index 866b04771..a094913df 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
@@ -22,7 +22,6 @@
}
.watch-icon-overlay,
-.label-overlay,
.duration-overlay,
.live-overlay {
font-size: 0.75rem;
@@ -33,15 +32,11 @@
}
.label-overlay {
- border-radius: 3px;
position: absolute;
padding: 0 5px;
left: 5px;
top: 5px;
- font-weight: $font-bold;
-
- &.warning { background-color: #ffa500; }
- &.danger { background-color: pvar(--red); }
+ z-index: z(miniature);
}
.duration-overlay,
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
index 6f7ed994b..3db155d78 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
@@ -1,4 +1,4 @@
-import { NgClass, NgIf, NgStyle, NgTemplateOutlet } from '@angular/common'
+import { CommonModule } from '@angular/common'
import { booleanAttribute, Component, inject, input, OnChanges, output, viewChild } from '@angular/core'
import { RouterLink } from '@angular/router'
import { ScreenService } from '@app/core'
@@ -27,13 +27,12 @@ export type VideoThumbnailInput = Pick<
selector: 'my-video-thumbnail',
styleUrls: [ './video-thumbnail.component.scss' ],
templateUrl: './video-thumbnail.component.html',
- imports: [ NgIf, RouterLink, NgTemplateOutlet, NgClass, NgbTooltip, GlobalIconComponent, NgStyle ]
+ imports: [ CommonModule, RouterLink, NgbTooltip, GlobalIconComponent ]
})
export class VideoThumbnailComponent implements OnChanges {
private screenService = inject(ScreenService)
readonly video = input.required
()
- readonly nsfw = input(false)
readonly videoRouterLink = input(undefined)
readonly queryParams = input<{
@@ -47,6 +46,7 @@ export class VideoThumbnailComponent implements OnChanges {
readonly playOverlay = input(true, { transform: booleanAttribute })
readonly ariaLabel = input.required()
+ readonly blur = input.required({ transform: booleanAttribute })
readonly watchLaterTooltip = viewChild('watchLaterTooltip')
readonly watchLaterClick = output()
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.html b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
index c499015fc..e069463e4 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.html
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
@@ -2,23 +2,36 @@
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
index 7363c4039..7a99c7c2d 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.scss
@@ -44,6 +44,10 @@ $filters-background: pvar(--bg-secondary-400);
}
}
+.form-group-description {
+ color: pvar(--fg-300);
+}
+
.filters {
--input-bg: #{pvar(--input-bg-in-secondary)};
--input-border-color: #{pvar(--input-bg-in-secondary)};
@@ -134,6 +138,11 @@ my-select-options {
}
@media screen and (max-width: $small-view) {
+ .scope-select {
+ min-width: auto;
+ max-width: 90%;
+ }
+
.filters-toggle {
margin-top: 0.5rem;
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.ts b/client/src/app/shared/shared-video-miniature/video-filters-header.component.ts
index d15d7fa99..667f1bab7 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.ts
@@ -81,7 +81,6 @@ export class VideoFiltersHeaderComponent implements OnInit {
this.form = this.fb.group({
sort: [ '' ],
- nsfw: [ '' ],
languageOneOf: [ '' ],
categoryOneOf: [ '' ],
scope: [ '' ],
diff --git a/client/src/app/shared/shared-video-miniature/video-filters.model.ts b/client/src/app/shared/shared-video-miniature/video-filters.model.ts
index 872d4ede0..c14503fcc 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters.model.ts
+++ b/client/src/app/shared/shared-video-miniature/video-filters.model.ts
@@ -1,11 +1,14 @@
+import { User } from '@app/core'
import { splitIntoArray, toBoolean } from '@app/helpers'
import { getAllPrivacies } from '@peertube/peertube-core-utils'
import {
BooleanBothQuery,
+ NSFWFlag,
NSFWPolicyType,
VideoInclude,
VideoIncludeType,
VideoPrivacyType,
+ VideosCommonQuery,
VideoSortField
} from '@peertube/peertube-models'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
@@ -29,7 +32,6 @@ export type VideoFilterActive = {
export class VideoFilters {
sort: VideoSortField
- nsfw: BooleanBothQuery
languageOneOf: string[]
categoryOneOf: number[]
@@ -41,9 +43,14 @@ export class VideoFilters {
search: string
+ private nsfwPolicy: NSFWPolicyType
+ private nsfwFlagsDisplayed: number
+ private nsfwFlagsHidden: number
+ private nsfwFlagsWarned: number
+ private nsfwFlagsBlurred: number
+
private defaultValues = new Map([
[ 'sort', '-publishedAt' ],
- [ 'nsfw', 'false' ],
[ 'languageOneOf', undefined ],
[ 'categoryOneOf', undefined ],
[ 'scope', 'federated' ],
@@ -53,7 +60,6 @@ export class VideoFilters {
])
private activeFilters: VideoFilterActive[] = []
- private defaultNSFWPolicy: NSFWPolicyType
private onChangeCallbacks: (() => void)[] = []
private oldFormObjectString: string
@@ -68,7 +74,7 @@ export class VideoFilters {
this.hiddenFields = hiddenFields
- this.reset(undefined, false)
+ this.reset({ triggerChange: false })
}
// ---------------------------------------------------------------------------
@@ -106,21 +112,23 @@ export class VideoFilters {
this.defaultValues.set('sort', sort)
}
- setNSFWPolicy (nsfwPolicy: NSFWPolicyType) {
- const nsfw = nsfwPolicy === 'do_not_list'
- ? 'false'
- : 'both'
-
- this.defaultValues.set('nsfw', nsfw)
- this.defaultNSFWPolicy = nsfwPolicy
-
- return nsfw
+ setNSFWPolicy (user: Pick) {
+ this.nsfwPolicy = user.nsfwPolicy
+ this.nsfwFlagsDisplayed = user.nsfwFlagsDisplayed
+ this.nsfwFlagsHidden = user.nsfwFlagsHidden
+ this.nsfwFlagsWarned = user.nsfwFlagsWarned
+ this.nsfwFlagsBlurred = user.nsfwFlagsBlurred
}
// ---------------------------------------------------------------------------
- reset (specificKey?: string, triggerChange = true) {
- debugLogger('Reset video filters', { specificKey, stack: new Error().stack })
+ private reset (options: {
+ specificKey?: string
+ triggerChange?: boolean // default true
+ }) {
+ const { specificKey, triggerChange = true } = options
+
+ debugLogger('Reset video filters', { specificKey })
for (const [ key, value ] of this.defaultValues) {
if (specificKey && specificKey !== key) continue
@@ -143,8 +151,6 @@ export class VideoFilters {
if (obj.sort !== undefined) this.sort = obj.sort
- if (obj.nsfw !== undefined) this.nsfw = obj.nsfw
-
if (obj.languageOneOf !== undefined) this.languageOneOf = splitIntoArray(obj.languageOneOf)
if (obj.categoryOneOf !== undefined) this.categoryOneOf = splitIntoArray(obj.categoryOneOf)
@@ -163,7 +169,14 @@ export class VideoFilters {
debugLogger('Cloning video filters', { videoFilters: this })
const cloned = new VideoFilters(this.defaultValues.get('sort'), this.defaultValues.get('scope'), this.hiddenFields)
- cloned.setNSFWPolicy(this.defaultNSFWPolicy)
+
+ cloned.setNSFWPolicy({
+ nsfwPolicy: this.nsfwPolicy,
+ nsfwFlagsDisplayed: this.nsfwFlagsDisplayed,
+ nsfwFlagsHidden: this.nsfwFlagsHidden,
+ nsfwFlagsWarned: this.nsfwFlagsWarned,
+ nsfwFlagsBlurred: this.nsfwFlagsBlurred
+ })
cloned.load(this.toUrlObject(), this.customizedByUser)
@@ -290,8 +303,9 @@ export class VideoFilters {
else if (this.live === 'false') isLive = false
return {
+ ...this.buildNSFWVideosAPIObject(),
+
sort: this.sort,
- nsfw: this.nsfw,
languageOneOf: this.languageOneOf,
categoryOneOf: this.categoryOneOf,
search: this.search,
@@ -302,18 +316,66 @@ export class VideoFilters {
}
}
+ private buildNSFWVideosAPIObject (): Partial> {
+ if (this.allVideos) {
+ return { nsfw: 'both', nsfwFlagsExcluded: NSFWFlag.NONE }
+ }
+
+ const nsfw: BooleanBothQuery = this.nsfwPolicy === 'do_not_list'
+ ? 'false'
+ : 'both'
+
+ let nsfwFlagsIncluded = NSFWFlag.NONE
+ let nsfwFlagsExcluded = NSFWFlag.NONE
+
+ if (this.nsfwPolicy === 'do_not_list') {
+ nsfwFlagsIncluded |= this.nsfwFlagsDisplayed
+ nsfwFlagsIncluded |= this.nsfwFlagsWarned
+ nsfwFlagsIncluded |= this.nsfwFlagsBlurred
+ } else {
+ nsfwFlagsExcluded |= this.nsfwFlagsHidden
+ }
+
+ return { nsfw, nsfwFlagsIncluded, nsfwFlagsExcluded }
+ }
+
// ---------------------------------------------------------------------------
- getNSFWDisplayLabel () {
- if (this.defaultNSFWPolicy === 'blur') return $localize`Blurred`
+ getNSFWSettingsLabel () {
+ let result = this.getGlobalNSFWLabel()
- return $localize`Displayed`
+ if (this.hasCustomNSFWFlags()) {
+ result += $localize` Some videos with a specific sensitive content category have a different policy.`
+ }
+
+ return result
+ }
+
+ private getGlobalNSFWLabel () {
+ if (this.nsfwPolicy === 'do_not_list') return $localize`Sensitive content hidden.`
+ if (this.nsfwPolicy === 'warn') return $localize`Sensitive content has a warning.`
+ if (this.nsfwPolicy === 'blur') return $localize`Sensitive content has a warning and the thumbnail is blurred.`
+
+ return $localize`Sensitive content is displayed.`
}
private getNSFWValue () {
- if (this.nsfw === 'false') return $localize`hidden`
- if (this.defaultNSFWPolicy === 'blur') return $localize`blurred`
+ if (this.hasCustomNSFWFlags()) {
+ if (this.nsfwPolicy === 'do_not_list') return $localize`hidden (with exceptions)`
+ if (this.nsfwPolicy === 'warn') return $localize`warned (with exceptions)`
+ if (this.nsfwPolicy === 'blur') return $localize`blurred (with exceptions)`
+
+ return $localize`displayed (with exceptions)`
+ }
+
+ if (this.nsfwPolicy === 'do_not_list') return $localize`hidden`
+ if (this.nsfwPolicy === 'warn') return $localize`warned`
+ if (this.nsfwPolicy === 'blur') return $localize`blurred`
return $localize`displayed`
}
+
+ private hasCustomNSFWFlags () {
+ return this.nsfwFlagsDisplayed || this.nsfwFlagsHidden || this.nsfwFlagsWarned || this.nsfwFlagsBlurred
+ }
}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index b0bb0d7d5..65d95d439 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -1,16 +1,25 @@
+
- @if (displayOptions().privacyLabel) {
- Unlisted
- Private
- Password protected
- }
+
+ Unlisted
+ Private
+ Password protected
+
+
+
+
+
+
@if (displayOptions().avatar) {
@@ -43,6 +52,10 @@
+
+ @if (!displayAsRow()) {
+
+ }
@@ -50,7 +63,7 @@
{{ video().name }}
@@ -61,6 +74,10 @@
(videoRemoved)="onVideoRemoved()" (videoBlocked)="onVideoBlocked()" (videoUnblocked)="onVideoUnblocked()" (videoAccountMuted)="onVideoAccountMuted()"
>
+
+ @if (displayAsRow() || (!displayOptions().avatar && !displayOptions().by)) {
+
+ }
@@ -72,22 +89,5 @@
-
-
- {{ video().privacy.label }}
-
-
-
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index cdb374763..7e90ff1e8 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -32,19 +32,21 @@ $more-button-width: 40px;
font-size: var(--co-fs-medium);
}
-.date-and-views,
-.video-info-privacy,
-.badges {
+.date-and-views {
font-size: var(--co-fs-small);
+ color: pvar(--fg-200);
}
-.date-and-views {
- color: pvar(--fg-200);
+.nsfw-warning {
+ my-global-icon {
+ @include global-icon-size(18px);
+ }
}
.owner-container {
display: flex;
min-width: 1px;
+ width: 100%;
}
my-actor-host {
@@ -144,14 +146,11 @@ my-actor-host {
margin-bottom: 25px;
.video-info {
- margin: 0 10px;
-
width: 100%;
text-align: left;
}
.video-actions {
- margin: 0;
top: -3px;
width: auto;
@@ -208,18 +207,6 @@ my-actor-host {
my-actor-avatar {
@include margin-right(0.5rem);
}
-
- .video-actions {
- margin-top: -3px;
- }
-}
-
-.badges {
- display: flex;
- flex-wrap: wrap;
- gap: 5px;
- font-size: 1rem;
- margin-top: 0.25rem;
}
@include on-small-main-col {
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index 5d37fd5ae..69aca6a7f 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -1,9 +1,8 @@
-import { NgClass, NgFor, NgIf } from '@angular/common'
+import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- LOCALE_ID,
OnInit,
booleanAttribute,
inject,
@@ -11,12 +10,13 @@ import {
numberAttribute,
output
} from '@angular/core'
-import { RouterLink } from '@angular/router'
import { AuthService, ScreenService, ServerService, User } from '@app/core'
-import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy } from '@peertube/peertube-models'
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
+import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy } from '@peertube/peertube-models'
import { switchMap } from 'rxjs/operators'
import { LinkType } from '../../../types/link.type'
import { ActorAvatarComponent } from '../shared-actor-image/actor-avatar.component'
+import { GlobalIconComponent } from '../shared-icons/global-icon.component'
import { LinkComponent } from '../shared-main/common/link.component'
import { DateToggleComponent } from '../shared-main/date/date-toggle.component'
import { Video } from '../shared-main/video/video.model'
@@ -32,9 +32,6 @@ export type MiniatureDisplayOptions = {
views?: boolean
avatar?: boolean
privacyLabel?: boolean
- privacyText?: boolean
- blacklistInfo?: boolean
- nsfw?: boolean
by?: boolean
forceChannelInBy?: boolean
@@ -46,17 +43,16 @@ export type MiniatureDisplayOptions = {
templateUrl: './video-miniature.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
- NgClass,
+ CommonModule,
VideoThumbnailComponent,
- NgIf,
ActorAvatarComponent,
LinkComponent,
DateToggleComponent,
VideoViewsCounterComponent,
- RouterLink,
- NgFor,
VideoActionsDropdownComponent,
- ActorHostComponent
+ ActorHostComponent,
+ GlobalIconComponent,
+ NgbTooltipModule
]
})
export class VideoMiniatureComponent implements OnInit {
@@ -66,11 +62,9 @@ export class VideoMiniatureComponent implements OnInit {
private videoPlaylistService = inject(VideoPlaylistService)
private videoService = inject(VideoService)
private cd = inject(ChangeDetectorRef)
- private localeId = inject(LOCALE_ID)
- readonly user = input
(undefined)
- readonly video = input(undefined)
- readonly containedInPlaylists = input(undefined)
+ readonly user = input.required()
+ readonly video = input.required()
readonly displayOptions = input({
date: true,
@@ -78,8 +72,6 @@ export class VideoMiniatureComponent implements OnInit {
by: true,
avatar: true,
privacyLabel: false,
- privacyText: false,
- blacklistInfo: false,
forceChannelInBy: false
})
@@ -127,25 +119,27 @@ export class VideoMiniatureComponent implements OnInit {
ownerHref: string
ownerTarget: string
+ nsfwTooltip: string
+
private ownerDisplayType: 'account' | 'videoChannel'
private actionsLoaded = false
- get authorAccount () {
+ get preferAuthorDisplayName () {
return this.serverConfig.client.videos.miniature.preferAuthorDisplayName
+ }
+
+ get authorAccount () {
+ return this.preferAuthorDisplayName
? this.video().account.displayName
: this.video().account.name
}
get authorChannel () {
- return this.serverConfig.client.videos.miniature.preferAuthorDisplayName
+ return this.preferAuthorDisplayName
? this.video().channel.displayName
: this.video().channel.name
}
- get isVideoBlur () {
- return this.video().isVideoNSFWForUser(this.user(), this.serverConfig)
- }
-
ngOnInit () {
this.serverConfig = this.serverService.getHTMLConfig()
@@ -154,6 +148,7 @@ export class VideoMiniatureComponent implements OnInit {
this.setUpBy()
+ this.nsfwTooltip = this.videoService.buildNSFWTooltip(this.video())
this.channelLinkTitle = $localize`${this.video().channel.name} (channel page)`
// We rely on mouseenter to lazy load actions
@@ -162,7 +157,7 @@ export class VideoMiniatureComponent implements OnInit {
}
}
- buildVideoLink () {
+ private buildVideoLink () {
const videoLinkType = this.videoLinkType()
const video = this.video()
if (videoLinkType === 'internal' || !video.url) {
@@ -181,7 +176,7 @@ export class VideoMiniatureComponent implements OnInit {
this.videoRouterLink = [ '/search/lazy-load-video', { url: video.url } ]
}
- buildOwnerLink () {
+ private buildOwnerLink () {
const video = this.video()
const linkType = this.videoLinkType()
@@ -302,6 +297,18 @@ export class VideoMiniatureComponent implements OnInit {
}
}
+ // ---------------------------------------------------------------------------
+
+ hasNSFWWarning () {
+ return this.video().isVideoNSFWWarnedForUser(this.user(), this.serverConfig)
+ }
+
+ hasNSFWBlur () {
+ return this.video().isVideoNSFWBlurForUser(this.user(), this.serverConfig)
+ }
+
+ // ---------------------------------------------------------------------------
+
private setUpBy () {
if (this.displayOptions().forceChannelInBy) {
this.ownerDisplayType = 'videoChannel'
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
index 9c7f8a8fd..bb3b6eede 100644
--- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts
@@ -1,4 +1,4 @@
-import { NgClass, NgFor, NgIf } from '@angular/common'
+import { CommonModule } from '@angular/common'
import { Component, OnDestroy, OnInit, booleanAttribute, inject, input, output } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import {
@@ -50,9 +50,7 @@ enum GroupDate {
templateUrl: './videos-list.component.html',
styleUrls: [ './videos-list.component.scss' ],
imports: [
- NgIf,
- NgClass,
- NgFor,
+ CommonModule,
ButtonComponent,
ButtonComponent,
VideoFiltersHeaderComponent,
@@ -109,11 +107,9 @@ export class VideosListComponent implements OnInit, OnDestroy {
views: true,
by: true,
avatar: true,
- privacyLabel: true,
- privacyText: false,
- blacklistInfo: false
+ privacyLabel: true
}
- displayModerationBlock = false
+ displayModerationBlock = true
private routeSub: Subscription
private userSub: Subscription
@@ -268,9 +264,9 @@ export class VideosListComponent implements OnInit, OnDestroy {
}
private loadUserSettings (user: User) {
- const nsfw = this.filters.setNSFWPolicy(user.nsfwPolicy)
+ this.filters.setNSFWPolicy(user)
- this.filters.load({ languageOneOf: user.videoLanguages, nsfw })
+ this.filters.load({ languageOneOf: user.videoLanguages })
}
private reloadSyndicationItems () {
@@ -298,6 +294,8 @@ export class VideosListComponent implements OnInit, OnDestroy {
.subscribe(user => {
debugLogger('User changed', { user })
+ this.user = user
+
if (this.loadUserVideoPreferences()) {
this.loadUserSettings(user)
}
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
index 5618c1d0e..7b3b4229c 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
@@ -11,7 +11,6 @@
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
index 3e07b37b2..2593d161b 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
@@ -3,7 +3,7 @@ import { AfterContentInit, Component, contentChildren, inject, input, model, Tem
import { FormsModule } from '@angular/forms'
import { ComponentPagination, Notifier, resetCurrentPage, User } from '@app/core'
import { objectKeysTyped } from '@peertube/peertube-core-utils'
-import { ResultList, VideosExistInPlaylists, VideoSortField } from '@peertube/peertube-models'
+import { ResultList, VideoSortField } from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
import { Observable, Subject } from 'rxjs'
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
@@ -23,7 +23,6 @@ export type SelectionType = { [id: number]: boolean }
export class VideosSelectionComponent implements AfterContentInit {
private notifier = inject(Notifier)
- readonly videosContainedInPlaylists = input
(undefined)
readonly user = input(undefined)
readonly pagination = input(undefined)
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
index 956729a09..45781ff04 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
@@ -7,7 +7,7 @@
@@ -21,8 +21,18 @@
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
>{{ playlistElement().video.name }}
- Private
- Password protected
+ @if (isVideoPrivate()) {
+ Private
+ } @else if(isVideoPasswordProtected()) {
+ Password protected
+ }
+
+
+
+
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
index cf0de2285..188ec7667 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.scss
@@ -26,6 +26,12 @@ my-video-thumbnail,
@include margin-right(10px);
}
+.nsfw-warning {
+ my-global-icon {
+ @include global-icon-size(18px);
+ }
+}
+
.video {
display: grid;
grid-template-columns: 1fr auto;
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
index 6b293319b..8dee8a49e 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
@@ -1,23 +1,22 @@
+import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, inject, input, output, viewChild } from '@angular/core'
-import { AuthService, Notifier, ServerService } from '@app/core'
-import { NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownButtonItem, NgbDropdownItem } from '@ng-bootstrap/ng-bootstrap'
+import { FormsModule } from '@angular/forms'
+import { RouterLink } from '@angular/router'
+import { Notifier, ServerService, User, UserService } from '@app/core'
+import { NgbDropdown, NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { secondsToTime } from '@peertube/peertube-core-utils'
import { HTMLServerConfig, VideoPlaylistElementType, VideoPlaylistElementUpdate, VideoPrivacy } from '@peertube/peertube-models'
+import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
+import { TimestampInputComponent } from '../shared-forms/timestamp-input.component'
+import { GlobalIconComponent } from '../shared-icons/global-icon.component'
+import { DateToggleComponent } from '../shared-main/date/date-toggle.component'
+import { Video } from '../shared-main/video/video.model'
+import { VideoService } from '../shared-main/video/video.service'
+import { VideoThumbnailComponent } from '../shared-thumbnail/video-thumbnail.component'
+import { VideoViewsCounterComponent } from '../shared-video/video-views-counter.component'
import { VideoPlaylistElement } from './video-playlist-element.model'
import { VideoPlaylist } from './video-playlist.model'
import { VideoPlaylistService } from './video-playlist.service'
-import { TimestampInputComponent } from '../shared-forms/timestamp-input.component'
-import { FormsModule } from '@angular/forms'
-import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
-
-import { VideoViewsCounterComponent } from '../shared-video/video-views-counter.component'
-import { DateToggleComponent } from '../shared-main/date/date-toggle.component'
-import { VideoThumbnailComponent } from '../shared-thumbnail/video-thumbnail.component'
-import { GlobalIconComponent } from '../shared-icons/global-icon.component'
-import { RouterLink } from '@angular/router'
-import { NgClass, NgIf } from '@angular/common'
-import { Video } from '../shared-main/video/video.model'
-import { VideoService } from '../shared-main/video/video.service'
@Component({
selector: 'my-video-playlist-element-miniature',
@@ -25,25 +24,21 @@ import { VideoService } from '../shared-main/video/video.service'
templateUrl: './video-playlist-element-miniature.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
- NgClass,
+ CommonModule,
RouterLink,
- NgIf,
GlobalIconComponent,
VideoThumbnailComponent,
DateToggleComponent,
VideoViewsCounterComponent,
- NgbDropdown,
- NgbDropdownToggle,
- NgbDropdownMenu,
- NgbDropdownButtonItem,
- NgbDropdownItem,
+ NgbDropdownModule,
PeertubeCheckboxComponent,
FormsModule,
- TimestampInputComponent
+ TimestampInputComponent,
+ NgbTooltipModule
]
})
export class VideoPlaylistElementMiniatureComponent implements OnInit {
- private authService = inject(AuthService)
+ private userService = inject(UserService)
private serverService = inject(ServerService)
private notifier = inject(Notifier)
private videoPlaylistService = inject(VideoPlaylistService)
@@ -73,9 +68,14 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit {
} = {} as any
private serverConfig: HTMLServerConfig
+ private user: User
ngOnInit (): void {
this.serverConfig = this.serverService.getHTMLConfig()
+
+ this.userService.getAnonymousOrLoggedUser().subscribe(user => {
+ this.user = user
+ })
}
getVideoAriaLabel () {
@@ -125,8 +125,16 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit {
}
}
- isVideoBlur (video: Video) {
- return video.isVideoNSFWForUser(this.authService.getUser(), this.serverConfig)
+ hasNSFWBlur (video: Video) {
+ return video.isVideoNSFWBlurForUser(this.user, this.serverConfig)
+ }
+
+ hasNSFWWarning (video: Video) {
+ return video.isVideoNSFWWarnedForUser(this.user, this.serverConfig)
+ }
+
+ getNSFWTooltip (video: Video) {
+ return this.videoService.buildNSFWTooltip(video)
}
removeFromPlaylist (playlistElement: VideoPlaylistElement) {
diff --git a/client/src/app/shared/shared-video/video-nsfw-badge.component.html b/client/src/app/shared/shared-video/video-nsfw-badge.component.html
new file mode 100644
index 000000000..1e83db836
--- /dev/null
+++ b/client/src/app/shared/shared-video/video-nsfw-badge.component.html
@@ -0,0 +1,5 @@
+Sensitive
diff --git a/client/src/app/shared/shared-video/video-nsfw-badge.component.ts b/client/src/app/shared/shared-video/video-nsfw-badge.component.ts
new file mode 100644
index 000000000..b780cf9f6
--- /dev/null
+++ b/client/src/app/shared/shared-video/video-nsfw-badge.component.ts
@@ -0,0 +1,32 @@
+import { CommonModule } from '@angular/common'
+import { Component, inject, input, OnInit } from '@angular/core'
+import { Video } from '@peertube/peertube-models'
+import { VideoService } from '../shared-main/video/video.service'
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
+
+@Component({
+ selector: 'my-video-nsfw-badge',
+ templateUrl: './video-nsfw-badge.component.html',
+ standalone: true,
+ imports: [
+ CommonModule,
+ NgbTooltipModule
+ ]
+})
+export class VideoNSFWBadgeComponent implements OnInit {
+ private videoService = inject(VideoService)
+
+ readonly video = input.required>()
+ readonly theme = input<'yellow' | 'red'>('yellow')
+
+ tooltip: string
+ badgeClass: string
+
+ ngOnInit () {
+ this.tooltip = this.videoService.buildNSFWTooltip(this.video())
+
+ this.badgeClass = this.theme() === 'yellow'
+ ? 'badge-warning'
+ : 'badge-danger'
+ }
+}
diff --git a/client/src/assets/images/feather/circle-alert.svg b/client/src/assets/images/feather/circle-alert.svg
new file mode 100644
index 000000000..bce0713a5
--- /dev/null
+++ b/client/src/assets/images/feather/circle-alert.svg
@@ -0,0 +1 @@
+
diff --git a/client/src/root-helpers/index.ts b/client/src/root-helpers/index.ts
index 86301eafa..b4479f0ec 100644
--- a/client/src/root-helpers/index.ts
+++ b/client/src/root-helpers/index.ts
@@ -4,6 +4,7 @@ export * from './images'
export * from './local-storage-utils'
export * from './logger'
export * from './peertube-web-storage'
+export * from './theme-manager'
export * from './plugins-manager'
export * from './string'
export * from './url'
diff --git a/client/src/root-helpers/local-storage-utils.ts b/client/src/root-helpers/local-storage-utils.ts
index c2b3f9035..4a4e1cf5c 100644
--- a/client/src/root-helpers/local-storage-utils.ts
+++ b/client/src/root-helpers/local-storage-utils.ts
@@ -1,10 +1,15 @@
-function getBoolOrDefault (value: string, defaultValue: boolean) {
+export function getBoolOrDefault (value: string, defaultValue: boolean) {
if (value === 'true') return true
if (value === 'false') return false
return defaultValue
}
-export {
- getBoolOrDefault
+export function getNumberOrDefault (value: string, defaultValue: number) {
+ if (!value) return defaultValue
+
+ const result = parseInt(value, 10)
+ if (isNaN(result)) return defaultValue
+
+ return result
}
diff --git a/client/src/root-helpers/theme-manager.ts b/client/src/root-helpers/theme-manager.ts
new file mode 100644
index 000000000..d40af8b80
--- /dev/null
+++ b/client/src/root-helpers/theme-manager.ts
@@ -0,0 +1,213 @@
+import { sortBy } from '@peertube/peertube-core-utils'
+import { getLuminance, parse, toHSLA } from 'color-bits'
+import { ServerConfigTheme } from '@peertube/peertube-models'
+import { logger } from './logger'
+import debug from 'debug'
+
+const debugLogger = debug('peertube:theme')
+
+export class ThemeManager {
+ private oldInjectedProperties: string[] = []
+
+ injectTheme (theme: ServerConfigTheme, apiUrl: string) {
+ const head = this.getHeadElement()
+
+ const result: HTMLLinkElement[] = []
+
+ for (const css of theme.css) {
+ const link = document.createElement('link')
+
+ const href = apiUrl + `/themes/${theme.name}/${theme.version}/css/${css}`
+ link.setAttribute('href', href)
+ link.setAttribute('rel', 'alternate stylesheet')
+ link.setAttribute('type', 'text/css')
+ link.setAttribute('title', theme.name)
+ link.setAttribute('disabled', '')
+
+ head.appendChild(link)
+
+ result.push(link)
+ }
+
+ return result
+ }
+
+ loadThemeStyle (name: string) {
+ const links = document.getElementsByTagName('link')
+
+ for (let i = 0; i < links.length; i++) {
+ const link = links[i]
+ if (link.getAttribute('rel').includes('style') && link.getAttribute('title')) {
+ link.disabled = link.getAttribute('title') !== name
+
+ if (!link.disabled) {
+ link.onload = () => this.injectColorPalette()
+ } else {
+ link.onload = undefined
+ }
+ }
+ }
+
+ document.body.dataset.ptTheme = name
+ }
+
+ injectCoreColorPalette (iteration = 0) {
+ if (iteration > 10) {
+ logger.error('Cannot inject core color palette: too many iterations')
+ return
+ }
+
+ if (!this.canInjectCoreColorPalette()) {
+ return setTimeout(() => this.injectCoreColorPalette(iteration + 1))
+ }
+
+ return this.injectColorPalette()
+ }
+
+ removeThemeLink (linkEl: HTMLLinkElement) {
+ this.getHeadElement().removeChild(linkEl)
+ }
+
+ private canInjectCoreColorPalette () {
+ const computedStyle = getComputedStyle(document.body)
+ const isDark = computedStyle.getPropertyValue('--is-dark')
+
+ return isDark === '0' || isDark === '1'
+ }
+
+ private injectColorPalette () {
+ console.log(`Injecting color palette`)
+
+ const rootStyle = document.body.style
+ const computedStyle = getComputedStyle(document.body)
+
+ // FIXME: Remove previously injected properties
+ for (const property of this.oldInjectedProperties) {
+ rootStyle.removeProperty(property)
+ }
+
+ this.oldInjectedProperties = []
+
+ const isGlobalDarkTheme = () => {
+ return this.isDarkTheme({
+ fg: computedStyle.getPropertyValue('--fg') || computedStyle.getPropertyValue('--mainForegroundColor'),
+ bg: computedStyle.getPropertyValue('--bg') || computedStyle.getPropertyValue('--mainBackgroundColor'),
+ isDarkVar: computedStyle.getPropertyValue('--is-dark')
+ })
+ }
+
+ const isMenuDarkTheme = () => {
+ return this.isDarkTheme({
+ fg: computedStyle.getPropertyValue('--menu-fg'),
+ bg: computedStyle.getPropertyValue('--menu-bg'),
+ isDarkVar: computedStyle.getPropertyValue('--is-menu-dark')
+ })
+ }
+
+ const toProcess = [
+ { prefix: 'primary', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
+ { prefix: 'on-primary', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
+ { prefix: 'bg-secondary', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
+ { prefix: 'fg', invertIfDark: true, fallbacks: { '--fg-300': '--greyForegroundColor' }, step: 5, darkTheme: isGlobalDarkTheme },
+
+ { prefix: 'input-bg', invertIfDark: true, step: 5, darkTheme: isGlobalDarkTheme },
+
+ { prefix: 'menu-fg', invertIfDark: true, step: 5, darkTheme: isMenuDarkTheme },
+ { prefix: 'menu-bg', invertIfDark: true, step: 5, darkTheme: isMenuDarkTheme }
+ ] as { prefix: string, invertIfDark: boolean, step: number, darkTheme: () => boolean, fallbacks?: Record }[]
+
+ for (const { prefix, invertIfDark, step, darkTheme, fallbacks = {} } of toProcess) {
+ const mainColor = computedStyle.getPropertyValue('--' + prefix)
+
+ const darkInverter = invertIfDark && darkTheme()
+ ? -1
+ : 1
+
+ if (!mainColor) {
+ console.error(`Cannot create palette of nonexistent "--${prefix}" CSS body variable`)
+ continue
+ }
+
+ // Use trim for some web browsers: https://github.com/Chocobozzz/PeerTube/issues/6952
+ const mainColorHSL = toHSLA(parse(mainColor.trim()))
+ debugLogger(`Theme main variable ${mainColor} -> ${this.toHSLStr(mainColorHSL)}`)
+
+ // Inject in alphabetical order for easy debug
+ const toInject: { id: number, key: string, value: string }[] = [
+ { id: 500, key: `--${prefix}-500`, value: this.toHSLStr(mainColorHSL) }
+ ]
+
+ for (const j of [ -1, 1 ]) {
+ let lastColorHSL = { ...mainColorHSL }
+
+ for (let i = 1; i <= 9; i++) {
+ const suffix = 500 + (50 * i * j)
+ const key = `--${prefix}-${suffix}`
+
+ const existingValue = computedStyle.getPropertyValue(key)
+ if (!existingValue || existingValue === '0') {
+ const newLuminance = this.buildNewLuminance(lastColorHSL, j * step, darkInverter)
+ const newColorHSL = { ...lastColorHSL, l: newLuminance }
+
+ const newColorStr = this.toHSLStr(newColorHSL)
+
+ const value = fallbacks[key]
+ ? `var(${fallbacks[key]}, ${newColorStr})`
+ : newColorStr
+
+ toInject.push({ id: suffix, key, value })
+
+ lastColorHSL = newColorHSL
+
+ debugLogger(`Injected theme palette ${key} -> ${value}`)
+ } else {
+ lastColorHSL = toHSLA(parse(existingValue))
+ }
+ }
+ }
+
+ for (const { key, value } of sortBy(toInject, 'id')) {
+ rootStyle.setProperty(key, value)
+ this.oldInjectedProperties.push(key)
+ }
+ }
+
+ document.body.dataset.bsTheme = isGlobalDarkTheme()
+ ? 'dark'
+ : ''
+ }
+
+ private buildNewLuminance (base: { l: number }, factor: number, darkInverter: number) {
+ return Math.max(Math.min(100, Math.round(base.l + (factor * -1 * darkInverter))), 0)
+ }
+
+ private toHSLStr (c: { h: number, s: number, l: number, a: number }) {
+ return `hsl(${Math.round(c.h)} ${Math.round(c.s)}% ${Math.round(c.l)}% / ${Math.round(c.a)})`
+ }
+
+ private isDarkTheme (options: {
+ fg: string
+ bg: string
+ isDarkVar: string
+ }) {
+ const { fg, bg, isDarkVar } = options
+
+ if (isDarkVar === '1') {
+ return true
+ } else if (fg && bg) {
+ try {
+ if (getLuminance(parse(bg)) < getLuminance(parse(fg))) {
+ return true
+ }
+ } catch (err) {
+ console.error('Cannot parse deprecated CSS variables', err)
+ }
+ }
+
+ return false
+ }
+
+ private getHeadElement () {
+ return document.getElementsByTagName('head')[0]
+ }
+}
diff --git a/client/src/root-helpers/users/user-local-storage-keys.ts b/client/src/root-helpers/users/user-local-storage-keys.ts
index 90ccf0e78..6d82b6d09 100644
--- a/client/src/root-helpers/users/user-local-storage-keys.ts
+++ b/client/src/root-helpers/users/user-local-storage-keys.ts
@@ -5,6 +5,12 @@ export const UserLocalStorageKeys = {
EMAIL: 'email',
NSFW_POLICY: 'nsfw_policy',
+
+ NSFW_FLAGS_DISPLAYED: 'nsfw_flags_displayed',
+ NSFW_FLAGS_HIDDEN: 'nsfw_flags_hidden',
+ NSFW_FLAGS_WARNED: 'nsfw_flags_warned',
+ NSFW_FLAGS_BLURRED: 'nsfw_flags_blurred',
+
P2P_ENABLED: 'peertube-videojs-webtorrent_enabled',
AUTO_PLAY_VIDEO: 'auto_play_video',
diff --git a/client/src/root-helpers/video.ts b/client/src/root-helpers/video.ts
index 4b1a5b51b..2999f0e40 100644
--- a/client/src/root-helpers/video.ts
+++ b/client/src/root-helpers/video.ts
@@ -1,6 +1,6 @@
-import { HTMLServerConfig, Video, VideoPrivacy, VideoPrivacyType } from '@peertube/peertube-models'
+import { HTMLServerConfig, User, Video, VideoPrivacy, VideoPrivacyType } from '@peertube/peertube-models'
-function buildVideoOrPlaylistEmbed (options: {
+export function buildVideoOrPlaylistEmbed (options: {
embedUrl: string
embedTitle: string
aspectRatio?: number
@@ -37,30 +37,71 @@ function buildVideoOrPlaylistEmbed (options: {
return iframe.outerHTML
}
-function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: boolean) {
+export function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: boolean) {
if (video.isLocal && config.tracker.enabled === false) return false
if (isWebRTCDisabled()) return false
return userP2PEnabled
}
-function videoRequiresUserAuth (video: Video, videoPassword?: string) {
+export function videoRequiresUserAuth (video: Video, videoPassword?: string) {
return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id) ||
(video.privacy.id === VideoPrivacy.PASSWORD_PROTECTED && !videoPassword)
-
}
-function videoRequiresFileToken (video: Video) {
+export function videoRequiresFileToken (video: Video) {
return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.PASSWORD_PROTECTED ]).has(video.privacy.id)
}
-export {
- buildVideoOrPlaylistEmbed,
- isP2PEnabled,
- videoRequiresUserAuth,
- videoRequiresFileToken
+export function isVideoNSFWWarnedForUser (video: Video, config: HTMLServerConfig, user: User) {
+ if (video.nsfw === false) return false
+ // Don't display NSFW warning for the owner of the video
+ if (user?.account?.id === video.account.id) return false
+
+ if (!user) {
+ return config.instance.defaultNSFWPolicy === 'warn' || config.instance.defaultNSFWPolicy === 'blur'
+ }
+
+ if ((user.nsfwFlagsWarned & video.nsfwFlags) !== 0) return true
+ if ((user.nsfwFlagsBlurred & video.nsfwFlags) !== 0) return true
+ if ((user.nsfwFlagsDisplayed & video.nsfwFlags) !== 0) return false
+ if ((user.nsfwFlagsHidden & video.nsfwFlags) !== 0) return false
+
+ return user.nsfwPolicy === 'warn' || user.nsfwPolicy === 'blur'
}
+export function isVideoNSFWBlurForUser (video: Video, config: HTMLServerConfig, user: User) {
+ if (video.nsfw === false) return false
+ // Don't display NSFW warning for the owner of the video
+ if (user?.account?.id === video.account.id) return false
+
+ if (!user) return config.instance.defaultNSFWPolicy === 'blur'
+
+ if ((user.nsfwFlagsBlurred & video.nsfwFlags) !== 0) return true
+ if ((user.nsfwFlagsWarned & video.nsfwFlags) !== 0) return false
+ if ((user.nsfwFlagsDisplayed & video.nsfwFlags) !== 0) return false
+ if ((user.nsfwFlagsHidden & video.nsfwFlags) !== 0) return false
+
+ return user.nsfwPolicy === 'blur'
+}
+
+export function isVideoNSFWHiddenForUser (video: Video, config: HTMLServerConfig, user: User) {
+ if (video.nsfw === false) return false
+ // Video is not hidden for the owner of the video
+ if (user?.account?.id === video.account.id) return false
+
+ if (!user) return config.instance.defaultNSFWPolicy === 'do_not_list'
+
+ if ((user.nsfwFlagsHidden & video.nsfwFlags) !== 0) return true
+ if ((user.nsfwFlagsBlurred & video.nsfwFlags) !== 0) return false
+ if ((user.nsfwFlagsWarned & video.nsfwFlags) !== 0) return false
+ if ((user.nsfwFlagsDisplayed & video.nsfwFlags) !== 0) return false
+
+ return user.nsfwPolicy === 'do_not_list'
+}
+
+// ---------------------------------------------------------------------------
+// Private
// ---------------------------------------------------------------------------
function isWebRTCDisabled () {
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss
index d9a2f33db..acd2126dc 100644
--- a/client/src/sass/bootstrap.scss
+++ b/client/src/sass/bootstrap.scss
@@ -68,16 +68,24 @@ body {
font-family: $main-fonts;
}
-.btn-outline-secondary {
- --bs-btn-color: #{pvar(--fg-300)};
-}
-
.btn {
--bs-btn-active-color: inherit;
--bs-btn-active-bg: inherit;
--bs-btn-active-border-color: inherit;
}
+.btn-outline-primary {
+ --bs-btn-color: #{pvar(--fg)};
+ --bs-btn-border-color: #{pvar(--bg-secondary-450)};
+ --bs-btn-hover-color: #fff;
+ --bs-btn-hover-bg: #{pvar(--bg-secondary-450)};
+ --bs-btn-hover-border-color: #{pvar(--bg-secondary-450)};
+
+ --bs-btn-active-color: #{pvar(--on-primary)};
+ --bs-btn-active-bg: #{pvar(--primary)};
+ --bs-btn-active-border-color: #{pvar(--primary)};
+}
+
.flex-auto {
flex: auto;
}
@@ -86,6 +94,12 @@ body {
cursor: pointer !important;
}
+.btn-group-vertical {
+ label {
+ margin-bottom: 0;
+ }
+}
+
// ---------------------------------------------------------------------------
// Dropdown
// ---------------------------------------------------------------------------
@@ -285,17 +299,6 @@ body {
font-size: $button-font-size;
}
-.btn-outline-secondary {
- border-color: pvar(--input-border-color);
-
- &:focus-within,
- &:focus,
- &:hover {
- color: #fff;
- background-color: #6c757d;
- }
-}
-
.form-control {
color: pvar(--fg);
background-color: pvar(--input-bg);
diff --git a/client/src/sass/class-helpers/_badges.scss b/client/src/sass/class-helpers/_badges.scss
index 24286188a..881a30e61 100644
--- a/client/src/sass/class-helpers/_badges.scss
+++ b/client/src/sass/class-helpers/_badges.scss
@@ -40,6 +40,11 @@ $badge-grey-dark: #2D3448;
font-size: 100%;
}
+ &.badge-small {
+ font-size: 10px;
+ padding: 1px 3px;
+ }
+
&.badge-primary {
color: pvar(--on-primary);
background-color: pvar(--primary);
diff --git a/client/src/sass/include/_form-mixins.scss b/client/src/sass/include/_form-mixins.scss
index 33e5cd073..82e35d0e0 100644
--- a/client/src/sass/include/_form-mixins.scss
+++ b/client/src/sass/include/_form-mixins.scss
@@ -56,13 +56,10 @@
@mixin peertube-select-container ($width) {
padding: 0;
margin: 0;
- width: $width;
position: relative;
height: min-content;
- @media screen and (max-width: $width) {
- width: 100%;
- }
+ @include responsive-width($width);
&::after {
top: 50%;
@@ -116,6 +113,7 @@
label {
font-size: $form-input-font-size;
+ color: pvar(--fg-350);
}
[type=radio]:focus-visible,[type=radio]:focus {
@@ -141,7 +139,6 @@
line-height: 20px;
display: inline-block;
font-weight: $font-regular;
- color: pvar(--fg);
}
[type=radio]:checked + label::before,
@@ -200,6 +197,7 @@
box-shadow: $focus-box-shadow-form;
}
+ // Checkbox
+ span {
position: relative;
width: 20px;
@@ -224,6 +222,7 @@
}
}
+ // Checkbox checked
&:checked + span {
background: pvar(--input-check-active-bg);
animation: jelly 0.6s ease;
@@ -246,10 +245,12 @@
}
}
+ // Label
+ span + span {
font-weight: $font-regular;
cursor: pointer;
display: inline;
+ color: pvar(--fg-350);
@include margin-left(8px);
}
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss
index f237ddcf4..8027422f6 100644
--- a/client/src/sass/include/_miniature.scss
+++ b/client/src/sass/include/_miniature.scss
@@ -13,12 +13,6 @@
&:hover {
text-decoration: none;
}
-
- &.blur-filter {
- filter: blur(3px);
-
- @include padding-left(4px);
- }
}
@mixin miniature-thumbnail {
@@ -94,7 +88,6 @@
&.blur-filter {
filter: blur(20px);
- transform: scale(1.03);
}
}
}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 9c3301394..c9f0551b9 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -96,7 +96,7 @@
}
}
-@mixin fill-svg-color ($color) {
+@mixin fill-path-svg-color ($color) {
::ng-deep svg {
path {
fill: $color;
@@ -104,6 +104,12 @@
}
}
+@mixin fill-svg-color ($color) {
+ ::ng-deep svg {
+ fill: $color;
+ }
+}
+
@mixin rounded-line-height-1-5 ($font-size) {
line-height: calc(#{$font-size} + #{math.round(math.div($font-size, 2))});
}
@@ -128,7 +134,7 @@
@mixin responsive-width ($width) {
width: $width;
- @media screen and (max-width: $width) {
+ @media screen and (max-width: #{$width - 30px}) {
width: 100%;
}
}
diff --git a/client/src/standalone/player/src/peertube-player.ts b/client/src/standalone/player/src/peertube-player.ts
index 182eb47cc..98eb51e23 100644
--- a/client/src/standalone/player/src/peertube-player.ts
+++ b/client/src/standalone/player/src/peertube-player.ts
@@ -1,20 +1,37 @@
-import './shared/context-menu'
-import './shared/upnext/end-card'
-import './shared/upnext/upnext-plugin'
-import './shared/stats/stats-card'
-import './shared/stats/stats-plugin'
+import { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@peertube/peertube-core-utils'
+import { logger } from '@root-helpers/logger'
+import { PluginsManager } from '@root-helpers/plugins-manager'
+import { TranslationsManager } from '@root-helpers/translations-manager'
+import { copyToClipboard } from '@root-helpers/utils'
+import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
+import { isMobile } from '@root-helpers/web-browser'
+import videojs, { VideoJsPlayer } from 'video.js'
+import { saveAverageBandwidth } from './peertube-player-local-storage'
import './shared/bezels/bezels-plugin'
-import './shared/peertube/peertube-plugin'
-import './shared/resolutions/peertube-resolutions-plugin'
+import './shared/context-menu'
import './shared/control-bar/caption-toggle-button'
-import './shared/control-bar/storyboard-plugin'
import './shared/control-bar/chapters-plugin'
-import './shared/control-bar/time-tooltip'
import './shared/control-bar/next-previous-video-button'
import './shared/control-bar/p2p-info-button'
import './shared/control-bar/peertube-link-button'
-import './shared/control-bar/theater-button'
import './shared/control-bar/peertube-live-display'
+import './shared/control-bar/storyboard-plugin'
+import './shared/control-bar/theater-button'
+import './shared/control-bar/time-tooltip'
+import './shared/dock/peertube-dock-component'
+import './shared/dock/peertube-dock-plugin'
+import './shared/nsfw/peertube-nsfw-component'
+import './shared/nsfw/peertube-nsfw-plugin'
+import './shared/hotkeys/peertube-hotkeys-plugin'
+import './shared/metrics/metrics-plugin'
+import './shared/mobile/peertube-mobile-buttons'
+import './shared/mobile/peertube-mobile-plugin'
+import './shared/p2p-media-loader/hls-plugin'
+import './shared/p2p-media-loader/p2p-media-loader-plugin'
+import './shared/peertube/peertube-plugin'
+import { ControlBarOptionsBuilder, HLSOptionsBuilder, WebVideoOptionsBuilder } from './shared/player-options-builder'
+import './shared/playlist/playlist-plugin'
+import './shared/resolutions/peertube-resolutions-plugin'
import './shared/settings/menu-focus-fixed'
import './shared/settings/resolution-menu-button'
import './shared/settings/resolution-menu-item'
@@ -23,37 +40,23 @@ import './shared/settings/settings-menu-button'
import './shared/settings/settings-menu-item'
import './shared/settings/settings-panel'
import './shared/settings/settings-panel-child'
-import './shared/playlist/playlist-plugin'
-import './shared/mobile/peertube-mobile-plugin'
-import './shared/mobile/peertube-mobile-buttons'
-import './shared/hotkeys/peertube-hotkeys-plugin'
-import './shared/metrics/metrics-plugin'
-import './shared/p2p-media-loader/hls-plugin'
-import './shared/p2p-media-loader/p2p-media-loader-plugin'
+import './shared/stats/stats-card'
+import './shared/stats/stats-plugin'
+import './shared/upnext/end-card'
+import './shared/upnext/upnext-plugin'
import './shared/web-video/web-video-plugin'
-import './shared/dock/peertube-dock-component'
-import './shared/dock/peertube-dock-plugin'
-import videojs, { VideoJsPlayer } from 'video.js'
-import { logger } from '@root-helpers/logger'
-import { PluginsManager } from '@root-helpers/plugins-manager'
-import { copyToClipboard } from '@root-helpers/utils'
-import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
-import { isMobile } from '@root-helpers/web-browser'
-import { buildVideoLink, decorateVideoLink, isDefaultLocale, pick } from '@peertube/peertube-core-utils'
-import { saveAverageBandwidth } from './peertube-player-local-storage'
-import { ControlBarOptionsBuilder, HLSOptionsBuilder, WebVideoOptionsBuilder } from './shared/player-options-builder'
-import { TranslationsManager } from '@root-helpers/translations-manager'
import { PeerTubePlayerConstructorOptions, PeerTubePlayerLoadOptions, PlayerNetworkInfo, VideoJSPluginOptions } from './types'
-// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
-(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
-
const CaptionsButton = videojs.getComponent('CaptionsButton') as any
// Change Captions to Subtitles/CC
CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
CaptionsButton.prototype.label_ = ' '
+// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
+const PlaybackRateMenuButton = videojs.getComponent('PlaybackRateMenuButton') as any
+PlaybackRateMenuButton.prototype.controlText_ = 'Speed'
+
// TODO: remove when https://github.com/videojs/video.js/pull/7598 is merged
const PlayProgressBar = videojs.getComponent('PlayProgressBar') as any
if (PlayProgressBar.prototype.options_.children.includes('timeTooltip') !== true) {
@@ -100,8 +103,11 @@ export class PeerTubePlayer {
this.loadDynamicPlugins()
- if (this.options.controlBar === false) this.player.controlBar.hide()
- else this.player.controlBar.show()
+ if (this.options.controlBar === false) {
+ this.player.controlBar.hide()
+ } else {
+ this.player.controlBar.show()
+ }
this.player.autoplay(this.getAutoPlayValue(this.currentLoadOptions.autoplay))
@@ -134,8 +140,7 @@ export class PeerTubePlayer {
enable () {
if (!this.player) return
-
- (this.player.el() as HTMLElement).style.pointerEvents = 'auto'
+ ;(this.player.el() as HTMLElement).style.pointerEvents = 'auto'
}
disable () {
@@ -148,9 +153,8 @@ export class PeerTubePlayer {
// Disable player
this.player.hasStarted(false)
this.player.removeClass('vjs-has-autoplay')
- this.player.bigPlayButton.hide();
-
- (this.player.el() as HTMLElement).style.pointerEvents = 'none'
+ this.player.bigPlayButton.hide()
+ ;(this.player.el() as HTMLElement).style.pointerEvents = 'none'
}
setCurrentTime (currentTime: number) {
@@ -252,6 +256,7 @@ export class PeerTubePlayer {
if (this.player.usingPlugin('stats')) this.player.stats().dispose()
if (this.player.usingPlugin('storyboard')) this.player.storyboard().dispose()
if (this.player.usingPlugin('chapters')) this.player.chapters().dispose()
+ if (this.player.usingPlugin('peertubeNSFW')) this.player.peertubeNSFW().dispose()
if (this.player.usingPlugin('peertubeDock')) this.player.peertubeDock().dispose()
@@ -305,11 +310,14 @@ export class PeerTubePlayer {
if (this.currentLoadOptions.dock) {
this.player.peertubeDock(this.currentLoadOptions.dock)
}
+
+ if (this.currentLoadOptions.nsfwWarning) {
+ this.player.peertubeNSFW(this.currentLoadOptions.nsfwWarning)
+ }
}
private async tryToRecoverHLSError (err: any) {
if (err.code === MediaError.MEDIA_ERR_DECODE) {
-
// Display a notification to user
if (this.videojsDecodeErrors === 0) {
this.options.errorNotifier(this.player.localize('The video failed to play, will try to fast forward.'))
@@ -425,6 +433,7 @@ export class PeerTubePlayer {
autoplay: this.getAutoPlayValue(this.currentLoadOptions.autoplay),
poster: this.currentLoadOptions.poster,
+
inactivityTimeout: this.options.inactivityTimeout,
playbackRates: [ 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
@@ -485,7 +494,6 @@ export class PeerTubePlayer {
}
private getContextMenuOptions () {
-
const content = () => {
const self = this
const player = this.player
diff --git a/client/src/standalone/player/src/sass/shared/index.scss b/client/src/standalone/player/src/sass/shared/index.scss
index 4bfd67a26..27a3ee6d4 100644
--- a/client/src/standalone/player/src/sass/shared/index.scss
+++ b/client/src/standalone/player/src/sass/shared/index.scss
@@ -2,6 +2,7 @@
@use './dock';
@use './control-bar';
@use './mobile';
+@use './nsfw';
@use './context-menu';
@use './settings-menu';
@use './spinner';
diff --git a/client/src/standalone/player/src/sass/shared/nsfw.scss b/client/src/standalone/player/src/sass/shared/nsfw.scss
new file mode 100644
index 000000000..7d30aa861
--- /dev/null
+++ b/client/src/standalone/player/src/sass/shared/nsfw.scss
@@ -0,0 +1,74 @@
+@use 'sass:math';
+@use '_variables' as *;
+@use '_mixins' as *;
+@use '_icons' as *;
+@use './_player-variables' as *;
+
+.video-js.vjs-peertube-skin {
+ --container-margin-x: 20px;
+ --container-margin-y: 20px;
+
+ &.vjs-size-570 {
+ --container-margin-x: 10px;
+ --container-margin-y: 10px;
+ }
+
+ .nsfw-container {
+ font-size: 14px;
+ position: absolute;
+ top: var(--container-margin-y);
+ right: var(--container-margin-x);
+ width: 100%;
+ width: fit-content;
+ background-color: pvar(--bg-secondary-500);
+ color: pvar(--fg-400);
+ max-width: calc(40% - 2 * var(--container-margin-x));
+ max-height: calc(100% - 2 * var(--container-margin-y));
+ padding: 1rem;;
+ border-radius: 4px;
+ overflow: auto;
+
+ .nsfw-title {
+ font-size: 1.25rem;
+ font-weight: $font-bold;
+ margin-bottom: 0.5rem;
+ }
+
+ button,
+ .nsfw-more-flags,
+ .nsfw-more-summary {
+ margin-top: 0.75rem;
+ font-size: 13px;
+ }
+
+ button {
+ padding: 0;
+ color: pvar(--fg-450);
+ text-decoration: underline;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &::after {
+ @include chevron-down(8px, 2px);
+ @include margin-left(5px);
+ }
+ }
+
+ .nsfw-more-content {
+ strong {
+ display: block;
+ margin-bottom: 5px;
+ }
+ }
+ }
+
+ &.peertube-dock {
+ .nsfw-container {
+ top: unset;
+ bottom: var(--container-margin-y);
+ max-width: 90%;
+ }
+ }
+}
diff --git a/client/src/standalone/player/src/shared/nsfw/index.ts b/client/src/standalone/player/src/shared/nsfw/index.ts
new file mode 100644
index 000000000..16e70a9c1
--- /dev/null
+++ b/client/src/standalone/player/src/shared/nsfw/index.ts
@@ -0,0 +1,2 @@
+export * from './peertube-dock-component'
+export * from './peertube-dock-plugin'
diff --git a/client/src/standalone/player/src/shared/nsfw/peertube-nsfw-component.ts b/client/src/standalone/player/src/shared/nsfw/peertube-nsfw-component.ts
new file mode 100644
index 000000000..d821f1834
--- /dev/null
+++ b/client/src/standalone/player/src/shared/nsfw/peertube-nsfw-component.ts
@@ -0,0 +1,92 @@
+import { NSFWFlag } from '@peertube/peertube-models'
+import videojs from 'video.js'
+import { type PeerTubeNSFWPluginOptions } from './peertube-nsfw-plugin'
+
+const Component = videojs.getComponent('Component')
+
+class PeerTubeNSFWComponent extends Component {
+ declare options_: videojs.ComponentOptions & PeerTubeNSFWPluginOptions
+
+ // eslint-disable-next-line @typescript-eslint/no-useless-constructor
+ constructor (player: videojs.Player, options: videojs.ComponentOptions & PeerTubeNSFWPluginOptions) {
+ super(player, options)
+ }
+
+ createEl () {
+ const el = super.createEl('div', { className: 'nsfw-container' })
+
+ const title = super.createEl('div', { className: 'nsfw-title' })
+ title.textContent = this.player().localize('Sensitive content')
+
+ const content = super.createEl('div', { className: 'nsfw-content' })
+ content.textContent = this.player().localize('This video contains sensitive content.')
+
+ el.appendChild(title)
+ el.appendChild(content)
+
+ if (this.options_.flags || this.options_.summary) {
+ const moreButton = super.createEl(
+ 'button',
+ { textContent: this.player().localize('Learn more') },
+ { type: 'button' }
+ ) as HTMLButtonElement
+
+ el.appendChild(moreButton)
+
+ moreButton.addEventListener('click', () => {
+ this.appendMoreContent()
+
+ moreButton.style.display = 'none'
+ })
+ }
+
+ return el
+ }
+
+ private appendMoreContent () {
+ const moreContentEl = super.createEl('div', { className: 'nsfw-more-content' })
+
+ if (this.options_.flags) {
+ const moreContentFlags = super.createEl('div', { className: 'nsfw-more-flags' })
+ moreContentFlags.appendChild(super.createEl('strong', { textContent: this.player().localize('Content warning') }))
+ moreContentFlags.appendChild(super.createEl('div', { textContent: this.buildFlagStrings().join(' - ') }))
+
+ moreContentEl.appendChild(moreContentFlags)
+ }
+
+ if (this.options_.summary) {
+ const moreContentSummary = super.createEl('div', { className: 'nsfw-more-summary' })
+ moreContentSummary.appendChild(super.createEl('strong', { textContent: `Author note` }))
+ moreContentSummary.appendChild(super.createEl('div', { textContent: this.options_.summary }))
+
+ moreContentEl.appendChild(moreContentSummary)
+ }
+
+ this.el().appendChild(moreContentEl)
+ }
+
+ private buildFlagStrings () {
+ const flags = this.options_.flags
+ const flagStrings: string[] = []
+
+ if ((flags & NSFWFlag.VIOLENT) === NSFWFlag.VIOLENT) {
+ flagStrings.push(this.player().localize(`Violence`))
+ }
+
+ if ((flags & NSFWFlag.SHOCKING_DISTURBING) === NSFWFlag.SHOCKING_DISTURBING) {
+ flagStrings.push(this.player().localize(`Shocking Content`))
+ }
+
+ if ((flags & NSFWFlag.EXPLICIT_SEX) === NSFWFlag.EXPLICIT_SEX) {
+ flagStrings.push(this.player().localize(`Explicit Sex`))
+ }
+
+ return flagStrings
+ }
+}
+
+videojs.registerComponent('PeerTubeNSFWComponent', PeerTubeNSFWComponent)
+
+export {
+ PeerTubeNSFWComponent
+}
diff --git a/client/src/standalone/player/src/shared/nsfw/peertube-nsfw-plugin.ts b/client/src/standalone/player/src/shared/nsfw/peertube-nsfw-plugin.ts
new file mode 100644
index 000000000..c5d87f145
--- /dev/null
+++ b/client/src/standalone/player/src/shared/nsfw/peertube-nsfw-plugin.ts
@@ -0,0 +1,40 @@
+import videojs from 'video.js'
+import { PeerTubeNSFWComponent } from './peertube-nsfw-component'
+
+const Plugin = videojs.getPlugin('plugin')
+
+export type PeerTubeNSFWPluginOptions = {
+ summary: string
+ flags: number
+}
+
+class PeerTubeNSFWPlugin extends Plugin {
+ declare private nsfwComponent: PeerTubeNSFWComponent
+
+ constructor (player: videojs.Player, options: videojs.PlayerOptions & PeerTubeNSFWPluginOptions) {
+ super(player, options)
+
+ player.ready(() => {
+ player.addClass('peertube-nsfw')
+ })
+
+ this.nsfwComponent = new PeerTubeNSFWComponent(player, options)
+ player.addChild(this.nsfwComponent)
+
+ player.one('play', () => {
+ this.nsfwComponent.hide()
+ })
+ }
+
+ dispose () {
+ this.nsfwComponent?.dispose()
+ this.player.removeChild(this.nsfwComponent)
+ this.player.removeClass('peertube-nsfw')
+
+ super.dispose()
+ }
+}
+
+videojs.registerPlugin('peertubeNSFW', PeerTubeNSFWPlugin)
+
+export { PeerTubeNSFWPlugin }
diff --git a/client/src/standalone/player/src/shared/playlist/playlist-plugin.ts b/client/src/standalone/player/src/shared/playlist/playlist-plugin.ts
index 54e3702a5..4cb208032 100644
--- a/client/src/standalone/player/src/shared/playlist/playlist-plugin.ts
+++ b/client/src/standalone/player/src/shared/playlist/playlist-plugin.ts
@@ -12,6 +12,8 @@ class PlaylistPlugin extends Plugin {
constructor (player: videojs.Player, options?: PlaylistPluginOptions) {
super(player, options)
+ this.player.addClass('vjs-playlist')
+
this.playlistMenu = new PlaylistMenu(player, options)
this.playlistButton = new PlaylistButton(player, { ...options, playlistMenu: this.playlistMenu })
diff --git a/client/src/standalone/player/src/types/peertube-player-options.ts b/client/src/standalone/player/src/types/peertube-player-options.ts
index 83d42334b..f26dc05ab 100644
--- a/client/src/standalone/player/src/types/peertube-player-options.ts
+++ b/client/src/standalone/player/src/types/peertube-player-options.ts
@@ -86,6 +86,11 @@ export type PeerTubePlayerLoadOptions = {
requiresPassword: boolean
videoPassword: () => string
+ nsfwWarning?: {
+ flags: number
+ summary: string
+ }
+
nextVideo: {
enabled: boolean
getVideoTitle: () => string
diff --git a/client/src/standalone/player/src/types/peertube-videojs-typings.ts b/client/src/standalone/player/src/types/peertube-videojs-typings.ts
index b78038e27..b0607a14c 100644
--- a/client/src/standalone/player/src/types/peertube-videojs-typings.ts
+++ b/client/src/standalone/player/src/types/peertube-videojs-typings.ts
@@ -10,6 +10,7 @@ import { StoryboardPlugin } from '../shared/control-bar/storyboard-plugin'
import { PeerTubeDockPlugin, PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin'
import { HotkeysOptions, PeerTubeHotkeysPlugin } from '../shared/hotkeys/peertube-hotkeys-plugin'
import { PeerTubeMobilePlugin } from '../shared/mobile/peertube-mobile-plugin'
+import { PeerTubeNSFWPlugin, PeerTubeNSFWPluginOptions } from '../shared/nsfw/peertube-nsfw-plugin'
import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin'
import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin'
import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager'
@@ -24,7 +25,6 @@ import { WebVideoPlugin } from '../shared/web-video/web-video-plugin'
import { PlayerMode } from './peertube-player-options'
declare module 'video.js' {
-
export interface VideoJsPlayer {
srOptions_: HlsjsConfigHandlerOptions
@@ -32,44 +32,45 @@ declare module 'video.js' {
// FIXME: add it to upstream typings
posterImage: {
- show (): void
- hide (): void
+ show(): void
+ hide(): void
}
- handleTechSeeked_ (): void
+ handleTechSeeked_(): void
- textTracks (): TextTrackList & {
+ textTracks(): TextTrackList & {
tracks_: (TextTrack & { id: string, label: string, src: string })[]
}
// Plugins
- peertube (): PeerTubePlugin
+ peertube(): PeerTubePlugin
- webVideo (options?: any): WebVideoPlugin
+ webVideo(options?: any): WebVideoPlugin
- p2pMediaLoader (options?: any): P2pMediaLoaderPlugin
- hlsjs (options?: any): any
+ p2pMediaLoader(options?: any): P2pMediaLoaderPlugin
+ hlsjs(options?: any): any
- peertubeResolutions (): PeerTubeResolutionsPlugin
+ peertubeResolutions(): PeerTubeResolutionsPlugin
- contextMenu (options?: ContextMenuPluginOptions): ContextMenuPlugin
+ contextMenu(options?: ContextMenuPluginOptions): ContextMenuPlugin
- bezels (): BezelsPlugin
- peertubeMobile (): PeerTubeMobilePlugin
- peerTubeHotkeysPlugin (options?: HotkeysOptions): PeerTubeHotkeysPlugin
+ bezels(): BezelsPlugin
+ peertubeMobile(): PeerTubeMobilePlugin
+ peerTubeHotkeysPlugin(options?: HotkeysOptions): PeerTubeHotkeysPlugin
- stats (options?: StatsCardOptions): StatsForNerdsPlugin
+ stats(options?: StatsCardOptions): StatsForNerdsPlugin
- storyboard (options?: StoryboardOptions): StoryboardPlugin
+ storyboard(options?: StoryboardOptions): StoryboardPlugin
- peertubeDock (options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin
+ peertubeDock(options?: PeerTubeDockPluginOptions): PeerTubeDockPlugin
+ peertubeNSFW(options?: PeerTubeNSFWPluginOptions): PeerTubeNSFWPlugin
- chapters (options?: ChaptersOptions): ChaptersPlugin
+ chapters(options?: ChaptersOptions): ChaptersPlugin
- upnext (options?: UpNextPluginOptions): UpNextPlugin
+ upnext(options?: UpNextPluginOptions): UpNextPlugin
- playlist (options?: PlaylistPluginOptions): PlaylistPlugin
+ playlist(options?: PlaylistPluginOptions): PlaylistPlugin
}
}
@@ -214,7 +215,7 @@ export type WebVideoPluginOptions = {
}
export type HLSLoaderClass = {
- new (confg: HlsConfig): Loader
+ new(confg: HlsConfig): Loader
getEngine(): HlsJsP2PEngine
}
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index e3f450758..714b88a32 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -18,6 +18,7 @@ import {
AuthHTTP,
LiveManager,
PeerTubePlugin,
+ PeerTubeTheme,
PlayerOptionsBuilder,
PlaylistFetcher,
PlaylistTracker,
@@ -41,6 +42,7 @@ export class PeerTubeEmbed {
private readonly videoFetcher: VideoFetcher
private readonly playlistFetcher: PlaylistFetcher
private readonly peertubePlugin: PeerTubePlugin
+ private readonly peertubeTheme: PeerTubeTheme
private readonly playerHTML: PlayerHTML
private readonly playerOptionsBuilder: PlayerOptionsBuilder
private readonly liveManager: LiveManager
@@ -65,6 +67,7 @@ export class PeerTubeEmbed {
this.videoFetcher = new VideoFetcher(this.http)
this.playlistFetcher = new PlaylistFetcher(this.http)
this.peertubePlugin = new PeerTubePlugin(this.http)
+ this.peertubeTheme = new PeerTubeTheme(this.peertubePlugin)
this.playerHTML = new PlayerHTML(videoWrapperId)
this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
this.liveManager = new LiveManager(this.playerHTML)
@@ -101,6 +104,8 @@ export class PeerTubeEmbed {
.then(res => res.json())
}
+ this.peertubeTheme.loadTheme(this.config)
+
const videoId = this.isPlaylistEmbed()
? await this.initPlaylist()
: this.getResourceId()
@@ -278,6 +283,8 @@ export class PeerTubeEmbed {
video,
captionsResponse,
chaptersResponse,
+
+ config: this.config,
translations,
storyboardsResponse,
@@ -379,6 +386,7 @@ export class PeerTubeEmbed {
this.peertubePlayer.unload()
this.peertubePlayer.disable()
+
this.peertubePlayer.setPoster(video.previewPath)
}
diff --git a/client/src/standalone/videos/shared/index.ts b/client/src/standalone/videos/shared/index.ts
index a09b8d450..eb36b400f 100644
--- a/client/src/standalone/videos/shared/index.ts
+++ b/client/src/standalone/videos/shared/index.ts
@@ -1,5 +1,6 @@
export * from './auth-http'
export * from './peertube-plugin'
+export * from './peertube-theme'
export * from './live-manager'
export * from './player-html'
export * from './player-options-builder'
diff --git a/client/src/standalone/videos/shared/peertube-theme.ts b/client/src/standalone/videos/shared/peertube-theme.ts
new file mode 100644
index 000000000..61b9db9d1
--- /dev/null
+++ b/client/src/standalone/videos/shared/peertube-theme.ts
@@ -0,0 +1,49 @@
+import { HTMLServerConfig, ServerConfig } from '@peertube/peertube-models'
+import { logger, ThemeManager } from '../../../root-helpers'
+import { PeerTubePlugin } from './peertube-plugin'
+import { getBackendUrl } from './url'
+
+export class PeerTubeTheme {
+ private themeManager = new ThemeManager()
+
+ constructor (private readonly pluginPlugin: PeerTubePlugin) {
+ }
+
+ loadTheme (config: HTMLServerConfig) {
+ for (const theme of config.theme.registered) {
+ this.themeManager.injectTheme(theme, getBackendUrl())
+ }
+
+ const themeName = this.getCurrentThemeName(config)
+ logger.info(`Enabling ${themeName} theme.`)
+
+ this.themeManager.loadThemeStyle(themeName)
+
+ const theme = config.theme.registered.find(t => t.name === themeName)
+ const isInternalTheme = config.theme.builtIn.map(t => t.name as string).includes(themeName)
+
+ if (isInternalTheme) {
+ logger.info(`Enabling internal theme ${themeName}`)
+ } else if (theme) {
+ logger.info(`Adding scripts of theme ${themeName}`)
+
+ const pluginManager = this.pluginPlugin.getPluginsManager()
+ pluginManager.addPlugin(theme, true)
+ pluginManager.reloadLoadedScopes()
+ }
+
+ this.themeManager.injectCoreColorPalette()
+ }
+
+ private getCurrentThemeName (config: HTMLServerConfig) {
+ const instanceTheme = config.theme.default
+ if (instanceTheme !== 'default') return instanceTheme
+
+ // Default to dark theme if available and wanted by the user
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ return 'peertube-core-dark-brown' satisfies ServerConfig['theme']['builtIn'][0]['name']
+ }
+
+ return instanceTheme
+ }
+}
diff --git a/client/src/standalone/videos/shared/player-options-builder.ts b/client/src/standalone/videos/shared/player-options-builder.ts
index 3bd0c1995..a6677466c 100644
--- a/client/src/standalone/videos/shared/player-options-builder.ts
+++ b/client/src/standalone/videos/shared/player-options-builder.ts
@@ -16,6 +16,9 @@ import {
getParamString,
getParamToggle,
isP2PEnabled,
+ isVideoNSFWBlurForUser,
+ isVideoNSFWHiddenForUser,
+ isVideoNSFWWarnedForUser,
logger,
peertubeLocalStorage,
UserLocalStorageKeys,
@@ -235,6 +238,7 @@ export class PlayerOptionsBuilder {
videoPassword: () => string
requiresPassword: boolean
+ config: HTMLServerConfig
translations: Translations
playlist?: {
@@ -256,7 +260,8 @@ export class PlayerOptionsBuilder {
playlist,
live,
storyboardsResponse,
- chaptersResponse
+ chaptersResponse,
+ config
} = options
const [ videoCaptions, storyboard, chapters ] = await Promise.all([
@@ -265,10 +270,13 @@ export class PlayerOptionsBuilder {
this.buildChapters(chaptersResponse)
])
+ const nsfwWarn = isVideoNSFWWarnedForUser(video, config, null) || isVideoNSFWHiddenForUser(video, config, null)
+ const nsfwBlur = isVideoNSFWBlurForUser(video, config, null) || isVideoNSFWHiddenForUser(video, config, null)
+
return {
mode: this.mode,
- autoplay: forceAutoplay || alreadyPlayed || this.autoplay,
+ autoplay: !nsfwWarn && (forceAutoplay || alreadyPlayed || this.autoplay),
forceAutoplay,
p2pEnabled: this.p2pEnabled,
@@ -291,11 +299,20 @@ export class PlayerOptionsBuilder {
videoShortUUID: video.shortUUID,
videoUUID: video.uuid,
+ nsfwWarning: nsfwWarn
+ ? {
+ flags: video.nsfwFlags,
+ summary: video.nsfwSummary
+ }
+ : undefined,
+
+ poster: nsfwBlur
+ ? null
+ : getBackendUrl() + video.previewPath,
+
duration: video.duration,
videoRatio: video.aspectRatio,
- poster: getBackendUrl() + video.previewPath,
-
embedUrl: getBackendUrl() + video.embedPath,
embedTitle: video.name,
diff --git a/client/src/types/index.ts b/client/src/types/index.ts
index 60564496c..c170dcf94 100644
--- a/client/src/types/index.ts
+++ b/client/src/types/index.ts
@@ -5,3 +5,4 @@ export * from './job-type-client.type'
export * from './link.type'
export * from './register-client-option.model'
export * from './select-options-item.model'
+export * from './select-radio-item.model'
diff --git a/client/src/types/select-radio-item.model.ts b/client/src/types/select-radio-item.model.ts
new file mode 100644
index 000000000..2c73acc82
--- /dev/null
+++ b/client/src/types/select-radio-item.model.ts
@@ -0,0 +1,5 @@
+export interface SelectRadioItem {
+ id: T
+ label: string
+ description?: string
+}
diff --git a/client/yarn.lock b/client/yarn.lock
index 28fcaf9ca..71b1d3881 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -446,10 +446,10 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
-"@browserstack/ai-sdk-node@1.5.9":
- version "1.5.9"
- resolved "https://registry.yarnpkg.com/@browserstack/ai-sdk-node/-/ai-sdk-node-1.5.9.tgz#ec604b5e1dd28aa040dca6a3da49c50e60147a3d"
- integrity sha512-RGLWcxadVEkafFaoeAPnIGxS/C+DtHDYWZ3Sy+P5VvWlDWhN9hZU5lZGMsUZg+XmrnuhodEEaZnJQV9P0IEeMQ==
+"@browserstack/ai-sdk-node@1.5.17":
+ version "1.5.17"
+ resolved "https://registry.yarnpkg.com/@browserstack/ai-sdk-node/-/ai-sdk-node-1.5.17.tgz#3666b01f7f16fe7b7ca0e12c251c77c40a6bb8e5"
+ integrity sha512-odjnFulpBeF64UGHA+bIxkIcALYvEPznTl4U0hRT1AFfn4FqT+4wQdPBYnSnlc2XWTedv4zCDvbp4AFrtKXHEw==
dependencies:
axios "^1.7.4"
uuid "9.0.1"
@@ -510,126 +510,251 @@
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461"
integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==
+"@esbuild/aix-ppc64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437"
+ integrity sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==
+
"@esbuild/android-arm64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894"
integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==
+"@esbuild/android-arm64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz#649e47e04ddb24a27dc05c395724bc5f4c55cbfe"
+ integrity sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==
+
"@esbuild/android-arm@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3"
integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==
+"@esbuild/android-arm@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.3.tgz#8a0f719c8dc28a4a6567ef7328c36ea85f568ff4"
+ integrity sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==
+
"@esbuild/android-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb"
integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==
+"@esbuild/android-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.3.tgz#e2ab182d1fd06da9bef0784a13c28a7602d78009"
+ integrity sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==
+
"@esbuild/darwin-arm64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936"
integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==
+"@esbuild/darwin-arm64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz#c7f3166fcece4d158a73dcfe71b2672ca0b1668b"
+ integrity sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==
+
"@esbuild/darwin-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9"
integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==
+"@esbuild/darwin-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz#d8c5342ec1a4bf4b1915643dfe031ba4b173a87a"
+ integrity sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==
+
"@esbuild/freebsd-arm64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00"
integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==
+"@esbuild/freebsd-arm64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz#9f7d789e2eb7747d4868817417cc968ffa84f35b"
+ integrity sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==
+
"@esbuild/freebsd-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f"
integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==
+"@esbuild/freebsd-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz#8ad35c51d084184a8e9e76bb4356e95350a64709"
+ integrity sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==
+
"@esbuild/linux-arm64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43"
integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==
+"@esbuild/linux-arm64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz#3af0da3d9186092a9edd4e28fa342f57d9e3cd30"
+ integrity sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==
+
"@esbuild/linux-arm@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736"
integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==
+"@esbuild/linux-arm@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz#e91cafa95e4474b3ae3d54da12e006b782e57225"
+ integrity sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==
+
"@esbuild/linux-ia32@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5"
integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==
+"@esbuild/linux-ia32@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz#81025732d85b68ee510161b94acdf7e3007ea177"
+ integrity sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==
+
"@esbuild/linux-loong64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc"
integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==
+"@esbuild/linux-loong64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz#3c744e4c8d5e1148cbe60a71a11b58ed8ee5deb8"
+ integrity sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==
+
"@esbuild/linux-mips64el@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb"
integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==
+"@esbuild/linux-mips64el@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz#1dfe2a5d63702db9034cc6b10b3087cc0424ec26"
+ integrity sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==
+
"@esbuild/linux-ppc64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412"
integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==
+"@esbuild/linux-ppc64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz#2e85d9764c04a1ebb346dc0813ea05952c9a5c56"
+ integrity sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==
+
"@esbuild/linux-riscv64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694"
integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==
+"@esbuild/linux-riscv64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz#a9ea3334556b09f85ccbfead58c803d305092415"
+ integrity sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==
+
"@esbuild/linux-s390x@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577"
integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==
+"@esbuild/linux-s390x@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz#f6a7cb67969222b200974de58f105dfe8e99448d"
+ integrity sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==
+
"@esbuild/linux-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f"
integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==
+"@esbuild/linux-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz#a237d3578ecdd184a3066b1f425e314ade0f8033"
+ integrity sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==
+
"@esbuild/netbsd-arm64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6"
integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==
+"@esbuild/netbsd-arm64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz#4c15c68d8149614ddb6a56f9c85ae62ccca08259"
+ integrity sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==
+
"@esbuild/netbsd-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40"
integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==
+"@esbuild/netbsd-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz#12f6856f8c54c2d7d0a8a64a9711c01a743878d5"
+ integrity sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==
+
"@esbuild/openbsd-arm64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f"
integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==
+"@esbuild/openbsd-arm64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz#ca078dad4a34df192c60233b058db2ca3d94bc5c"
+ integrity sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==
+
"@esbuild/openbsd-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205"
integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==
+"@esbuild/openbsd-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz#c9178adb60e140e03a881d0791248489c79f95b2"
+ integrity sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==
+
"@esbuild/sunos-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6"
integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==
+"@esbuild/sunos-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz#03765eb6d4214ff27e5230af779e80790d1ee09f"
+ integrity sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==
+
"@esbuild/win32-arm64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85"
integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==
+"@esbuild/win32-arm64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz#f1c867bd1730a9b8dfc461785ec6462e349411ea"
+ integrity sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==
+
"@esbuild/win32-ia32@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2"
integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==
+"@esbuild/win32-ia32@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz#77491f59ef6c9ddf41df70670d5678beb3acc322"
+ integrity sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==
+
"@esbuild/win32-x64@0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b"
integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==
+"@esbuild/win32-x64@0.25.3":
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz#b17a2171f9074df9e91bfb07ef99a892ac06412a"
+ integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==
+
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56"
@@ -722,6 +847,17 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
+"@inquirer/checkbox@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-3.0.1.tgz#0a57f704265f78c36e17f07e421b98efb4b9867b"
+ integrity sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/figures" "^1.0.6"
+ "@inquirer/type" "^2.0.0"
+ ansi-escapes "^4.3.2"
+ yoctocolors-cjs "^2.1.2"
+
"@inquirer/checkbox@^4.0.4":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-4.1.1.tgz#5f2c0ce74a75e3872f8e170fd209655972ce7802"
@@ -741,6 +877,14 @@
"@inquirer/core" "^10.1.2"
"@inquirer/type" "^3.0.2"
+"@inquirer/confirm@^4.0.1":
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-4.0.1.tgz#9106d6bffa0b2fdd0e4f60319b6f04f2e06e6e25"
+ integrity sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/type" "^2.0.0"
+
"@inquirer/confirm@^5.1.1":
version "5.1.5"
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.5.tgz#0e6bf86794f69f849667ee38815608d6cd5917ba"
@@ -763,6 +907,33 @@
wrap-ansi "^6.2.0"
yoctocolors-cjs "^2.1.2"
+"@inquirer/core@^9.2.1":
+ version "9.2.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-9.2.1.tgz#677c49dee399c9063f31e0c93f0f37bddc67add1"
+ integrity sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==
+ dependencies:
+ "@inquirer/figures" "^1.0.6"
+ "@inquirer/type" "^2.0.0"
+ "@types/mute-stream" "^0.0.4"
+ "@types/node" "^22.5.5"
+ "@types/wrap-ansi" "^3.0.0"
+ ansi-escapes "^4.3.2"
+ cli-width "^4.1.0"
+ mute-stream "^1.0.0"
+ signal-exit "^4.1.0"
+ strip-ansi "^6.0.1"
+ wrap-ansi "^6.2.0"
+ yoctocolors-cjs "^2.1.2"
+
+"@inquirer/editor@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-3.0.1.tgz#d109f21e050af6b960725388cb1c04214ed7c7bc"
+ integrity sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/type" "^2.0.0"
+ external-editor "^3.1.0"
+
"@inquirer/editor@^4.2.1":
version "4.2.6"
resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-4.2.6.tgz#dec442b9f7ada0804bb9ba689370cc05fd385b20"
@@ -772,6 +943,15 @@
"@inquirer/type" "^3.0.4"
external-editor "^3.1.0"
+"@inquirer/expand@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-3.0.1.tgz#aed9183cac4d12811be47a4a895ea8e82a17e22c"
+ integrity sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/type" "^2.0.0"
+ yoctocolors-cjs "^2.1.2"
+
"@inquirer/expand@^4.0.4":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-4.0.8.tgz#8438bd34af182d4a37d8d7101a328e10430efadc"
@@ -786,6 +966,19 @@
resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.10.tgz#e3676a51c9c51aaabcd6ba18a28e82b98417db37"
integrity sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==
+"@inquirer/figures@^1.0.6":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.11.tgz#4744e6db95288fea1dead779554859710a959a21"
+ integrity sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==
+
+"@inquirer/input@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-3.0.1.tgz#de63d49e516487388508d42049deb70f2cb5f28e"
+ integrity sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/type" "^2.0.0"
+
"@inquirer/input@^4.1.1":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-4.1.5.tgz#ea3ffed7947c28d61ef3f261c4f261e99c4cac8a"
@@ -794,6 +987,14 @@
"@inquirer/core" "^10.1.6"
"@inquirer/type" "^3.0.4"
+"@inquirer/number@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-2.0.1.tgz#b9863080d02ab7dc2e56e16433d83abea0f2a980"
+ integrity sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/type" "^2.0.0"
+
"@inquirer/number@^3.0.4":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-3.0.8.tgz#ca44c09a8ac74040e2327e04694799eae603e9de"
@@ -802,6 +1003,15 @@
"@inquirer/core" "^10.1.6"
"@inquirer/type" "^3.0.4"
+"@inquirer/password@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-3.0.1.tgz#2a9a9143591088336bbd573bcb05d5bf080dbf87"
+ integrity sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/type" "^2.0.0"
+ ansi-escapes "^4.3.2"
+
"@inquirer/password@^4.0.4":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-4.0.8.tgz#ac2b14800a75f15e3404d98616d9dc7d8c2df38b"
@@ -827,6 +1037,31 @@
"@inquirer/search" "^3.0.4"
"@inquirer/select" "^4.0.4"
+"@inquirer/prompts@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-6.0.1.tgz#43f5c0ed35c5ebfe52f1d43d46da2d363d950071"
+ integrity sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==
+ dependencies:
+ "@inquirer/checkbox" "^3.0.1"
+ "@inquirer/confirm" "^4.0.1"
+ "@inquirer/editor" "^3.0.1"
+ "@inquirer/expand" "^3.0.1"
+ "@inquirer/input" "^3.0.1"
+ "@inquirer/number" "^2.0.1"
+ "@inquirer/password" "^3.0.1"
+ "@inquirer/rawlist" "^3.0.1"
+ "@inquirer/search" "^2.0.1"
+ "@inquirer/select" "^3.0.1"
+
+"@inquirer/rawlist@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-3.0.1.tgz#729def358419cc929045f264131878ed379e0af3"
+ integrity sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/type" "^2.0.0"
+ yoctocolors-cjs "^2.1.2"
+
"@inquirer/rawlist@^4.0.4":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-4.0.8.tgz#1d4389186d63861a2abe2dd107f72e813dc0ea4b"
@@ -836,6 +1071,16 @@
"@inquirer/type" "^3.0.4"
yoctocolors-cjs "^2.1.2"
+"@inquirer/search@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-2.0.1.tgz#69b774a0a826de2e27b48981d01bc5ad81e73721"
+ integrity sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/figures" "^1.0.6"
+ "@inquirer/type" "^2.0.0"
+ yoctocolors-cjs "^2.1.2"
+
"@inquirer/search@^3.0.4":
version "3.0.8"
resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-3.0.8.tgz#38c25f5b2db15a268be76b09bd12b4599ecc216b"
@@ -846,6 +1091,17 @@
"@inquirer/type" "^3.0.4"
yoctocolors-cjs "^2.1.2"
+"@inquirer/select@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-3.0.1.tgz#1df9ed27fb85a5f526d559ac5ce7cc4e9dc4e7ec"
+ integrity sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==
+ dependencies:
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/figures" "^1.0.6"
+ "@inquirer/type" "^2.0.0"
+ ansi-escapes "^4.3.2"
+ yoctocolors-cjs "^2.1.2"
+
"@inquirer/select@^4.0.4":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-4.0.8.tgz#dde85e10bc4e650c51542de533a91b6bc63498b7"
@@ -864,6 +1120,13 @@
dependencies:
mute-stream "^1.0.0"
+"@inquirer/type@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-2.0.0.tgz#08fa513dca2cb6264fe1b0a2fabade051444e3f6"
+ integrity sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==
+ dependencies:
+ mute-stream "^1.0.0"
+
"@inquirer/type@^3.0.2", "@inquirer/type@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.4.tgz#fa5f9e91a0abf3c9e93d3e1990ecb891d8195cf2"
@@ -978,13 +1241,6 @@
dependencies:
"@inquirer/type" "^1.5.5"
-"@ljharb/through@^2.3.11":
- version "2.3.13"
- resolved "https://registry.yarnpkg.com/@ljharb/through/-/through-2.3.13.tgz#b7e4766e0b65aa82e529be945ab078de79874edc"
- integrity sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==
- dependencies:
- call-bind "^1.0.7"
-
"@lmdb/lmdb-darwin-arm64@3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.2.tgz#39e25e2a95d35a7350862af96d05e5396ea8a074"
@@ -1395,25 +1651,25 @@
he "^1.2.0"
tokenizr "^1.6.4"
-"@percy/appium-app@^2.0.1":
- version "2.0.9"
- resolved "https://registry.yarnpkg.com/@percy/appium-app/-/appium-app-2.0.9.tgz#b93f085b76514f8d180745978d837df093872c7f"
- integrity sha512-50tBFevee8f6zxAPr8o8e73vy8JNuLKRi7BcbdWh+N7dK+B6Iu1tyGNElFddDjJaTlWcv1Tg9iAdRlXLfIpZhg==
+"@percy/appium-app@^2.0.9":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@percy/appium-app/-/appium-app-2.1.0.tgz#f1c8b798362fbdeea98dbd5dfb9c0857835b1d70"
+ integrity sha512-XVigKgAcXEerIch3Ufngac07gOH4KnfTDp/xyPujDyjvAZSWfIyIRnojmfbLEs2HnZEnmFFoEMX6ZB4Tk0SO/Q==
dependencies:
- "@percy/sdk-utils" "^1.28.2"
+ "@percy/sdk-utils" "^1.30.9"
tmp "^0.2.3"
-"@percy/sdk-utils@^1.28.2", "@percy/sdk-utils@^1.30.3":
- version "1.30.7"
- resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.30.7.tgz#39fcba80aa041dee8b585dbcf9b95c01c93f7993"
- integrity sha512-HVQSg0MgY4Ziv0mtbeelz4aRBKoEQnKaKtWl7Nf6FzSELAdUXNz4BNRBAJWOt8O6M5MRXbk6/7jSFJStGsg5Zw==
+"@percy/sdk-utils@^1.30.9":
+ version "1.30.10"
+ resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.30.10.tgz#f90d1373868a79b3887fccae0d9c246417d8d557"
+ integrity sha512-EOFm6XDbXIpo1YjF+JWxNCW5TB0ZaqjQfHLtOCmffhHi2T0MCXSAHdNxeTUyADyySzWjD4bKba/PbZwwTVE8Zw==
-"@percy/selenium-webdriver@^2.0.3":
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/@percy/selenium-webdriver/-/selenium-webdriver-2.2.2.tgz#36e795f2a26fb8cd2d17b8df59af3e4c72cfe2c6"
- integrity sha512-ksgPO9q/twhZTSVUrw8a96iiMMi2Y+SpGtwIEyOuZtNeEqEeJH3Mta1EvEUSauTH7HjqkP3Qemh/HaWrLUDK5w==
+"@percy/selenium-webdriver@^2.2.2":
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/@percy/selenium-webdriver/-/selenium-webdriver-2.2.3.tgz#1751c55588e87d8d91bc11af00f6c92ab8194ece"
+ integrity sha512-dVUsgKkDUYvv7+jN4S4HuwSoYxb7Up0U7dM3DRj3/XzLp3boZiyTWAdFdOGS8R5eSsiY5UskTcGQKmGqHRle1Q==
dependencies:
- "@percy/sdk-utils" "^1.30.3"
+ "@percy/sdk-utils" "^1.30.9"
node-request-interceptor "^0.6.3"
"@pkgjs/parseargs@^0.11.0":
@@ -1466,6 +1722,19 @@
unbzip2-stream "1.4.3"
yargs "17.7.2"
+"@puppeteer/browsers@^2.2.0":
+ version "2.10.2"
+ resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.10.2.tgz#c2a63cee699c6b5b971b9fcba9095098970f1648"
+ integrity sha512-i4Ez+s9oRWQbNjtI/3+jxr7OH508mjAKvza0ekPJem0ZtmsYHP3B5dq62+IaBHKaGCOuqJxXzvFLUhJvQ6jtsQ==
+ dependencies:
+ debug "^4.4.0"
+ extract-zip "^2.0.1"
+ progress "^2.0.3"
+ proxy-agent "^6.5.0"
+ semver "^7.7.1"
+ tar-fs "^3.0.8"
+ yargs "^17.7.2"
+
"@rollup/plugin-inject@^5.0.5":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz#616f3a73fe075765f91c5bec90176608bed277a3"
@@ -1683,6 +1952,11 @@
"@angular-devkit/schematics" "19.1.5"
jsonc-parser "3.3.1"
+"@sec-ant/readable-stream@^0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c"
+ integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==
+
"@sigstore/bundle@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-3.0.0.tgz#ffffc750436c6eb8330ead1ca65bc892f893a7c5"
@@ -1739,6 +2013,11 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668"
integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==
+"@sindresorhus/merge-streams@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz#abb11d99aeb6d27f1b563c38147a72d50058e339"
+ integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==
+
"@socket.io/component-emitter@~3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
@@ -1944,7 +2223,7 @@
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd"
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
-"@types/mocha@^10.0.0":
+"@types/mocha@^10.0.6":
version "10.0.10"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.10.tgz#91f62905e8d23cbd66225312f239454a23bebfa0"
integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==
@@ -1954,6 +2233,13 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
+"@types/mute-stream@^0.0.4":
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478"
+ integrity sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==
+ dependencies:
+ "@types/node" "*"
+
"@types/node-fetch@^2.6.12":
version "2.6.12"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03"
@@ -1976,6 +2262,20 @@
dependencies:
undici-types "~5.26.4"
+"@types/node@^20.1.0", "@types/node@^20.1.1", "@types/node@^20.11.28", "@types/node@^20.11.30":
+ version "20.17.32"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.32.tgz#cb9703514cd8e172c11beff582c66006644c2d88"
+ integrity sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==
+ dependencies:
+ undici-types "~6.19.2"
+
+"@types/node@^22.5.5":
+ version "22.15.3"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.3.tgz#b7fb9396a8ec5b5dfb1345d8ac2502060e9af68b"
+ integrity sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==
+ dependencies:
+ undici-types "~6.21.0"
+
"@types/normalize-package-data@^2.4.1":
version "2.4.4"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
@@ -2009,6 +2309,11 @@
dependencies:
"@types/node" "*"
+"@types/sinonjs__fake-timers@^8.1.5":
+ version "8.1.5"
+ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2"
+ integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==
+
"@types/stack-utils@^2.0.0":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
@@ -2034,6 +2339,11 @@
resolved "https://registry.yarnpkg.com/@types/which/-/which-2.0.2.tgz#54541d02d6b1daee5ec01ac0d1b37cecf37db1ae"
integrity sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==
+"@types/wrap-ansi@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd"
+ integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==
+
"@types/ws@^8.5.3":
version "8.5.14"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.14.tgz#93d44b268c9127d96026cf44353725dd9b6c3c21"
@@ -2190,7 +2500,14 @@
dependencies:
tinyrainbow "^1.2.0"
-"@vitest/snapshot@^2.0.3", "@vitest/snapshot@^2.0.4":
+"@vitest/pretty-format@2.1.9":
+ version "2.1.9"
+ resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf"
+ integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==
+ dependencies:
+ tinyrainbow "^1.2.0"
+
+"@vitest/snapshot@^2.0.3":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de"
integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==
@@ -2199,58 +2516,67 @@
magic-string "^0.30.12"
pathe "^1.1.2"
-"@wdio/browserstack-service@^8.10.5":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-8.41.0.tgz#16b1b2fbe4fe6a7e1381939dca8ec614584fba43"
- integrity sha512-ghpXZdixEVRU/2Z1gmle3gXJDCnWUTn1LdSP8WP56JpGx9m7CROHUOj6iYxCoVf1rEDYOYCX3k/7QARMhybj3g==
+"@vitest/snapshot@^2.0.5", "@vitest/snapshot@^2.1.1":
+ version "2.1.9"
+ resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91"
+ integrity sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==
dependencies:
- "@browserstack/ai-sdk-node" "1.5.9"
- "@percy/appium-app" "^2.0.1"
- "@percy/selenium-webdriver" "^2.0.3"
+ "@vitest/pretty-format" "2.1.9"
+ magic-string "^0.30.12"
+ pathe "^1.1.2"
+
+"@wdio/browserstack-service@^9.12.7":
+ version "9.12.7"
+ resolved "https://registry.yarnpkg.com/@wdio/browserstack-service/-/browserstack-service-9.12.7.tgz#83bd1400798641360fe72cea2340180da098511f"
+ integrity sha512-NXYWQ4mnW1pUC8eOSfQIirm5udXUXGWcBVz356dVXYyAE6cFvDaBGgqWlGwXZdph4alkZEcG6/JmZ9xs1ZO8Gg==
+ dependencies:
+ "@browserstack/ai-sdk-node" "1.5.17"
+ "@percy/appium-app" "^2.0.9"
+ "@percy/selenium-webdriver" "^2.2.2"
"@types/gitconfiglocal" "^2.0.1"
- "@wdio/logger" "8.38.0"
- "@wdio/reporter" "8.41.0"
- "@wdio/types" "8.41.0"
+ "@wdio/logger" "9.4.4"
+ "@wdio/reporter" "9.12.6"
+ "@wdio/types" "9.12.6"
browserstack-local "^1.5.1"
chalk "^5.3.0"
csv-writer "^1.6.0"
formdata-node "5.0.1"
git-repo-info "^2.1.1"
gitconfiglocal "^2.1.0"
- got "^12.6.1"
+ undici "^6.20.1"
uuid "^10.0.0"
- webdriverio "8.41.0"
+ webdriverio "9.12.7"
winston-transport "^4.5.0"
yauzl "^3.0.0"
-"@wdio/cli@^8.10.5":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-8.41.0.tgz#f53576736d179dd4df32c68bbbaaad12b149168d"
- integrity sha512-+f4McBz6M8/oEJLeoYxHNyfvQ4NTGRACKjtw/FdTJ+GYRu8DkU9sgXOo70NIPCMUajVIOtbDVUbLjJKuxWTEFQ==
+"@wdio/cli@^9.12.7":
+ version "9.12.7"
+ resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-9.12.7.tgz#c2df972730540c1ac269b217529fcc0b6d86c53c"
+ integrity sha512-X764hL/nHcbMTepvr7zNF/pSvb4r3twoa5lKllkIIraRDI0cg1/AKHreX24htjHpoA5OLzjEJaydQVJpZ3RzmA==
dependencies:
- "@types/node" "^22.2.0"
- "@vitest/snapshot" "^2.0.4"
- "@wdio/config" "8.41.0"
- "@wdio/globals" "8.41.0"
- "@wdio/logger" "8.38.0"
- "@wdio/protocols" "8.40.3"
- "@wdio/types" "8.41.0"
- "@wdio/utils" "8.41.0"
+ "@types/node" "^20.1.1"
+ "@vitest/snapshot" "^2.1.1"
+ "@wdio/config" "9.12.6"
+ "@wdio/globals" "9.12.7"
+ "@wdio/logger" "9.4.4"
+ "@wdio/protocols" "9.12.5"
+ "@wdio/types" "9.12.6"
+ "@wdio/utils" "9.12.6"
async-exit-hook "^2.0.1"
chalk "^5.2.0"
chokidar "^4.0.0"
- cli-spinners "^2.9.0"
dotenv "^16.3.1"
ejs "^3.1.9"
- execa "^8.0.1"
+ execa "^9.2.0"
import-meta-resolve "^4.0.0"
- inquirer "9.2.12"
+ inquirer "^11.0.1"
lodash.flattendeep "^4.4.0"
lodash.pickby "^4.6.0"
lodash.union "^4.6.0"
- read-pkg-up "10.0.0"
+ read-pkg-up "^10.0.0"
recursive-readdir "^2.2.3"
- webdriverio "8.41.0"
+ tsx "^4.7.2"
+ webdriverio "9.12.7"
yargs "^17.7.2"
"@wdio/config@8.41.0":
@@ -2266,7 +2592,36 @@
glob "^10.2.2"
import-meta-resolve "^4.0.0"
-"@wdio/globals@8.41.0", "@wdio/globals@^8.29.3":
+"@wdio/config@9.12.6":
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/@wdio/config/-/config-9.12.6.tgz#2b1d1b7950450940563d54ef988eec2dcc2a3bb3"
+ integrity sha512-zlOJixJUHxeoyfIN/KdM797HwJj/oNgBaEdftgJARqbXt5AVZu18vJ3zljb+wzbY2M0pl7Y4+5OFH06WlDgQ+A==
+ dependencies:
+ "@wdio/logger" "9.4.4"
+ "@wdio/types" "9.12.6"
+ "@wdio/utils" "9.12.6"
+ deepmerge-ts "^7.0.3"
+ glob "^10.2.2"
+ import-meta-resolve "^4.0.0"
+
+"@wdio/dot-reporter@9.12.6":
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/@wdio/dot-reporter/-/dot-reporter-9.12.6.tgz#84490a9c0d3a0e7b14768b98e3b23ae4d526a8cf"
+ integrity sha512-den2sRD+blw6ymI97X808ESxQ5cVVuNeu5V2VUJk3NWA7Q36cuaJ8s/rPKzcF0CdfdbkkKF8pEQWUVDrrWsK7Q==
+ dependencies:
+ "@wdio/reporter" "9.12.6"
+ "@wdio/types" "9.12.6"
+ chalk "^5.0.1"
+
+"@wdio/globals@9.12.7":
+ version "9.12.7"
+ resolved "https://registry.yarnpkg.com/@wdio/globals/-/globals-9.12.7.tgz#c700a4e769d2c27766e7787581b4fa66b1e86e5c"
+ integrity sha512-WanmrLXRMmW3hwsXCm+x618gDsdGwkrxhiirgMC9Ny0g78qt7JLSOvAHKx+dCZtk77QwvFuNpLCd+Nxnszon9Q==
+ optionalDependencies:
+ expect-webdriverio "^5.1.0"
+ webdriverio "9.12.7"
+
+"@wdio/globals@^8.29.3":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@wdio/globals/-/globals-8.41.0.tgz#3c68a814ff993201834517ddfbbd728d89bdfc54"
integrity sha512-xfUpEppdKzMHy4qoSoQN1cXoBPPh7oMeX+U/jtdvOtla+dd/YZ8pu47zLhQ/GM3gDVrBGnO4w3u4L6Zf/P3KEw==
@@ -2274,16 +2629,16 @@
expect-webdriverio "^4.11.2"
webdriverio "8.41.0"
-"@wdio/local-runner@^8.10.5":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-8.41.0.tgz#ae3c2c1e0f7e40b3a239a96d64124643a35a564c"
- integrity sha512-A5msAjAC8gqiWvtFl+VNm9BBlVb5q3a2o7i+L+Cw7idV3aFY5etigB2wLYMtyBWgB8cXvbZaxXizHhGvZ+iB8Q==
+"@wdio/local-runner@^9.12.7":
+ version "9.12.7"
+ resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-9.12.7.tgz#959752a6e2457254caa5a6545ed8f33341264270"
+ integrity sha512-DabNXK0VkF7dZfhdGsfOXHnBNQ4Xy+TgFxJOXAcDKHw6nJ+WFf/pzvatV8Cye4oEvPeyhFHZlcLYJiMKUt9LKw==
dependencies:
- "@types/node" "^22.2.0"
- "@wdio/logger" "8.38.0"
- "@wdio/repl" "8.40.3"
- "@wdio/runner" "8.41.0"
- "@wdio/types" "8.41.0"
+ "@types/node" "^20.1.0"
+ "@wdio/logger" "9.4.4"
+ "@wdio/repl" "9.4.4"
+ "@wdio/runner" "9.12.7"
+ "@wdio/types" "9.12.6"
async-exit-hook "^2.0.1"
split2 "^4.1.0"
stream-buffers "^3.0.2"
@@ -2298,23 +2653,38 @@
loglevel-plugin-prefix "^0.8.4"
strip-ansi "^7.1.0"
-"@wdio/mocha-framework@^8.10.4":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/mocha-framework/-/mocha-framework-8.41.0.tgz#3582434e60501c3589b0c9602418c127593f50a2"
- integrity sha512-wVSU/kZOp//QiwF+V5xl3tDtZixJdGIoBn3FJ/0qltDkBME7wj+Rvn3Sp3KbbwgaNFwx0HpTbfYqnfCCSMibXw==
+"@wdio/logger@9.4.4", "@wdio/logger@^9.1.3":
+ version "9.4.4"
+ resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-9.4.4.tgz#e4851256a076e2b9401f45caaa7a34d6f0278d4a"
+ integrity sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==
dependencies:
- "@types/mocha" "^10.0.0"
- "@types/node" "^22.2.0"
- "@wdio/logger" "8.38.0"
- "@wdio/types" "8.41.0"
- "@wdio/utils" "8.41.0"
- mocha "^10.0.0"
+ chalk "^5.1.2"
+ loglevel "^1.6.0"
+ loglevel-plugin-prefix "^0.8.4"
+ strip-ansi "^7.1.0"
+
+"@wdio/mocha-framework@^9.12.6":
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/@wdio/mocha-framework/-/mocha-framework-9.12.6.tgz#e2f2503e3fd58122aa60be081b1631ec3c64a9d6"
+ integrity sha512-vBAtVY+PLCGTZqTqsfxNtPVthk6tKI4JSffgGNlWDK/uCcjUOZdvsRRs7VRLr8COyeP1QQFzJ8cHDpCu8nd7Fw==
+ dependencies:
+ "@types/mocha" "^10.0.6"
+ "@types/node" "^20.11.28"
+ "@wdio/logger" "9.4.4"
+ "@wdio/types" "9.12.6"
+ "@wdio/utils" "9.12.6"
+ mocha "^10.3.0"
"@wdio/protocols@8.40.3":
version "8.40.3"
resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-8.40.3.tgz#cf823f4a571b650750b12b9033b65cf177fdb367"
integrity sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==
+"@wdio/protocols@9.12.5":
+ version "9.12.5"
+ resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-9.12.5.tgz#68f21c03c0ed19cde435fa119b9dd6fee7c9ac0c"
+ integrity sha512-i+yc0EZtZOh5fFuwHxvcnXeTXk2ZjFICRbcAxTNE0F2Jr4uOydvcAOw4EIIRmb9NWUSPf/bGZAA+4SEXmxmjUA==
+
"@wdio/repl@8.40.3":
version "8.40.3"
resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-8.40.3.tgz#897b225b4ea1b961ac014ff0a6cb51c8917bd139"
@@ -2322,56 +2692,62 @@
dependencies:
"@types/node" "^22.2.0"
-"@wdio/reporter@8.41.0":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-8.41.0.tgz#0d68a6f0b4b81c02a9b3ba4df69fb0a991d2e53b"
- integrity sha512-LmQ6tnZA3fxctBjJiMzQHDMx7+EbsYojTR2ZFiy5eOYr/pbieQiMl8OHv7xq4ketmBJIKOYBsG4stirbGf6RYg==
+"@wdio/repl@9.4.4":
+ version "9.4.4"
+ resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-9.4.4.tgz#47a5393e908990a3ba34da8561cd87b8fd1a2c1a"
+ integrity sha512-kchPRhoG/pCn4KhHGiL/ocNhdpR8OkD2e6sANlSUZ4TGBVi86YSIEjc2yXUwLacHknC/EnQk/SFnqd4MsNjGGg==
dependencies:
- "@types/node" "^22.2.0"
- "@wdio/logger" "8.38.0"
- "@wdio/types" "8.41.0"
+ "@types/node" "^20.1.0"
+
+"@wdio/reporter@9.12.6":
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-9.12.6.tgz#116641dc4e416eddbdc999ba1d509ffdcdfacbcc"
+ integrity sha512-8cR74tEp5nzC8nP59n4hkDUpoaHUNDbJvP3jD9EfX+ZO4OgPMyJMVTGubo1o88Wuty/Gd2jvOZLHoGD8KlHcKw==
+ dependencies:
+ "@types/node" "^20.1.0"
+ "@wdio/logger" "9.4.4"
+ "@wdio/types" "9.12.6"
diff "^7.0.0"
object-inspect "^1.12.0"
-"@wdio/runner@8.41.0":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-8.41.0.tgz#fe4063bf55f7812b02fd23dfb01a7c5582e9345a"
- integrity sha512-eQ9vZaHIXBLw7XqiKsasiUGjC8PgJawnHFMPKS0i/4ds+5arHo6ciX0s2uhJ3j/EHw3PYvFPCREp/sXetRuNlQ==
+"@wdio/runner@9.12.7":
+ version "9.12.7"
+ resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-9.12.7.tgz#f40d720809bab54aa3f1ee7402459c7f37b7e752"
+ integrity sha512-rslRCDT712SUxCyCpIvLZAnfB5+pMQA3asXfVzLD+CKs5MP5ywh7yW+BnYAT6HvT+Br4MrHt+0rVCJkrsXq4JA==
dependencies:
- "@types/node" "^22.2.0"
- "@wdio/config" "8.41.0"
- "@wdio/globals" "8.41.0"
- "@wdio/logger" "8.38.0"
- "@wdio/types" "8.41.0"
- "@wdio/utils" "8.41.0"
- deepmerge-ts "^5.1.0"
- expect-webdriverio "^4.12.0"
- gaze "^1.1.3"
- webdriver "8.41.0"
- webdriverio "8.41.0"
+ "@types/node" "^20.11.28"
+ "@wdio/config" "9.12.6"
+ "@wdio/dot-reporter" "9.12.6"
+ "@wdio/globals" "9.12.7"
+ "@wdio/logger" "9.4.4"
+ "@wdio/types" "9.12.6"
+ "@wdio/utils" "9.12.6"
+ deepmerge-ts "^7.0.3"
+ expect-webdriverio "^5.1.0"
+ webdriver "9.12.6"
+ webdriverio "9.12.7"
-"@wdio/shared-store-service@^8.10.5":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/shared-store-service/-/shared-store-service-8.41.0.tgz#b0042b3f1c73fe4a208f00649310e3e3e5ecb05b"
- integrity sha512-mS0FB5p1mEouaudlHJatQ3bR24/Kz3CIDBllytzsY1DFElL4h46YImqVfFjqubN9wkszyDExEMLNMsyCighXYw==
+"@wdio/shared-store-service@^9.12.7":
+ version "9.12.7"
+ resolved "https://registry.yarnpkg.com/@wdio/shared-store-service/-/shared-store-service-9.12.7.tgz#0efa9a1d4035e10e534ec5d8048812b74d16e259"
+ integrity sha512-ZfgwS/RcoEmFD74nfx4CQDIzbOAyOCPtXB5fjXZwdVpveWlNkaBb+/x6u17Gup5elQ3zn7AQWB4bQUFvquhXJg==
dependencies:
"@polka/parse" "^1.0.0-next.0"
- "@wdio/logger" "8.38.0"
- "@wdio/types" "8.41.0"
- got "^12.6.1"
+ "@wdio/logger" "9.4.4"
+ "@wdio/types" "9.12.6"
polka "^0.5.2"
- webdriverio "8.41.0"
+ webdriverio "9.12.7"
-"@wdio/spec-reporter@^8.10.5":
- version "8.41.0"
- resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-8.41.0.tgz#a6df1877d2059230cb1b3f226379ca1150d54c11"
- integrity sha512-JrIp6Chc6e0athApSoMJFLVnIqrNRUcVjhg93BbesaDDq6yuB0vJ+rHSHKlUqCPpVYO7YJLmLP4kKPDcVf+4ow==
+"@wdio/spec-reporter@^9.12.6":
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-9.12.6.tgz#ce14e871c6b8424d22bbcd606c90055591f1116b"
+ integrity sha512-zLBbp5tuCdwyxCdh7IXdFk7fideP/e/U8GjuWpRMMuGHCrfIdVxShM5CEq1XEv7Lnw4RWkO6Yo00LU4F0Lafmg==
dependencies:
- "@wdio/reporter" "8.41.0"
- "@wdio/types" "8.41.0"
+ "@wdio/reporter" "9.12.6"
+ "@wdio/types" "9.12.6"
chalk "^5.1.2"
easy-table "^1.2.0"
- pretty-ms "^7.0.0"
+ pretty-ms "^9.0.0"
"@wdio/types@8.41.0":
version "8.41.0"
@@ -2380,6 +2756,13 @@
dependencies:
"@types/node" "^22.2.0"
+"@wdio/types@9.12.6":
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/@wdio/types/-/types-9.12.6.tgz#62b243fa49c5de76f265882c506f63cb4c6a9622"
+ integrity sha512-WzZhaN834du9wjqT/Go9qPyB7VkzV2bjr6pr06DrIzxIpJq/snWOv96C6OjJu8nmYNRjV769mAxyggBUf+sUoQ==
+ dependencies:
+ "@types/node" "^20.1.0"
+
"@wdio/utils@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-8.41.0.tgz#1640e94bc6f71a2d99535d71483c3b54f9732c4a"
@@ -2399,6 +2782,25 @@
split2 "^4.2.0"
wait-port "^1.0.4"
+"@wdio/utils@9.12.6":
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-9.12.6.tgz#eb789fccb9b1a0c44b72dd6ad7f63785d407ed6e"
+ integrity sha512-JfI4CxBRQCOgToJeQNaZLv+wYNIGyJG1gqrpxUOvkrJvBgdOAmIu3dzlcKP/WviXlcxvwLQF2FK8bQVTjHv0fQ==
+ dependencies:
+ "@puppeteer/browsers" "^2.2.0"
+ "@wdio/logger" "9.4.4"
+ "@wdio/types" "9.12.6"
+ decamelize "^6.0.0"
+ deepmerge-ts "^7.0.3"
+ edgedriver "^6.1.1"
+ geckodriver "^5.0.0"
+ get-port "^7.0.0"
+ import-meta-resolve "^4.0.0"
+ locate-app "^2.2.24"
+ safaridriver "^1.0.0"
+ split2 "^4.2.0"
+ wait-port "^1.1.0"
+
"@xmldom/xmldom@^0.8.3", "@xmldom/xmldom@^0.8.7":
version "0.8.10"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
@@ -2414,6 +2816,11 @@
resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.7.57.tgz#66a7ddc071f3e3aa789af50647c04a525685e1a4"
integrity sha512-BtonQ1/jDnGiMed6OkV6rZYW78gLmLswkHOzyMrMb+CAR7CZO8phOHO6c2qw6qb1g1betN7kwEHhhZk30dv+NA==
+"@zip.js/zip.js@^2.7.53":
+ version "2.7.60"
+ resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.7.60.tgz#0de96b93519cad804c82f96faebceda836cb24c0"
+ integrity sha512-vA3rLyqdxBrVo1FWSsbyoecaqWTV+vgPRf0QKeM7kVDG0r+lHUqd7zQDv1TO9k4BcAoNzNDSNrrel24Mk6addA==
+
abbrev@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-3.0.0.tgz#c29a6337e167ac61a84b41b80461b29c5c271a27"
@@ -2584,7 +2991,7 @@ archiver-utils@^5.0.0, archiver-utils@^5.0.2:
normalize-path "^3.0.0"
readable-stream "^4.0.0"
-archiver@^7.0.0:
+archiver@^7.0.0, archiver@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61"
integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==
@@ -2612,7 +3019,7 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-aria-query@5.3.2, aria-query@^5.0.0:
+aria-query@5.3.2, aria-query@^5.0.0, aria-query@^5.3.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
@@ -3231,7 +3638,7 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chalk@^5.1.2, chalk@^5.2.0, chalk@^5.3.0:
+chalk@^5.0.1, chalk@^5.1.2, chalk@^5.2.0, chalk@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8"
integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==
@@ -3255,6 +3662,35 @@ chartjs-plugin-zoom@~2.0.1:
dependencies:
hammerjs "^2.0.8"
+cheerio-select@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
+ integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==
+ dependencies:
+ boolbase "^1.0.0"
+ css-select "^5.1.0"
+ css-what "^6.1.0"
+ domelementtype "^2.3.0"
+ domhandler "^5.0.3"
+ domutils "^3.0.1"
+
+cheerio@^1.0.0-rc.12:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81"
+ integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==
+ dependencies:
+ cheerio-select "^2.1.0"
+ dom-serializer "^2.0.0"
+ domhandler "^5.0.3"
+ domutils "^3.1.0"
+ encoding-sniffer "^0.2.0"
+ htmlparser2 "^9.1.0"
+ parse5 "^7.1.2"
+ parse5-htmlparser2-tree-adapter "^7.0.0"
+ parse5-parser-stream "^7.1.2"
+ undici "^6.19.5"
+ whatwg-mimetype "^4.0.0"
+
chokidar@^3.5.1, chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
@@ -3335,7 +3771,7 @@ cli-cursor@^5.0.0:
dependencies:
restore-cursor "^5.0.0"
-cli-spinners@^2.5.0, cli-spinners@^2.9.0:
+cli-spinners@^2.5.0:
version "2.9.2"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41"
integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==
@@ -3766,6 +4202,11 @@ deepmerge-ts@^5.0.0, deepmerge-ts@^5.1.0:
resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz#c55206cc4c7be2ded89b9c816cf3608884525d7a"
integrity sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==
+deepmerge-ts@^7.0.3:
+ version "7.1.5"
+ resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz#ff818564007f5c150808d2b7b732cac83aa415ab"
+ integrity sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==
+
defaults@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
@@ -4006,6 +4447,21 @@ edgedriver@^5.5.0:
node-fetch "^3.3.2"
which "^4.0.0"
+edgedriver@^6.1.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/edgedriver/-/edgedriver-6.1.1.tgz#9374160a0aa1bab48d3ed21dc7af8f6fe2c3103f"
+ integrity sha512-/dM/PoBf22Xg3yypMWkmRQrBKEnSyNaZ7wHGCT9+qqT14izwtFT+QvdR89rjNkMfXwW+bSFoqOfbcvM+2Cyc7w==
+ dependencies:
+ "@wdio/logger" "^9.1.3"
+ "@zip.js/zip.js" "^2.7.53"
+ decamelize "^6.0.0"
+ edge-paths "^3.0.5"
+ fast-xml-parser "^4.5.0"
+ http-proxy-agent "^7.0.2"
+ https-proxy-agent "^7.0.5"
+ node-fetch "^3.3.2"
+ which "^5.0.0"
+
ejs@^3.1.9:
version "3.1.10"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
@@ -4046,6 +4502,14 @@ emoji-regex@^9.2.2:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+encoding-sniffer@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5"
+ integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==
+ dependencies:
+ iconv-lite "^0.6.3"
+ whatwg-encoding "^3.1.1"
+
encoding@^0.1.13:
version "0.1.13"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
@@ -4244,6 +4708,37 @@ esbuild@0.24.2, esbuild@^0.24.2:
"@esbuild/win32-ia32" "0.24.2"
"@esbuild/win32-x64" "0.24.2"
+esbuild@~0.25.0:
+ version "0.25.3"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.3.tgz#371f7cb41283e5b2191a96047a7a89562965a285"
+ integrity sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.25.3"
+ "@esbuild/android-arm" "0.25.3"
+ "@esbuild/android-arm64" "0.25.3"
+ "@esbuild/android-x64" "0.25.3"
+ "@esbuild/darwin-arm64" "0.25.3"
+ "@esbuild/darwin-x64" "0.25.3"
+ "@esbuild/freebsd-arm64" "0.25.3"
+ "@esbuild/freebsd-x64" "0.25.3"
+ "@esbuild/linux-arm" "0.25.3"
+ "@esbuild/linux-arm64" "0.25.3"
+ "@esbuild/linux-ia32" "0.25.3"
+ "@esbuild/linux-loong64" "0.25.3"
+ "@esbuild/linux-mips64el" "0.25.3"
+ "@esbuild/linux-ppc64" "0.25.3"
+ "@esbuild/linux-riscv64" "0.25.3"
+ "@esbuild/linux-s390x" "0.25.3"
+ "@esbuild/linux-x64" "0.25.3"
+ "@esbuild/netbsd-arm64" "0.25.3"
+ "@esbuild/netbsd-x64" "0.25.3"
+ "@esbuild/openbsd-arm64" "0.25.3"
+ "@esbuild/openbsd-x64" "0.25.3"
+ "@esbuild/sunos-x64" "0.25.3"
+ "@esbuild/win32-arm64" "0.25.3"
+ "@esbuild/win32-ia32" "0.25.3"
+ "@esbuild/win32-x64" "0.25.3"
+
escalade@^3.1.1, escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
@@ -4264,11 +4759,6 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-escape-string-regexp@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
- integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
-
escodegen@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17"
@@ -4499,27 +4989,30 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
-execa@^8.0.1:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c"
- integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==
+execa@^9.2.0:
+ version "9.5.2"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-9.5.2.tgz#a4551034ee0795e241025d2f987dab3f4242dff2"
+ integrity sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==
dependencies:
+ "@sindresorhus/merge-streams" "^4.0.0"
cross-spawn "^7.0.3"
- get-stream "^8.0.1"
- human-signals "^5.0.0"
- is-stream "^3.0.0"
- merge-stream "^2.0.0"
- npm-run-path "^5.1.0"
- onetime "^6.0.0"
+ figures "^6.1.0"
+ get-stream "^9.0.0"
+ human-signals "^8.0.0"
+ is-plain-obj "^4.1.0"
+ is-stream "^4.0.1"
+ npm-run-path "^6.0.0"
+ pretty-ms "^9.0.0"
signal-exit "^4.1.0"
- strip-final-newline "^3.0.0"
+ strip-final-newline "^4.0.0"
+ yoctocolors "^2.0.0"
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
-expect-webdriverio@^4.11.2, expect-webdriverio@^4.12.0, expect-webdriverio@^4.2.3:
+expect-webdriverio@^4.11.2, expect-webdriverio@^4.2.3:
version "4.15.4"
resolved "https://registry.yarnpkg.com/expect-webdriverio/-/expect-webdriverio-4.15.4.tgz#0d0c572e6aa6477c5094b0c689106bc8890d1416"
integrity sha512-Op1xZoevlv1pohCq7g2Og5Gr3xP2NhY7MQueOApmopVxgweoJ/BqJxyvMNP0A//QsMg8v0WsN/1j81Sx2er9Wg==
@@ -4533,6 +5026,16 @@ expect-webdriverio@^4.11.2, expect-webdriverio@^4.12.0, expect-webdriverio@^4.2.
"@wdio/logger" "^8.28.0"
webdriverio "^8.29.3"
+expect-webdriverio@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/expect-webdriverio/-/expect-webdriverio-5.1.0.tgz#53fe0ffdd9226cb0ee28c5f179b3aa1f47436abf"
+ integrity sha512-4u3q+Dqx/lXNgvCx1gKia4CfS28z1UxGGfVUkoMNbrsBlTBB2fYqXG+4+YtYoerxvp/XPwIb/+89IGEdyPbDXQ==
+ dependencies:
+ "@vitest/snapshot" "^2.0.5"
+ expect "^29.7.0"
+ jest-matcher-utils "^29.7.0"
+ lodash.isequal "^4.5.0"
+
expect@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc"
@@ -4563,7 +5066,7 @@ external-editor@^3.1.0:
iconv-lite "^0.4.24"
tmp "^0.0.33"
-extract-zip@2.0.1:
+extract-zip@2.0.1, extract-zip@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@@ -4622,6 +5125,13 @@ fast-xml-parser@^4.4.1:
dependencies:
strnum "^1.0.5"
+fast-xml-parser@^4.5.0:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz#c54d6b35aa0f23dc1ea60b6c884340c006dc6efb"
+ integrity sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==
+ dependencies:
+ strnum "^1.1.1"
+
fastest-levenshtein@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
@@ -4654,13 +5164,12 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
-figures@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/figures/-/figures-5.0.0.tgz#126cd055052dea699f8a54e8c9450e6ecfc44d5f"
- integrity sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==
+figures@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a"
+ integrity sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==
dependencies:
- escape-string-regexp "^5.0.0"
- is-unicode-supported "^1.2.0"
+ is-unicode-supported "^2.0.0"
file-entry-cache@^10.0.5:
version "10.0.6"
@@ -4866,12 +5375,19 @@ functions-have-names@^1.2.3:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
-gaze@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
- integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==
+geckodriver@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-5.0.0.tgz#88437f3812075988bb05b5e19dc4aaa42d200577"
+ integrity sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==
dependencies:
- globule "^1.0.0"
+ "@wdio/logger" "^9.1.3"
+ "@zip.js/zip.js" "^2.7.53"
+ decamelize "^6.0.0"
+ http-proxy-agent "^7.0.2"
+ https-proxy-agent "^7.0.5"
+ node-fetch "^3.3.2"
+ tar-fs "^3.0.6"
+ which "^5.0.0"
geckodriver@~4.2.0:
version "4.2.1"
@@ -4943,10 +5459,13 @@ get-stream@^6.0.1:
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
-get-stream@^8.0.1:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2"
- integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==
+get-stream@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27"
+ integrity sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==
+ dependencies:
+ "@sec-ant/readable-stream" "^0.4.1"
+ is-stream "^4.0.1"
get-symbol-description@^1.1.0:
version "1.1.0"
@@ -4957,6 +5476,13 @@ get-symbol-description@^1.1.0:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
+get-tsconfig@^4.7.5:
+ version "4.10.0"
+ resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz#403a682b373a823612475a4c2928c7326fc0f6bb"
+ integrity sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==
+ dependencies:
+ resolve-pkg-maps "^1.0.0"
+
get-uri@^6.0.1:
version "6.0.4"
resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.4.tgz#6daaee9e12f9759e19e55ba313956883ef50e0a7"
@@ -5037,18 +5563,6 @@ glob@^8.1.0:
minimatch "^5.0.1"
once "^1.3.0"
-glob@~7.1.1:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
- integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
global-modules@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -5110,15 +5624,6 @@ globjoin@^0.1.4:
resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==
-globule@^1.0.0:
- version "1.3.4"
- resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.4.tgz#7c11c43056055a75a6e68294453c17f2796170fb"
- integrity sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==
- dependencies:
- glob "~7.1.1"
- lodash "^4.17.21"
- minimatch "~3.0.2"
-
gopd@^1.0.1, gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
@@ -5146,7 +5651,7 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2,
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
-grapheme-splitter@^1.0.2:
+grapheme-splitter@^1.0.2, grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
@@ -5282,6 +5787,11 @@ html-tags@^3.3.1:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
+htmlfy@^0.6.0:
+ version "0.6.7"
+ resolved "https://registry.yarnpkg.com/htmlfy/-/htmlfy-0.6.7.tgz#598172336a75915e41e4abf558656e11fcc4c449"
+ integrity sha512-r8hRd+oIM10lufovN+zr3VKPTYEIvIwqXGucidh2XQufmiw6sbUXFUFjWlfjo3AnefIDTyzykVzQ8IUVuT1peQ==
+
htmlparser2@^8.0.0:
version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
@@ -5307,7 +5817,7 @@ http-cache-semantics@^4.1.1:
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
-http-proxy-agent@^7.0.0:
+http-proxy-agent@^7.0.0, http-proxy-agent@^7.0.1, http-proxy-agent@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
@@ -5328,7 +5838,7 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==
-https-proxy-agent@7.0.6, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.6:
+https-proxy-agent@7.0.6, https-proxy-agent@^7.0.1, https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.5, https-proxy-agent@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
@@ -5344,10 +5854,17 @@ https-proxy-agent@^5.0.1:
agent-base "6"
debug "4"
-human-signals@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28"
- integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==
+human-signals@^8.0.0:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-8.0.1.tgz#f08bb593b6d1db353933d06156cedec90abe51fb"
+ integrity sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==
+
+iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
iconv-lite@^0.4.24, iconv-lite@~0.4.24:
version "0.4.24"
@@ -5356,13 +5873,6 @@ iconv-lite@^0.4.24, iconv-lite@~0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-iconv-lite@^0.6.2:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
- integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
- dependencies:
- safer-buffer ">= 2.1.2 < 3.0.0"
-
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -5446,26 +5956,19 @@ ini@^1.3.2, ini@^1.3.5, ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
-inquirer@9.2.12:
- version "9.2.12"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.2.12.tgz#0348e9311765b7c93fce143bb1c0ef1ae879b1d7"
- integrity sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==
+inquirer@^11.0.1:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-11.1.0.tgz#aa540337ae049a4a0ac6bab07f46f4664c4dab0c"
+ integrity sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==
dependencies:
- "@ljharb/through" "^2.3.11"
+ "@inquirer/core" "^9.2.1"
+ "@inquirer/prompts" "^6.0.1"
+ "@inquirer/type" "^2.0.0"
+ "@types/mute-stream" "^0.0.4"
ansi-escapes "^4.3.2"
- chalk "^5.3.0"
- cli-cursor "^3.1.0"
- cli-width "^4.1.0"
- external-editor "^3.1.0"
- figures "^5.0.0"
- lodash "^4.17.21"
- mute-stream "1.0.0"
- ora "^5.4.1"
+ mute-stream "^1.0.0"
run-async "^3.0.0"
rxjs "^7.8.1"
- string-width "^4.2.3"
- strip-ansi "^6.0.1"
- wrap-ansi "^6.2.0"
internal-slot@^1.1.0:
version "1.1.0"
@@ -5732,10 +6235,10 @@ is-stream@^2.0.1:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
-is-stream@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac"
- integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==
+is-stream@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b"
+ integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==
is-string@^1.0.7, is-string@^1.1.1:
version "1.1.1"
@@ -5766,10 +6269,10 @@ is-unicode-supported@^0.1.0:
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
-is-unicode-supported@^1.2.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714"
- integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==
+is-unicode-supported@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a"
+ integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==
is-weakmap@^2.0.2:
version "2.0.2"
@@ -6129,7 +6632,7 @@ lmdb@3.2.2:
"@lmdb/lmdb-linux-x64" "3.2.2"
"@lmdb/lmdb-win32-x64" "3.2.2"
-locate-app@^2.1.0:
+locate-app@^2.1.0, locate-app@^2.2.24:
version "2.5.0"
resolved "https://registry.yarnpkg.com/locate-app/-/locate-app-2.5.0.tgz#4c1e0e78678bffa8cb3bf363ee2560fb69ebe467"
integrity sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==
@@ -6204,7 +6707,7 @@ lodash.zip@^4.2.0:
resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020"
integrity sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==
-lodash@^4.17.15, lodash@^4.17.21:
+lodash@^4.17.15:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -6402,11 +6905,6 @@ meow@^13.2.0:
resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f"
integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==
-merge-stream@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
- integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
-
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -6450,11 +6948,6 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
-mimic-fn@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
- integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
-
mimic-function@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076"
@@ -6508,13 +7001,6 @@ minimatch@^9.0.0, minimatch@^9.0.4, minimatch@^9.0.5:
dependencies:
brace-expansion "^2.0.1"
-minimatch@~3.0.2:
- version "3.0.8"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1"
- integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==
- dependencies:
- brace-expansion "^1.1.7"
-
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6, minimist@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@@ -6619,7 +7105,7 @@ mkdirp@^3.0.1:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
-mocha@^10.0.0:
+mocha@^10.3.0:
version "10.8.2"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96"
integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==
@@ -6696,7 +7182,7 @@ msgpackr@^1.11.2:
optionalDependencies:
msgpackr-extract "^3.0.2"
-mute-stream@1.0.0, mute-stream@^1.0.0:
+mute-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e"
integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==
@@ -6976,12 +7462,13 @@ npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
-npm-run-path@^5.1.0:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f"
- integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==
+npm-run-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-6.0.0.tgz#25cfdc4eae04976f3349c0b1afc089052c362537"
+ integrity sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==
dependencies:
path-key "^4.0.0"
+ unicorn-magic "^0.3.0"
nth-check@^2.0.1:
version "2.1.1"
@@ -7063,13 +7550,6 @@ onetime@^5.1.0:
dependencies:
mimic-fn "^2.1.0"
-onetime@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4"
- integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==
- dependencies:
- mimic-fn "^4.0.0"
-
onetime@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60"
@@ -7089,7 +7569,7 @@ optionator@^0.9.3:
type-check "^0.4.0"
word-wrap "^1.2.5"
-ora@5.4.1, ora@^5.4.1:
+ora@5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"
integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==
@@ -7216,6 +7696,20 @@ pac-proxy-agent@^7.0.1:
pac-resolver "^7.0.1"
socks-proxy-agent "^8.0.5"
+pac-proxy-agent@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df"
+ integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==
+ dependencies:
+ "@tootallnate/quickjs-emscripten" "^0.23.0"
+ agent-base "^7.1.2"
+ debug "^4.3.4"
+ get-uri "^6.0.1"
+ http-proxy-agent "^7.0.0"
+ https-proxy-agent "^7.0.6"
+ pac-resolver "^7.0.1"
+ socks-proxy-agent "^8.0.5"
+
pac-resolver@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6"
@@ -7305,10 +7799,10 @@ parse-json@^7.0.0:
lines-and-columns "^2.0.3"
type-fest "^3.8.0"
-parse-ms@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
- integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
+parse-ms@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-4.0.0.tgz#c0c058edd47c2a590151a718990533fd62803df4"
+ integrity sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==
parse5-html-rewriting-stream@7.0.0:
version "7.0.0"
@@ -7319,6 +7813,21 @@ parse5-html-rewriting-stream@7.0.0:
parse5 "^7.0.0"
parse5-sax-parser "^7.0.0"
+parse5-htmlparser2-tree-adapter@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b"
+ integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==
+ dependencies:
+ domhandler "^5.0.3"
+ parse5 "^7.0.0"
+
+parse5-parser-stream@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1"
+ integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==
+ dependencies:
+ parse5 "^7.0.0"
+
parse5-sax-parser@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz#4c05064254f0488676aca75fb39ca069ec96dee5"
@@ -7542,12 +8051,12 @@ pretty-format@^29.7.0:
ansi-styles "^5.0.0"
react-is "^18.0.0"
-pretty-ms@^7.0.0:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8"
- integrity sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==
+pretty-ms@^9.0.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.2.0.tgz#e14c0aad6493b69ed63114442a84133d7e560ef0"
+ integrity sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==
dependencies:
- parse-ms "^2.1.0"
+ parse-ms "^4.0.0"
primeng@^17:
version "17.18.15"
@@ -7571,7 +8080,7 @@ process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
-progress@2.0.3:
+progress@2.0.3, progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@@ -7603,6 +8112,20 @@ proxy-agent@6.3.1:
proxy-from-env "^1.1.0"
socks-proxy-agent "^8.0.2"
+proxy-agent@^6.5.0:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.5.0.tgz#9e49acba8e4ee234aacb539f89ed9c23d02f232d"
+ integrity sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==
+ dependencies:
+ agent-base "^7.1.2"
+ debug "^4.3.4"
+ http-proxy-agent "^7.0.1"
+ https-proxy-agent "^7.0.6"
+ lru-cache "^7.14.1"
+ pac-proxy-agent "^7.1.0"
+ proxy-from-env "^1.1.0"
+ socks-proxy-agent "^8.0.5"
+
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
@@ -7678,7 +8201,7 @@ qs@^6.12.3:
dependencies:
side-channel "^1.1.0"
-query-selector-shadow-dom@^1.0.0:
+query-selector-shadow-dom@^1.0.0, query-selector-shadow-dom@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz#1c7b0058eff4881ac44f45d8f84ede32e9a2f349"
integrity sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==
@@ -7738,16 +8261,16 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
-read-pkg-up@10.0.0:
- version "10.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-10.0.0.tgz#0542d21ff1001d2bfff1f6eac8b4d1d1dc486617"
- integrity sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==
+read-pkg-up@^10.0.0:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-10.1.0.tgz#2d13ab732d2f05d6e8094167c2112e2ee50644f4"
+ integrity sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==
dependencies:
find-up "^6.3.0"
- read-pkg "^8.0.0"
- type-fest "^3.12.0"
+ read-pkg "^8.1.0"
+ type-fest "^4.2.0"
-read-pkg@^8.0.0:
+read-pkg@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-8.1.0.tgz#6cf560b91d90df68bce658527e7e3eee75f7c4c7"
integrity sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==
@@ -7882,6 +8405,11 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+resolve-pkg-maps@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
+ integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
+
resolve@1.22.10, resolve@^1.17.0, resolve@^1.22.4:
version "1.22.10"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39"
@@ -7898,7 +8426,7 @@ responselike@^3.0.0:
dependencies:
lowercase-keys "^3.0.0"
-resq@^1.9.1:
+resq@^1.11.0, resq@^1.9.1:
version "1.11.0"
resolved "https://registry.yarnpkg.com/resq/-/resq-1.11.0.tgz#edec8c58be9af800fd628118c0ca8815283de196"
integrity sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==
@@ -8069,6 +8597,11 @@ safaridriver@^0.1.0:
resolved "https://registry.yarnpkg.com/safaridriver/-/safaridriver-0.1.2.tgz#166571d5881c7d6f884900d92d51ee1309c05aa4"
integrity sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==
+safaridriver@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/safaridriver/-/safaridriver-1.0.0.tgz#bccb5edf9df13b75ca08f23081420f3025ae83ed"
+ integrity sha512-J92IFbskyo7OYB3Dt4aTdyhag1GlInrfbPCmMteb7aBK7PwlnGz1HI0+oyNN97j7pV9DqUAVoVgkNRMrfY47mQ==
+
safe-array-concat@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3"
@@ -8285,7 +8818,12 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semve
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.0.tgz#9c6fe61d0c6f9fa9e26575162ee5a9180361b09c"
integrity sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==
-serialize-error@^11.0.1:
+semver@^7.7.1:
+ version "7.7.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
+ integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
+
+serialize-error@^11.0.1, serialize-error@^11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-11.0.3.tgz#b54f439e15da5b4961340fbbd376b6b04aa52e92"
integrity sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==
@@ -8789,10 +9327,10 @@ strip-bom@^3.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
-strip-final-newline@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
- integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
+strip-final-newline@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
+ integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==
strip-json-comments@3.1.1, strip-json-comments@^3.1.1:
version "3.1.1"
@@ -8809,6 +9347,11 @@ strnum@^1.0.5:
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==
+strnum@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.1.2.tgz#57bca4fbaa6f271081715dbc9ed7cee5493e28e4"
+ integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==
+
stylelint-config-sass-guidelines@^11.0.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/stylelint-config-sass-guidelines/-/stylelint-config-sass-guidelines-11.1.0.tgz#0106f3ec4991a598823b55841bf45fce63268c8c"
@@ -8977,7 +9520,7 @@ tar-fs@^2.0.0:
pump "^3.0.0"
tar-stream "^2.1.4"
-tar-fs@^3.0.4:
+tar-fs@^3.0.4, tar-fs@^3.0.6, tar-fs@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.8.tgz#8f62012537d5ff89252d01e48690dc4ebed33ab7"
integrity sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==
@@ -9163,6 +9706,16 @@ tslib@2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, t
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+tsx@^4.7.2:
+ version "4.19.4"
+ resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.19.4.tgz#647b4141f4fdd9d773a9b564876773d2846901f4"
+ integrity sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==
+ dependencies:
+ esbuild "~0.25.0"
+ get-tsconfig "^4.7.5"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
tty-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
@@ -9211,7 +9764,7 @@ type-fest@^2.12.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
-type-fest@^3.12.0, type-fest@^3.8.0:
+type-fest@^3.8.0:
version "3.13.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706"
integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==
@@ -9327,11 +9880,31 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+undici-types@~6.19.2:
+ version "6.19.8"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
+ integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
+
undici-types@~6.20.0:
version "6.20.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
+undici-types@~6.21.0:
+ version "6.21.0"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
+ integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
+
+undici@^6.19.5, undici@^6.20.1:
+ version "6.21.2"
+ resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.2.tgz#49c5884e8f9039c65a89ee9018ef3c8e2f1f4928"
+ integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==
+
+unicorn-magic@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104"
+ integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==
+
unique-filename@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-4.0.0.tgz#a06534d370e7c977a939cd1d11f7f0ab8f1fed13"
@@ -9400,7 +9973,7 @@ url@^0.11.4:
punycode "^1.4.1"
qs "^6.12.3"
-urlpattern-polyfill@10.0.0:
+urlpattern-polyfill@10.0.0, urlpattern-polyfill@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz#f0a03a97bfb03cdf33553e5e79a2aadd22cac8ec"
integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==
@@ -9593,7 +10166,7 @@ vscode-uri@^3.0.2:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
-wait-port@^1.0.4:
+wait-port@^1.0.4, wait-port@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/wait-port/-/wait-port-1.1.0.tgz#e5d64ee071118d985e2b658ae7ad32b2ce29b6b5"
integrity sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==
@@ -9649,6 +10222,22 @@ webdriver@8.41.0:
ky "^0.33.0"
ws "^8.8.0"
+webdriver@9.12.6:
+ version "9.12.6"
+ resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-9.12.6.tgz#dc5d5bd7030dcacf66119409771e846a7fa03ae0"
+ integrity sha512-Alz+JiaVW15b/Qy6zSmJeYXxvmtMIVpEAg7QDfCWqG9miZSKJYWwgWE3xoSrwYn5kTylUszqb17Pb5wyrj7YFw==
+ dependencies:
+ "@types/node" "^20.1.0"
+ "@types/ws" "^8.5.3"
+ "@wdio/config" "9.12.6"
+ "@wdio/logger" "9.4.4"
+ "@wdio/protocols" "9.12.5"
+ "@wdio/types" "9.12.6"
+ "@wdio/utils" "9.12.6"
+ deepmerge-ts "^7.0.3"
+ undici "^6.20.1"
+ ws "^8.8.0"
+
webdriverio@8.41.0, webdriverio@^8.29.3:
version "8.41.0"
resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-8.41.0.tgz#94837f81123bf8f941bd8375c7c7f6a8a2b20fa2"
@@ -9680,6 +10269,37 @@ webdriverio@8.41.0, webdriverio@^8.29.3:
serialize-error "^11.0.1"
webdriver "8.41.0"
+webdriverio@9.12.7:
+ version "9.12.7"
+ resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-9.12.7.tgz#07e5367ff42a883aa1d1902288708d9923c25cb4"
+ integrity sha512-HxpLQrFuadfE65dqh+Qc2pdvz18FbsdpdiZogy8VUBtxRanijbOsi4cV84ffGXab8Ownzu+bNBJuJjlTBDX00Q==
+ dependencies:
+ "@types/node" "^20.11.30"
+ "@types/sinonjs__fake-timers" "^8.1.5"
+ "@wdio/config" "9.12.6"
+ "@wdio/logger" "9.4.4"
+ "@wdio/protocols" "9.12.5"
+ "@wdio/repl" "9.4.4"
+ "@wdio/types" "9.12.6"
+ "@wdio/utils" "9.12.6"
+ archiver "^7.0.1"
+ aria-query "^5.3.0"
+ cheerio "^1.0.0-rc.12"
+ css-shorthand-properties "^1.1.1"
+ css-value "^0.0.1"
+ grapheme-splitter "^1.0.4"
+ htmlfy "^0.6.0"
+ is-plain-obj "^4.1.0"
+ jszip "^3.10.1"
+ lodash.clonedeep "^4.5.0"
+ lodash.zip "^4.2.0"
+ query-selector-shadow-dom "^1.0.1"
+ resq "^1.11.0"
+ rgb2hex "0.2.5"
+ serialize-error "^11.0.3"
+ urlpattern-polyfill "^10.0.0"
+ webdriver "9.12.6"
+
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@@ -9693,6 +10313,18 @@ webrtc-polyfill@^1.1.10:
node-datachannel "^v0.12.0"
node-domexception "^1.0.0"
+whatwg-encoding@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
+ integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
+ dependencies:
+ iconv-lite "0.6.3"
+
+whatwg-mimetype@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
+ integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
+
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@@ -10025,6 +10657,11 @@ yoctocolors-cjs@^2.1.2:
resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242"
integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==
+yoctocolors@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.1.tgz#e0167474e9fbb9e8b3ecca738deaa61dd12e56fc"
+ integrity sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==
+
zip-stream@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb"
diff --git a/config/default.yaml b/config/default.yaml
index 6e5dae6f6..74bc01f80 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -957,7 +957,7 @@ instance:
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
# Moreover, the NSFW checkbox on video upload will be automatically checked by default
is_nsfw: false
- # By default, `do_not_list` or `blur` or `display` NSFW videos
+ # By default, `do_not_list`, `blur`, `warn` or `display` NSFW videos
# Could be overridden per user with a setting
default_nsfw_policy: 'do_not_list'
diff --git a/config/production.yaml.example b/config/production.yaml.example
index e2fd77185..5660ed159 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -967,7 +967,7 @@ instance:
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
# Moreover, the NSFW checkbox on video upload will be automatically checked by default
is_nsfw: false
- # By default, `do_not_list` or `blur` or `display` NSFW videos
+ # By default, `do_not_list`, `blur`, `warn` or `display` NSFW videos
# Could be overridden per user with a setting
default_nsfw_policy: 'do_not_list'
diff --git a/packages/models/src/activitypub/objects/common-objects.ts b/packages/models/src/activitypub/objects/common-objects.ts
index ad2842384..3b7cbca3f 100644
--- a/packages/models/src/activitypub/objects/common-objects.ts
+++ b/packages/models/src/activitypub/objects/common-objects.ts
@@ -1,4 +1,5 @@
import { AbusePredefinedReasonsString } from '../../moderation/abuse/abuse-reason.model.js'
+import { NSFWFlagString } from '../../videos/nsfw-flag.enum.js'
export interface ActivityIdentifierObject {
identifier: string
@@ -122,11 +123,17 @@ export interface ActivityFlagReasonObject {
name: AbusePredefinedReasonsString
}
+export interface ActivitySensitiveTagObject {
+ type: 'SensitiveTag'
+ name: NSFWFlagString
+}
+
export type ActivityTagObject =
| ActivityPlaylistSegmentHashesObject
| ActivityStreamingPlaylistInfohashesObject
| ActivityVideoUrlObject
| ActivityHashTagObject
+ | ActivitySensitiveTagObject
| ActivityMentionObject
| ActivityBitTorrentUrlObject
| ActivityMagnetUrlObject
diff --git a/packages/models/src/activitypub/objects/video-object.ts b/packages/models/src/activitypub/objects/video-object.ts
index ab44530ab..a19a21432 100644
--- a/packages/models/src/activitypub/objects/video-object.ts
+++ b/packages/models/src/activitypub/objects/video-object.ts
@@ -24,6 +24,7 @@ export interface VideoObject {
views: number
sensitive: boolean
+ summary: string
isLiveBroadcast: boolean
liveSaveReplay: boolean
@@ -80,7 +81,7 @@ export interface VideoObject {
export interface ActivityPubStoryboard {
type: 'Image'
- rel: [ 'storyboard' ]
+ rel: ['storyboard']
url: {
href: string
mediaType: string
diff --git a/packages/models/src/search/videos-common-query.model.ts b/packages/models/src/search/videos-common-query.model.ts
index 81a2f8035..9dbb26393 100644
--- a/packages/models/src/search/videos-common-query.model.ts
+++ b/packages/models/src/search/videos-common-query.model.ts
@@ -9,6 +9,8 @@ export interface VideosCommonQuery {
sort?: string
nsfw?: BooleanBothQuery
+ nsfwFlagsIncluded?: number
+ nsfwFlagsExcluded?: number
isLive?: boolean
diff --git a/packages/models/src/users/user-update-me.model.ts b/packages/models/src/users/user-update-me.model.ts
index ba9672136..fd7080604 100644
--- a/packages/models/src/users/user-update-me.model.ts
+++ b/packages/models/src/users/user-update-me.model.ts
@@ -3,7 +3,12 @@ import { NSFWPolicyType } from '../videos/nsfw-policy.type.js'
export interface UserUpdateMe {
displayName?: string
description?: string
+
nsfwPolicy?: NSFWPolicyType
+ nsfwFlagsDisplayed?: number
+ nsfwFlagsHidden?: number
+ nsfwFlagsBlurred?: number
+ nsfwFlagsWarned?: number
p2pEnabled?: boolean
diff --git a/packages/models/src/users/user.model.ts b/packages/models/src/users/user.model.ts
index ea03e08ca..6ae5a9d8c 100644
--- a/packages/models/src/users/user.model.ts
+++ b/packages/models/src/users/user.model.ts
@@ -14,7 +14,12 @@ export interface User {
emailVerified: boolean
emailPublic: boolean
+
nsfwPolicy: NSFWPolicyType
+ nsfwFlagsDisplayed: number
+ nsfwFlagsHidden: number
+ nsfwFlagsBlurred: number
+ nsfwFlagsWarned: number
adminFlags?: UserAdminFlagType
diff --git a/packages/models/src/videos/import/video-import-create.model.ts b/packages/models/src/videos/import/video-import-create.model.ts
index 81618631a..ce3f1437f 100644
--- a/packages/models/src/videos/import/video-import-create.model.ts
+++ b/packages/models/src/videos/import/video-import-create.model.ts
@@ -1,6 +1,6 @@
-import { VideoUpdate } from '../video-update.model.js'
+import { VideoCreateUpdateCommon } from '../video-create-update-common.model.js'
-export interface VideoImportCreate extends VideoUpdate {
+export interface VideoImportCreate extends VideoCreateUpdateCommon {
targetUrl?: string
magnetUri?: string
torrentfile?: Blob
diff --git a/packages/models/src/videos/index.ts b/packages/models/src/videos/index.ts
index 24f4cf5c7..5cf14d2e3 100644
--- a/packages/models/src/videos/index.ts
+++ b/packages/models/src/videos/index.ts
@@ -14,6 +14,7 @@ export * from './transcoding/index.js'
export * from './channel-sync/index.js'
export * from './chapter/index.js'
+export * from './nsfw-flag.enum.js'
export * from './nsfw-policy.type.js'
export * from './storyboard.model.js'
@@ -21,6 +22,7 @@ export * from './thumbnail.type.js'
export * from './video-constant.model.js'
export * from './video-create.model.js'
+export * from './video-create-update-common.model.js'
export * from './video-privacy.enum.js'
export * from './video-include.enum.js'
diff --git a/packages/models/src/videos/nsfw-flag.enum.ts b/packages/models/src/videos/nsfw-flag.enum.ts
new file mode 100644
index 000000000..23add25c2
--- /dev/null
+++ b/packages/models/src/videos/nsfw-flag.enum.ts
@@ -0,0 +1,49 @@
+export const NSFWFlag = {
+ NONE: 0 << 0,
+ VIOLENT: 1 << 0,
+ SHOCKING_DISTURBING: 1 << 1,
+ EXPLICIT_SEX: 1 << 2
+} as const
+
+export type NSFWFlagType = typeof NSFWFlag[keyof typeof NSFWFlag]
+
+export type NSFWFlagString =
+ | 'violent'
+ | 'shockingOrDisturbing'
+ | 'explicitSex'
+
+const nsfwFlagsToStringMap: {
+ [key in NSFWFlagString]: NSFWFlagType
+} = {
+ violent: NSFWFlag.VIOLENT,
+ shockingOrDisturbing: NSFWFlag.SHOCKING_DISTURBING,
+ explicitSex: NSFWFlag.EXPLICIT_SEX
+} as const
+
+const nsfwFlagsStringToEnumMap: {
+ [key in NSFWFlagType]: NSFWFlagString
+} = {
+ [NSFWFlag.VIOLENT]: 'violent',
+ [NSFWFlag.SHOCKING_DISTURBING]: 'shockingOrDisturbing',
+ [NSFWFlag.EXPLICIT_SEX]: 'explicitSex'
+} as const
+
+export function nsfwFlagToString (nsfwFlag: NSFWFlagType): NSFWFlagString {
+ return nsfwFlagsStringToEnumMap[nsfwFlag]
+}
+
+export function nsfwFlagsToString (nsfwFlags: number): NSFWFlagString[] {
+ const acc: NSFWFlagString[] = []
+
+ for (const [ flagString, flag ] of Object.entries(nsfwFlagsToStringMap)) {
+ if ((nsfwFlags & flag) === flag) {
+ acc.push(flagString as NSFWFlagString)
+ }
+ }
+
+ return acc
+}
+
+export function stringToNSFWFlag (nsfwFlag: NSFWFlagString): NSFWFlagType {
+ return nsfwFlagsToStringMap[nsfwFlag]
+}
diff --git a/packages/models/src/videos/nsfw-policy.type.ts b/packages/models/src/videos/nsfw-policy.type.ts
index dc0032a14..a0948dc74 100644
--- a/packages/models/src/videos/nsfw-policy.type.ts
+++ b/packages/models/src/videos/nsfw-policy.type.ts
@@ -1 +1 @@
-export type NSFWPolicyType = 'do_not_list' | 'blur' | 'display'
+export type NSFWPolicyType = 'do_not_list' | 'warn' | 'blur' | 'display'
diff --git a/packages/models/src/videos/video-create-update-common.model.ts b/packages/models/src/videos/video-create-update-common.model.ts
new file mode 100644
index 000000000..f0b5692d0
--- /dev/null
+++ b/packages/models/src/videos/video-create-update-common.model.ts
@@ -0,0 +1,32 @@
+import { VideoCommentPolicyType } from './comment/video-comment-policy.enum.js'
+import { VideoPrivacyType } from './video-privacy.enum.js'
+import { VideoScheduleUpdate } from './video-schedule-update.model.js'
+
+export interface VideoCreateUpdateCommon {
+ name?: string
+ category?: number
+ licence?: number
+ language?: string
+ description?: string
+ support?: string
+ privacy?: VideoPrivacyType
+ tags?: string[]
+
+ // TODO: remove, deprecated in 6.2
+ commentsEnabled?: boolean
+ commentsPolicy?: VideoCommentPolicyType
+
+ downloadEnabled?: boolean
+
+ nsfw?: boolean
+ nsfwSummary?: string
+ nsfwFlags?: number
+
+ waitTranscoding?: boolean
+ channelId?: number
+ thumbnailfile?: Blob
+ previewfile?: Blob
+ scheduleUpdate?: VideoScheduleUpdate
+ originallyPublishedAt?: Date | string
+ videoPasswords?: string[]
+}
diff --git a/packages/models/src/videos/video-create.model.ts b/packages/models/src/videos/video-create.model.ts
index 8179a4705..b4223f6c5 100644
--- a/packages/models/src/videos/video-create.model.ts
+++ b/packages/models/src/videos/video-create.model.ts
@@ -1,32 +1,10 @@
-import { VideoCommentPolicyType } from './comment/video-comment-policy.enum.js'
+import { VideoCreateUpdateCommon } from './video-create-update-common.model.js'
import { VideoPrivacyType } from './video-privacy.enum.js'
-import { VideoScheduleUpdate } from './video-schedule-update.model.js'
-export interface VideoCreate {
+export interface VideoCreate extends VideoCreateUpdateCommon {
name: string
channelId: number
-
- category?: number
- licence?: number
- language?: string
- description?: string
- support?: string
- nsfw?: boolean
- waitTranscoding?: boolean
- tags?: string[]
-
- // TODO: remove, deprecated in 6.2
- commentsEnabled?: boolean
- commentsPolicy?: VideoCommentPolicyType
-
- downloadEnabled?: boolean
privacy: VideoPrivacyType
- scheduleUpdate?: VideoScheduleUpdate
- originallyPublishedAt?: Date | string
- videoPasswords?: string[]
-
- thumbnailfile?: Blob
- previewfile?: Blob
// Default is true if the feature is enabled by the instance admin
generateTranscription?: boolean
diff --git a/packages/models/src/videos/video-update.model.ts b/packages/models/src/videos/video-update.model.ts
index 5e7580a44..8dec37e30 100644
--- a/packages/models/src/videos/video-update.model.ts
+++ b/packages/models/src/videos/video-update.model.ts
@@ -1,30 +1,5 @@
-import { VideoCommentPolicyType } from './index.js'
-import { VideoPrivacyType } from './video-privacy.enum.js'
-import { VideoScheduleUpdate } from './video-schedule-update.model.js'
-
-export interface VideoUpdate {
- name?: string
- category?: number
- licence?: number
- language?: string
- description?: string
- support?: string
- privacy?: VideoPrivacyType
- tags?: string[]
-
- // TODO: remove, deprecated in 6.2
- commentsEnabled?: boolean
- commentsPolicy?: VideoCommentPolicyType
-
- downloadEnabled?: boolean
- nsfw?: boolean
- waitTranscoding?: boolean
- channelId?: number
- thumbnailfile?: Blob
- previewfile?: Blob
- scheduleUpdate?: VideoScheduleUpdate
- originallyPublishedAt?: Date | string
- videoPasswords?: string[]
+import { VideoCreateUpdateCommon } from './video-create-update-common.model.js'
+export interface VideoUpdate extends VideoCreateUpdateCommon {
pluginData?: any
}
diff --git a/packages/models/src/videos/video.model.ts b/packages/models/src/videos/video.model.ts
index b6262fb32..daf7bdc8b 100644
--- a/packages/models/src/videos/video.model.ts
+++ b/packages/models/src/videos/video.model.ts
@@ -54,6 +54,8 @@ export interface Video extends Partial {
comments: number
nsfw: boolean
+ nsfwFlags: number
+ nsfwSummary: string
account: AccountSummary
channel: VideoChannelSummary
diff --git a/packages/server-commands/src/search/search-command.ts b/packages/server-commands/src/search/search-command.ts
index e766a2861..48e374467 100644
--- a/packages/server-commands/src/search/search-command.ts
+++ b/packages/server-commands/src/search/search-command.ts
@@ -11,10 +11,11 @@ import {
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export class SearchCommand extends AbstractCommand {
-
- searchChannels (options: OverrideCommandOptions & {
- search: string
- }) {
+ searchChannels (
+ options: OverrideCommandOptions & {
+ search: string
+ }
+ ) {
return this.advancedChannelSearch({
...options,
@@ -22,9 +23,11 @@ export class SearchCommand extends AbstractCommand {
})
}
- advancedChannelSearch (options: OverrideCommandOptions & {
- search: VideoChannelsSearchQuery
- }) {
+ advancedChannelSearch (
+ options: OverrideCommandOptions & {
+ search: VideoChannelsSearchQuery
+ }
+ ) {
const { search } = options
const path = '/api/v1/search/video-channels'
@@ -38,9 +41,11 @@ export class SearchCommand extends AbstractCommand {
})
}
- searchPlaylists (options: OverrideCommandOptions & {
- search: string
- }) {
+ searchPlaylists (
+ options: OverrideCommandOptions & {
+ search: string
+ }
+ ) {
return this.advancedPlaylistSearch({
...options,
@@ -48,9 +53,11 @@ export class SearchCommand extends AbstractCommand {
})
}
- advancedPlaylistSearch (options: OverrideCommandOptions & {
- search: VideoPlaylistsSearchQuery
- }) {
+ advancedPlaylistSearch (
+ options: OverrideCommandOptions & {
+ search: VideoPlaylistsSearchQuery
+ }
+ ) {
const { search } = options
const path = '/api/v1/search/video-playlists'
@@ -64,10 +71,12 @@ export class SearchCommand extends AbstractCommand {
})
}
- searchVideos (options: OverrideCommandOptions & {
- search: string
- sort?: string
- }) {
+ searchVideos (
+ options: OverrideCommandOptions & {
+ search?: string
+ sort?: string
+ }
+ ) {
const { search, sort } = options
return this.advancedVideoSearch({
@@ -80,9 +89,11 @@ export class SearchCommand extends AbstractCommand {
})
}
- advancedVideoSearch (options: OverrideCommandOptions & {
- search: VideosSearchQuery
- }) {
+ advancedVideoSearch (
+ options: OverrideCommandOptions & {
+ search?: VideosSearchQuery
+ }
+ ) {
const { search } = options
const path = '/api/v1/search/videos'
diff --git a/packages/server-commands/src/videos/live-command.ts b/packages/server-commands/src/videos/live-command.ts
index f6a7a0e7a..c71e79e2c 100644
--- a/packages/server-commands/src/videos/live-command.ts
+++ b/packages/server-commands/src/videos/live-command.ts
@@ -106,12 +106,23 @@ export class LiveCommand extends AbstractCommand {
async create (
options: OverrideCommandOptions & {
- fields: Omit & { thumbnailfile?: string | Blob, previewfile?: string | Blob }
+ fields: Omit & {
+ thumbnailfile?: string | Blob
+ previewfile?: string | Blob
+ channelId?: number
+ }
}
) {
const { fields } = options
const path = '/api/v1/videos/live'
+ let defaultChannelId = 1
+
+ try {
+ const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
+ defaultChannelId = videoChannels[0].id
+ } catch (e) { /* empty */ }
+
const attaches: any = {}
if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
if (fields.previewfile) attaches.previewfile = fields.previewfile
@@ -121,7 +132,11 @@ export class LiveCommand extends AbstractCommand {
path,
attaches,
- fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]),
+ fields: {
+ channelId: defaultChannelId,
+
+ ...omit(fields, [ 'thumbnailfile', 'previewfile' ])
+ },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
}))
diff --git a/packages/server-commands/src/videos/videos-command.ts b/packages/server-commands/src/videos/videos-command.ts
index e3b33a2ac..8a0970729 100644
--- a/packages/server-commands/src/videos/videos-command.ts
+++ b/packages/server-commands/src/videos/videos-command.ts
@@ -649,6 +649,8 @@ export class VideosCommand extends AbstractCommand {
'count',
'sort',
'nsfw',
+ 'nsfwFlagsExcluded',
+ 'nsfwFlagsIncluded',
'isLive',
'categoryOneOf',
'licenceOneOf',
diff --git a/packages/tests/src/api/check-params/live.ts b/packages/tests/src/api/check-params/live.ts
index 6793fb6e8..ff75fe68c 100644
--- a/packages/tests/src/api/check-params/live.ts
+++ b/packages/tests/src/api/check-params/live.ts
@@ -5,6 +5,7 @@ import {
HttpStatusCode,
LiveVideoCreate,
LiveVideoLatencyMode,
+ NSFWFlag,
VideoCommentPolicy,
VideoCreateResult,
VideoPrivacy
@@ -109,6 +110,32 @@ describe('Test video lives API validator', function () {
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
+ it('Should fail with a bad NSFW', async function () {
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwFlags: NSFWFlag.EXPLICIT_SEX }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwSummary: 'toto' }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwFlags: 'toto' as any }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwSummary: 't' }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+ })
+
it('Should fail with a bad category', async function () {
const fields = { ...baseCorrectParams, category: 125 }
@@ -346,7 +373,6 @@ describe('Test video lives API validator', function () {
})
describe('When getting live information', function () {
-
it('Should fail with a bad access token', async function () {
await command.get({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
})
@@ -395,7 +421,6 @@ describe('Test video lives API validator', function () {
})
describe('When getting live sessions', function () {
-
it('Should fail with a bad access token', async function () {
await command.listSessions({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
})
@@ -426,7 +451,6 @@ describe('Test video lives API validator', function () {
})
describe('When getting live session of a replay', function () {
-
it('Should fail with a bad video id', async function () {
await command.getReplaySession({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
@@ -441,7 +465,6 @@ describe('Test video lives API validator', function () {
})
describe('When updating live information', async function () {
-
it('Should fail without access token', async function () {
await command.update({ token: '', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
})
@@ -510,7 +533,6 @@ describe('Test video lives API validator', function () {
await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } })
await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
-
})
it('Should fail to update replay status if replay is not allowed on the instance', async function () {
diff --git a/packages/tests/src/api/check-params/my-user.ts b/packages/tests/src/api/check-params/my-user.ts
index 4872ba804..84e12fad9 100644
--- a/packages/tests/src/api/check-params/my-user.ts
+++ b/packages/tests/src/api/check-params/my-user.ts
@@ -1,9 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
-import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
-import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import { HttpStatusCode, UserRole, VideoCreateResult } from '@peertube/peertube-models'
+import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import {
cleanupTests,
createSingleServer,
@@ -14,6 +12,8 @@ import {
setAccessTokensToServers,
UsersCommand
} from '@peertube/peertube-server-commands'
+import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
+import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
describe('Test my user API validators', function () {
const path = '/api/v1/users/'
@@ -153,6 +153,36 @@ describe('Test my user API validators', function () {
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
})
+ it('Should fail with an invalid NSFW flags attribute', async function () {
+ for (const key of [ 'nsfwFlagsDisplayed', 'nsfwFlagsHidden', 'nsfwFlagsBlurred', 'nsfwFlagsWarned' ]) {
+ const fields = {
+ [key]: 'hello'
+ }
+
+ await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
+ }
+ })
+
+ it('Should fail with a conflicted NSFW flags attributes', async function () {
+ {
+ const fields = {
+ nsfwFlagsDisplayed: 1,
+ nsfwFlagsWarned: 1
+ }
+
+ await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
+ }
+
+ {
+ const fields = {
+ nsfwFlagsHidden: 1,
+ nsfwFlagsBlurred: 1
+ }
+
+ await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
+ }
+ })
+
it('Should fail with an invalid autoPlayVideo attribute', async function () {
const fields = {
autoPlayVideo: -1
@@ -253,7 +283,11 @@ describe('Test my user API validators', function () {
const fields = {
currentPassword: 'password',
password: 'my super password',
- nsfwPolicy: 'blur',
+ nsfwPolicy: 'warn',
+ nsfwFlagsDisplayed: 0,
+ nsfwFlagsHidden: 1,
+ nsfwFlagsWarned: 2,
+ nsfwFlagsBlurred: 0,
autoPlayVideo: false,
email: 'super_email@example.com',
theme: 'default',
@@ -273,7 +307,7 @@ describe('Test my user API validators', function () {
it('Should succeed without password change with the correct params', async function () {
const fields = {
- nsfwPolicy: 'blur',
+ nsfwPolicy: 'warn',
autoPlayVideo: false
}
diff --git a/packages/tests/src/api/check-params/video-imports.ts b/packages/tests/src/api/check-params/video-imports.ts
index 354d2c196..4866bdb75 100644
--- a/packages/tests/src/api/check-params/video-imports.ts
+++ b/packages/tests/src/api/check-params/video-imports.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { omit } from '@peertube/peertube-core-utils'
-import { HttpStatusCode, VideoCommentPolicy, VideoImportCreate, VideoPrivacy } from '@peertube/peertube-models'
+import { HttpStatusCode, NSFWFlag, VideoCommentPolicy, VideoImportCreate, VideoPrivacy } from '@peertube/peertube-models'
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import {
PeerTubeServer,
@@ -158,6 +158,32 @@ describe('Test video imports API validator', function () {
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
+ it('Should fail with a bad NSFW', async function () {
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwFlags: NSFWFlag.EXPLICIT_SEX }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwSummary: 'toto' }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwFlags: 'toto' as any }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwSummary: 't' }
+
+ await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
+ }
+ })
+
it('Should fail with a bad category', async function () {
const fields = { ...baseCorrectParams, category: 125 }
@@ -302,6 +328,20 @@ describe('Test video imports API validator', function () {
fields: baseCorrectParams,
expectedStatus: HttpStatusCode.OK_200
})
+
+ await makePostBodyRequest({
+ url: server.url,
+ path,
+ token: server.accessToken,
+ fields: {
+ ...baseCorrectParams,
+
+ nsfw: true,
+ nsfwFlags: NSFWFlag.EXPLICIT_SEX,
+ nsfwSummary: 'toto'
+ },
+ expectedStatus: HttpStatusCode.OK_200
+ })
})
it('Should forbid to import http videos', async function () {
diff --git a/packages/tests/src/api/check-params/videos-common-filters.ts b/packages/tests/src/api/check-params/videos-common-filters.ts
index 3920d9b72..0ca9fee84 100644
--- a/packages/tests/src/api/check-params/videos-common-filters.ts
+++ b/packages/tests/src/api/check-params/videos-common-filters.ts
@@ -1,14 +1,8 @@
+/* eslint-disable @typescript-eslint/indent */
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import {
- HttpStatusCode,
- HttpStatusCodeType,
- UserRole,
- VideoInclude,
- VideoIncludeType,
- VideoPrivacy,
- VideoPrivacyType
-} from '@peertube/peertube-models'
+import { pick } from '@peertube/peertube-core-utils'
+import { HttpStatusCode, HttpStatusCodeType, UserRole, VideoInclude, VideoPrivacy, VideosCommonQuery } from '@peertube/peertube-models'
import {
cleanupTests,
createSingleServer,
@@ -23,6 +17,13 @@ describe('Test video filters validators', function () {
let userAccessToken: string
let moderatorAccessToken: string
+ const validIncludes = [
+ VideoInclude.NONE,
+ VideoInclude.BLOCKED_OWNER,
+ VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED,
+ VideoInclude.SOURCE
+ ]
+
// ---------------------------------------------------------------
before(async function () {
@@ -43,62 +44,80 @@ describe('Test video filters validators', function () {
moderatorAccessToken = await server.login.getAccessToken(moderator)
})
- describe('When setting video filters', function () {
- const validIncludes = [
- VideoInclude.NONE,
- VideoInclude.BLOCKED_OWNER,
- VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED,
- VideoInclude.SOURCE
+ async function testEndpoints (
+ options:
+ & Pick<
+ VideosCommonQuery,
+ | 'isLocal'
+ | 'include'
+ | 'privacyOneOf'
+ | 'autoTagOneOf'
+ | 'excludeAlreadyWatched'
+ | 'nsfw'
+ | 'nsfwFlagsExcluded'
+ | 'nsfwFlagsIncluded'
+ >
+ & {
+ token?: string
+ expectedStatus: HttpStatusCodeType
+ unauthenticatedUser?: boolean
+ filter?: string
+ }
+ ) {
+ const paths = [
+ '/api/v1/video-channels/root_channel/videos',
+ '/api/v1/accounts/root/videos',
+ '/api/v1/videos',
+ '/api/v1/search/videos'
]
- async function testEndpoints (options: {
- token?: string
- isLocal?: boolean
- include?: VideoIncludeType
- privacyOneOf?: VideoPrivacyType[]
- autoTagOneOf?: string[]
- expectedStatus: HttpStatusCodeType
- excludeAlreadyWatched?: boolean
- unauthenticatedUser?: boolean
- filter?: string
- }) {
- const paths = [
- '/api/v1/video-channels/root_channel/videos',
- '/api/v1/accounts/root/videos',
- '/api/v1/videos',
- '/api/v1/search/videos'
- ]
-
- if (options.unauthenticatedUser !== true) {
- paths.push('/api/v1/users/me/videos')
- }
-
- for (const path of paths) {
- const token = options.unauthenticatedUser
- ? undefined
- : options.token || server.accessToken
-
- await makeGetRequest({
- url: server.url,
- path,
- token,
- query: {
- isLocal: options.isLocal,
- privacyOneOf: options.privacyOneOf,
- autoTagOneOf: options.autoTagOneOf,
- include: options.include,
- excludeAlreadyWatched: options.excludeAlreadyWatched,
- filter: options.filter
- },
- expectedStatus: options.expectedStatus
- })
- }
+ if (options.unauthenticatedUser !== true) {
+ paths.push('/api/v1/users/me/videos')
}
+ for (const path of paths) {
+ const token = options.unauthenticatedUser
+ ? undefined
+ : options.token || server.accessToken
+
+ await makeGetRequest({
+ url: server.url,
+ path,
+ token,
+ query: pick(options, [
+ 'isLocal',
+ 'privacyOneOf',
+ 'autoTagOneOf',
+ 'include',
+ 'excludeAlreadyWatched',
+ 'filter',
+ 'nsfw',
+ 'nsfwFlagsExcluded',
+ 'nsfwFlagsIncluded'
+ ]),
+ expectedStatus: options.expectedStatus
+ })
+ }
+ }
+
+ describe('Local filter', function () {
it('Should fail with the old filter query param', async function () {
await testEndpoints({ filter: 'all-local', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
+ it('Should succeed on the feeds endpoint with the local filter', async function () {
+ await makeGetRequest({
+ url: server.url,
+ path: '/feeds/videos.json',
+ expectedStatus: HttpStatusCode.OK_200,
+ query: {
+ isLocal: true
+ }
+ })
+ })
+ })
+
+ describe('Privacy', function () {
it('Should fail with a bad privacyOneOf', async function () {
await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
@@ -114,7 +133,9 @@ describe('Test video filters validators', function () {
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
})
})
+ })
+ describe('Auto tag', function () {
it('Should fail to use autoTagOneOf with a simple user', async function () {
await testEndpoints({
autoTagOneOf: [ 'test' ],
@@ -130,7 +151,9 @@ describe('Test video filters validators', function () {
expectedStatus: HttpStatusCode.OK_200
})
})
+ })
+ describe('Include', function () {
it('Should fail with a bad include', async function () {
await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
@@ -172,18 +195,9 @@ describe('Test video filters validators', function () {
})
}
})
+ })
- it('Should succeed on the feeds endpoint with the local filter', async function () {
- await makeGetRequest({
- url: server.url,
- path: '/feeds/videos.json',
- expectedStatus: HttpStatusCode.OK_200,
- query: {
- isLocal: true
- }
- })
- })
-
+ describe('Exclude already watched', function () {
it('Should fail when trying to exclude already watched videos for an unlogged user', async function () {
await testEndpoints({ excludeAlreadyWatched: true, unauthenticatedUser: true, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
@@ -193,6 +207,37 @@ describe('Test video filters validators', function () {
})
})
+ describe('NSFW', function () {
+ it('Should fail with an invalid nsfw', async function () {
+ await testEndpoints({ nsfw: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ })
+
+ it('Should fail with an invalid nsfwFlagsExcluded', async function () {
+ await testEndpoints({ nsfwFlagsExcluded: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ })
+
+ it('Should fail with an invalid nsfwFlagsIncluded', async function () {
+ await testEndpoints({ nsfwFlagsIncluded: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+ })
+
+ it('Should fail with conflicted nsfwFlagsIncluded and nsfwFlagsExcluded', async function () {
+ await testEndpoints({
+ nsfwFlagsIncluded: 1,
+ nsfwFlagsExcluded: 1,
+ expectedStatus: HttpStatusCode.BAD_REQUEST_400
+ })
+ })
+
+ it('Should succeed with the correct NSFW params', async function () {
+ await testEndpoints({
+ nsfw: 'true',
+ nsfwFlagsIncluded: 2,
+ nsfwFlagsExcluded: 1,
+ expectedStatus: HttpStatusCode.OK_200
+ })
+ })
+ })
+
after(async function () {
await cleanupTests([ server ])
})
diff --git a/packages/tests/src/api/check-params/videos.ts b/packages/tests/src/api/check-params/videos.ts
index ce86caa73..61120c4e2 100644
--- a/packages/tests/src/api/check-params/videos.ts
+++ b/packages/tests/src/api/check-params/videos.ts
@@ -1,7 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { omit, randomInt } from '@peertube/peertube-core-utils'
-import { HttpStatusCode, PeerTubeProblemDocument, VideoCommentPolicy, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
+import {
+ HttpStatusCode,
+ NSFWFlag,
+ PeerTubeProblemDocument,
+ VideoCommentPolicy,
+ VideoCreateResult,
+ VideoPrivacy
+} from '@peertube/peertube-models'
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import {
PeerTubeServer,
@@ -268,6 +275,36 @@ describe('Test videos API validator', function () {
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
})
+ it('Should fail with a bad NSFW', async function () {
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwFlags: NSFWFlag.EXPLICIT_SEX }
+ const attaches = baseCorrectAttaches
+
+ await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwSummary: 'toto' }
+ const attaches = baseCorrectAttaches
+
+ await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwFlags: 'toto' as any }
+ const attaches = baseCorrectAttaches
+
+ await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwSummary: 't' }
+ const attaches = baseCorrectAttaches
+
+ await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
+ }
+ })
+
it('Should fail with a bad category', async function () {
const fields = { ...baseCorrectParams, category: 125 }
const attaches = baseCorrectAttaches
@@ -521,7 +558,7 @@ describe('Test videos API validator', function () {
await checkUploadVideoParam({
...baseOptions(),
- attributes: { ...fields, ...attaches },
+ attributes: { ...fields, ...attaches, nsfw: true, nsfwFlags: NSFWFlag.EXPLICIT_SEX, nsfwSummary: 'toto' },
expectedStatus: HttpStatusCode.OK_200
})
}
@@ -584,6 +621,32 @@ describe('Test videos API validator', function () {
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
})
+ it('Should fail with a bad NSFW', async function () {
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwFlags: NSFWFlag.EXPLICIT_SEX }
+
+ await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: false, nsfwSummary: 'toto' }
+
+ await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwFlags: 'toto' as any }
+
+ await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
+ }
+
+ {
+ const fields = { ...baseCorrectParams, nsfw: true, nsfwSummary: 't' }
+
+ await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
+ }
+ })
+
it('Should fail with a bad category', async function () {
const fields = { ...baseCorrectParams, category: 125 }
@@ -761,6 +824,20 @@ describe('Test videos API validator', function () {
fields,
expectedStatus: HttpStatusCode.NO_CONTENT_204
})
+
+ await makePutBodyRequest({
+ url: server.url,
+ path: path + video.shortUUID,
+ token: server.accessToken,
+ fields: {
+ ...fields,
+
+ nsfw: true,
+ nsfwFlags: NSFWFlag.EXPLICIT_SEX,
+ nsfwSummary: 'toto'
+ },
+ expectedStatus: HttpStatusCode.NO_CONTENT_204
+ })
})
})
diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts
index 1ac0675c0..6e880d519 100644
--- a/packages/tests/src/api/server/config.ts
+++ b/packages/tests/src/api/server/config.ts
@@ -168,7 +168,7 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
categories: [ 1, 2 ],
isNSFW: true,
- defaultNSFWPolicy: 'blur' as 'blur',
+ defaultNSFWPolicy: 'warn' as 'warn',
serverCountry: 'France',
support: {
@@ -450,7 +450,6 @@ describe('Test config', function () {
})
describe('Config keys', function () {
-
it('Should have the correct default config', async function () {
const data = await server.config.getConfig()
@@ -612,7 +611,6 @@ describe('Test config', function () {
})
describe('Image files', function () {
-
async function checkAndGetServerImages () {
const { instance } = await server.config.getAbout()
const htmlConfig = await server.config.getConfig()
diff --git a/packages/tests/src/api/videos/video-nsfw.ts b/packages/tests/src/api/videos/video-nsfw.ts
index 162d6e2bf..ac6b67ea5 100644
--- a/packages/tests/src/api/videos/video-nsfw.ts
+++ b/packages/tests/src/api/videos/video-nsfw.ts
@@ -1,58 +1,63 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+import {
+ CustomConfig,
+ NSFWFlag,
+ NSFWPolicyType,
+ ResultList,
+ Video,
+ VideoPrivacy,
+ VideosCommonQuery,
+ VideosOverview
+} from '@peertube/peertube-models'
+import {
+ cleanupTests,
+ createMultipleServers,
+ doubleFollow,
+ PeerTubeServer,
+ sendRTMPStream,
+ setAccessTokensToServers,
+ stopFfmpeg,
+ waitJobs
+} from '@peertube/peertube-server-commands'
+import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { expect } from 'chai'
-import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
-import { BooleanBothQuery, CustomConfig, ResultList, Video, VideosOverview } from '@peertube/peertube-models'
function createOverviewRes (overview: VideosOverview) {
const videos = overview.categories[0].videos
+
return { data: videos, total: videos.length }
}
describe('Test video NSFW policy', function () {
- let server: PeerTubeServer
+ let servers: PeerTubeServer[]
let userAccessToken: string
let customConfig: CustomConfig
- async function getVideosFunctions (token?: string, query: { nsfw?: BooleanBothQuery } = {}) {
- const user = await server.users.getMyInfo()
+ async function getVideosFunctions (
+ token?: string,
+ query: Partial> = {}
+ ) {
+ const user = await servers[0].users.getMyInfo()
const channelName = user.videoChannels[0].name
const accountName = user.account.name + '@' + user.account.host
const hasQuery = Object.keys(query).length !== 0
- let promises: Promise>[]
- if (token) {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- promises = [
- server.search.advancedVideoSearch({ token, search: { search: 'n', sort: '-publishedAt', ...query } }),
- server.videos.listWithToken({ token, ...query }),
- server.videos.listByAccount({ token, handle: accountName, ...query }),
- server.videos.listByChannel({ token, handle: channelName, ...query })
- ]
+ const promises = [
+ token
+ ? servers[0].videos.listWithToken({ token, ...query })
+ : servers[0].videos.list(query),
- // Overviews do not support video filters
- if (!hasQuery) {
- const p = server.overviews.getVideos({ page: 1, token })
- .then(res => createOverviewRes(res))
- promises.push(p)
- }
-
- return Promise.all(promises)
- }
-
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- promises = [
- server.search.searchVideos({ search: 'n', sort: '-publishedAt' }),
- server.videos.list(),
- server.videos.listByAccount({ token: null, handle: accountName }),
- server.videos.listByChannel({ token: null, handle: channelName })
+ servers[0].search.advancedVideoSearch({ token: token || null, search: { sort: '-publishedAt', ...query } }),
+ servers[0].videos.listByAccount({ token: token || null, handle: accountName, ...query }),
+ servers[0].videos.listByChannel({ token: token || null, handle: channelName, ...query })
]
// Overviews do not support video filters
if (!hasQuery) {
- const p = server.overviews.getVideos({ page: 1 })
+ const p = servers[0].overviews.getVideos({ page: 1, token })
.then(res => createOverviewRes(res))
promises.push(p)
@@ -61,47 +66,236 @@ describe('Test video NSFW policy', function () {
return Promise.all(promises)
}
+ async function checkHasAll (token?: string) {
+ for (const body of await getVideosFunctions(token)) {
+ expect(body.total).to.equal(5)
+
+ const videos = body.data
+ expect(videos).to.have.lengthOf(5)
+
+ expect(videos.map(v => v.name)).to.have.members([ 'not nsfw', 'nsfw simple', 'nsfw sex', 'import violent', 'live violent' ])
+ }
+ }
+
before(async function () {
this.timeout(50000)
- server = await createSingleServer(1)
+
+ servers = await createMultipleServers(2)
// Get the access tokens
- await setAccessTokensToServers([ server ])
+ await setAccessTokensToServers(servers)
- {
- const attributes = { name: 'nsfw', nsfw: true, category: 1 }
- await server.videos.upload({ attributes })
- }
+ customConfig = await servers[0].config.getCustomConfig()
- {
- const attributes = { name: 'normal', nsfw: false, category: 1 }
- await server.videos.upload({ attributes })
- }
+ await doubleFollow(servers[0], servers[1])
+ })
- customConfig = await server.config.getCustomConfig()
+ describe('NSFW federation', function () {
+ let videoUUID: string
+
+ it('Should upload a video without NSFW', async function () {
+ // Add category to have results in overview
+ const { uuid } = await servers[0].videos.upload({ attributes: { name: 'not nsfw', nsfw: false, category: 1 } })
+
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: uuid })
+
+ expect(video.nsfw).to.be.false
+ expect(video.nsfwFlags).to.equal(0)
+ expect(video.nsfwSummary).to.be.null
+ }
+ })
+
+ it('Should upload a video with NSFW but without NSFW flags and summary', async function () {
+ const { uuid } = await servers[0].videos.upload({ attributes: { name: 'nsfw simple', nsfw: true, category: 1 } })
+
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: uuid })
+
+ expect(video.nsfw).to.be.true
+ expect(video.nsfwFlags).to.equal(0)
+ expect(video.nsfwSummary).to.be.null
+ }
+ })
+
+ it('Should upload a video with NSFW and flags and summary', async function () {
+ const { uuid } = await servers[0].videos.upload({
+ attributes: {
+ name: 'nsfw sex',
+ nsfw: true,
+ nsfwFlags: NSFWFlag.SHOCKING_DISTURBING | NSFWFlag.EXPLICIT_SEX,
+ nsfwSummary: 'This is a shocking and disturbing video',
+ category: 1
+ }
+ })
+ videoUUID = uuid
+
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: uuid })
+
+ expect(video.nsfw).to.be.true
+ expect(video.nsfwFlags).to.equal(6)
+ expect(video.nsfwSummary).to.equal('This is a shocking and disturbing video')
+ }
+ })
+
+ it('Should update a NSFW tags of a video', async function () {
+ {
+ await servers[0].videos.update({ id: videoUUID, attributes: { nsfw: false } })
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: videoUUID })
+
+ expect(video.nsfw).to.be.false
+ expect(video.nsfwFlags).to.equal(0)
+ expect(video.nsfwSummary).to.be.null
+ }
+ }
+
+ {
+ await servers[0].videos.update({ id: videoUUID, attributes: { nsfw: true, nsfwFlags: NSFWFlag.VIOLENT } })
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: videoUUID })
+
+ expect(video.nsfw).to.be.true
+ expect(video.nsfwFlags).to.equal(NSFWFlag.VIOLENT)
+ expect(video.nsfwSummary).to.be.null
+ }
+ }
+
+ {
+ await servers[0].videos.update({ id: videoUUID, attributes: { nsfw: true, nsfwFlags: NSFWFlag.EXPLICIT_SEX, nsfwSummary: 'test' } })
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: videoUUID })
+
+ expect(video.nsfw).to.be.true
+ expect(video.nsfwFlags).to.equal(NSFWFlag.EXPLICIT_SEX)
+ expect(video.nsfwSummary).to.equal('test')
+ }
+ }
+ })
+
+ it('Should import a video with NSFW', async function () {
+ const { video: { uuid } } = await servers[0].videoImports.importVideo({
+ attributes: {
+ targetUrl: FIXTURE_URLS.goodVideo,
+ name: 'import violent',
+ nsfw: true,
+ nsfwFlags: NSFWFlag.VIOLENT,
+ nsfwSummary: 'This is a violent video',
+ privacy: VideoPrivacy.PUBLIC,
+ category: 1
+ }
+ })
+
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: uuid })
+
+ expect(video.nsfw).to.be.true
+ expect(video.nsfwFlags).to.equal(1)
+ expect(video.nsfwSummary).to.equal('This is a violent video')
+ }
+ })
+
+ it('Should create a live with a replay with NSFW', async function () {
+ await servers[0].config.save()
+ await servers[0].config.enableMinimumTranscoding()
+ await servers[0].config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' })
+
+ const checkVideo = (video: Video) => {
+ expect(video.nsfw).to.be.true
+ expect(video.nsfwFlags).to.equal(1)
+ expect(video.nsfwSummary).to.equal('This is a violent live')
+ }
+
+ const { uuid } = await servers[0].live.create({
+ fields: {
+ name: 'live violent',
+ saveReplay: true,
+ permanentLive: false,
+ privacy: VideoPrivacy.PUBLIC,
+ nsfw: true,
+ nsfwFlags: NSFWFlag.VIOLENT,
+ nsfwSummary: 'This is a violent live',
+ category: 1
+ }
+ })
+ const live = await servers[0].live.get({ videoId: uuid })
+
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: uuid })
+
+ checkVideo(video)
+ }
+
+ const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
+ await servers[0].live.waitUntilPublished({ videoId: uuid })
+
+ await stopFfmpeg(ffmpegCommand)
+
+ await servers[0].live.waitUntilReplacedByReplay({ videoId: uuid })
+ await waitJobs(servers)
+
+ for (const server of servers) {
+ const video = await server.videos.get({ id: uuid })
+
+ checkVideo(video)
+ }
+
+ await servers[0].config.rollback()
+ })
})
describe('Instance default NSFW policy', function () {
-
it('Should display NSFW videos with display default NSFW policy', async function () {
- const serverConfig = await server.config.getConfig()
+ const serverConfig = await servers[0].config.getConfig()
expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display')
- for (const body of await getVideosFunctions()) {
+ await checkHasAll()
+ })
+
+ it('Should hide some content with nsfwFlagsExcluded', async function () {
+ for (const body of await getVideosFunctions(undefined, { nsfwFlagsExcluded: NSFWFlag.VIOLENT })) {
+ expect(body.total).to.equal(3)
+
+ const videos = body.data
+ expect(videos).to.have.lengthOf(3)
+
+ expect(videos.map(v => v.name)).to.not.have.members([ 'live violent', 'import violent' ])
+ }
+
+ for (
+ const body of await getVideosFunctions(undefined, { nsfwFlagsExcluded: NSFWFlag.VIOLENT | NSFWFlag.EXPLICIT_SEX })
+ ) {
expect(body.total).to.equal(2)
const videos = body.data
expect(videos).to.have.lengthOf(2)
- expect(videos[0].name).to.equal('normal')
- expect(videos[1].name).to.equal('nsfw')
+
+ expect(videos.map(v => v.name)).to.not.have.members([ 'live violent', 'import violent', 'nsfw sex' ])
}
})
it('Should not display NSFW videos with do_not_list default NSFW policy', async function () {
customConfig.instance.defaultNSFWPolicy = 'do_not_list'
- await server.config.updateCustomConfig({ newCustomConfig: customConfig })
+ await servers[0].config.updateCustomConfig({ newCustomConfig: customConfig })
- const serverConfig = await server.config.getConfig()
+ const serverConfig = await servers[0].config.getConfig()
expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list')
for (const body of await getVideosFunctions()) {
@@ -109,122 +303,255 @@ describe('Test video NSFW policy', function () {
const videos = body.data
expect(videos).to.have.lengthOf(1)
- expect(videos[0].name).to.equal('normal')
+ expect(videos[0].name).to.equal('not nsfw')
}
})
- it('Should display NSFW videos with blur default NSFW policy', async function () {
- customConfig.instance.defaultNSFWPolicy = 'blur'
- await server.config.updateCustomConfig({ newCustomConfig: customConfig })
-
- const serverConfig = await server.config.getConfig()
- expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur')
-
- for (const body of await getVideosFunctions()) {
- expect(body.total).to.equal(2)
+ it('Should display NSFW videos with nsfwFlagsIncluded', async function () {
+ for (const body of await getVideosFunctions(undefined, { nsfwFlagsIncluded: NSFWFlag.VIOLENT })) {
+ expect(body.total).to.equal(3)
const videos = body.data
- expect(videos).to.have.lengthOf(2)
- expect(videos[0].name).to.equal('normal')
- expect(videos[1].name).to.equal('nsfw')
+ expect(videos).to.have.lengthOf(3)
+
+ expect(videos.map(v => v.name)).to.have.members([ 'not nsfw', 'live violent', 'import violent' ])
+ }
+
+ for (
+ const body of await getVideosFunctions(undefined, { nsfwFlagsIncluded: NSFWFlag.VIOLENT | NSFWFlag.EXPLICIT_SEX })
+ ) {
+ expect(body.total).to.equal(4)
+
+ const videos = body.data
+ expect(videos).to.have.lengthOf(4)
+
+ expect(videos.map(v => v.name)).to.have.members([ 'not nsfw', 'live violent', 'import violent', 'nsfw sex' ])
+ }
+ })
+
+ it('Should display NSFW videos with warn/warn_and_blur default NSFW policy', async function () {
+ for (const policy of [ 'warn', 'blur' ] as NSFWPolicyType[]) {
+ customConfig.instance.defaultNSFWPolicy = policy
+ await servers[0].config.updateCustomConfig({ newCustomConfig: customConfig })
+
+ const serverConfig = await servers[0].config.getConfig()
+ expect(serverConfig.instance.defaultNSFWPolicy).to.equal(policy)
+
+ await checkHasAll()
+ }
+ })
+
+ it('Should hide some content with nsfwFlagsExcluded', async function () {
+ for (const body of await getVideosFunctions(undefined, { nsfwFlagsExcluded: NSFWFlag.VIOLENT })) {
+ expect(body.total).to.equal(3)
+
+ const videos = body.data
+ expect(videos).to.have.lengthOf(3)
+
+ expect(videos.map(v => v.name)).to.not.have.members([ 'live violent', 'import violent' ])
}
})
})
describe('User NSFW policy', function () {
+ async function checkNSFWFlag (options: {
+ token: string
+ check: (results: ResultList[]) => Promise
+ nsfwFlagsHidden?: number
+ nsfwFlagsWarned?: number
+ nsfwFlagsBlurred?: number
+ nsfwFlagsDisplayed?: number
+ }) {
+ const { token, check, nsfwFlagsHidden, nsfwFlagsWarned, nsfwFlagsBlurred, nsfwFlagsDisplayed } = options
+
+ await check(
+ await getVideosFunctions(token, {
+ nsfwFlagsExcluded: nsfwFlagsHidden,
+ nsfwFlagsIncluded: (nsfwFlagsDisplayed || NSFWFlag.NONE) | (nsfwFlagsWarned || NSFWFlag.NONE) |
+ (nsfwFlagsBlurred || NSFWFlag.NONE)
+ })
+ )
+
+ await servers[0].users.updateMe({ token, nsfwFlagsHidden, nsfwFlagsWarned, nsfwFlagsBlurred, nsfwFlagsDisplayed })
+
+ const me = await servers[0].users.getMyInfo({ token })
+ expect(me.nsfwFlagsHidden).to.equal(nsfwFlagsHidden || NSFWFlag.NONE)
+ expect(me.nsfwFlagsWarned).to.equal(nsfwFlagsWarned || NSFWFlag.NONE)
+ expect(me.nsfwFlagsBlurred).to.equal(nsfwFlagsBlurred || NSFWFlag.NONE)
+ expect(me.nsfwFlagsDisplayed).to.equal(nsfwFlagsDisplayed || NSFWFlag.NONE)
+
+ await check(await getVideosFunctions(token))
+
+ await servers[0].users.updateMe({
+ token,
+ nsfwFlagsHidden: NSFWFlag.NONE,
+ nsfwFlagsWarned: NSFWFlag.NONE,
+ nsfwFlagsBlurred: NSFWFlag.NONE,
+ nsfwFlagsDisplayed: NSFWFlag.NONE
+ })
+ }
it('Should create a user having the default nsfw policy', async function () {
+ await servers[0].config.updateExistingConfig({ newConfig: { instance: { defaultNSFWPolicy: 'warn' } } })
+
const username = 'user1'
const password = 'my super password'
- await server.users.create({ username, password })
+ await servers[0].users.create({ username, password })
- userAccessToken = await server.login.getAccessToken({ username, password })
+ userAccessToken = await servers[0].login.getAccessToken({ username, password })
- const user = await server.users.getMyInfo({ token: userAccessToken })
- expect(user.nsfwPolicy).to.equal('blur')
+ const user = await servers[0].users.getMyInfo({ token: userAccessToken })
+ expect(user.nsfwPolicy).to.equal('warn')
})
- it('Should display NSFW videos with blur user NSFW policy', async function () {
+ it('Should display NSFW videos with warn user NSFW policy', async function () {
customConfig.instance.defaultNSFWPolicy = 'do_not_list'
- await server.config.updateCustomConfig({ newCustomConfig: customConfig })
+ await servers[0].config.updateCustomConfig({ newCustomConfig: customConfig })
- for (const body of await getVideosFunctions(userAccessToken)) {
- expect(body.total).to.equal(2)
+ await checkHasAll(userAccessToken)
+ })
- const videos = body.data
- expect(videos).to.have.lengthOf(2)
- expect(videos[0].name).to.equal('normal')
- expect(videos[1].name).to.equal('nsfw')
+ it('Should exclude some videos using NSFW flags', async function () {
+ const check = async (results: ResultList[]) => {
+ for (const body of results) {
+ expect(body.total).to.equal(3)
+
+ const videos = body.data
+ expect(videos).to.have.lengthOf(3)
+
+ expect(videos.map(v => v.name)).to.not.have.members([ 'live violent', 'import violent' ])
+ }
}
+
+ await checkNSFWFlag({
+ token: userAccessToken,
+ check,
+ nsfwFlagsHidden: NSFWFlag.VIOLENT
+ })
})
it('Should display NSFW videos with display user NSFW policy', async function () {
- await server.users.updateMe({ nsfwPolicy: 'display' })
+ await servers[0].users.updateMe({ nsfwPolicy: 'display' })
- for (const body of await getVideosFunctions(server.accessToken)) {
- expect(body.total).to.equal(2)
+ await checkHasAll(servers[0].accessToken)
+ })
- const videos = body.data
- expect(videos).to.have.lengthOf(2)
- expect(videos[0].name).to.equal('normal')
- expect(videos[1].name).to.equal('nsfw')
+ it('Should exclude some videos using NSFW flags', async function () {
+ const check = async (results: ResultList[]) => {
+ for (const body of results) {
+ expect(body.total).to.equal(2)
+
+ const videos = body.data
+ expect(videos).to.have.lengthOf(2)
+
+ expect(videos.map(v => v.name)).to.not.have.members([ 'live violent', 'import violent', 'nsfw sex' ])
+ }
}
+
+ await checkNSFWFlag({
+ token: userAccessToken,
+ check,
+ nsfwFlagsHidden: NSFWFlag.VIOLENT | NSFWFlag.EXPLICIT_SEX
+ })
})
it('Should not display NSFW videos with do_not_list user NSFW policy', async function () {
- await server.users.updateMe({ nsfwPolicy: 'do_not_list' })
+ await servers[0].users.updateMe({ nsfwPolicy: 'do_not_list' })
- for (const body of await getVideosFunctions(server.accessToken)) {
+ for (const body of await getVideosFunctions(servers[0].accessToken)) {
expect(body.total).to.equal(1)
const videos = body.data
expect(videos).to.have.lengthOf(1)
- expect(videos[0].name).to.equal('normal')
+
+ expect(videos.map(v => v.name)).to.have.members([ 'not nsfw' ])
}
})
- it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () {
- const { total, data } = await server.videos.listMyVideos()
- expect(total).to.equal(2)
+ it('Should include some videos using NSFW flags', async function () {
+ const check = async (results: ResultList[]) => {
+ for (const body of results) {
+ expect(body.total).to.equal(2)
- expect(data).to.have.lengthOf(2)
- expect(data[0].name).to.equal('normal')
- expect(data[1].name).to.equal('nsfw')
+ const videos = body.data
+ expect(videos).to.have.lengthOf(2)
+
+ expect(videos.map(v => v.name)).to.have.members([ 'not nsfw', 'nsfw sex' ])
+ }
+ }
+
+ await checkNSFWFlag({
+ token: servers[0].accessToken,
+ check,
+ nsfwFlagsDisplayed: NSFWFlag.EXPLICIT_SEX
+ })
+ })
+
+ it('Should include and warn some videos using NSFW flags', async function () {
+ const check = async (results: ResultList[]) => {
+ for (const body of results) {
+ expect(body.total).to.equal(4)
+
+ const videos = body.data
+ expect(videos).to.have.lengthOf(4)
+
+ expect(videos.map(v => v.name)).to.have.members([ 'not nsfw', 'live violent', 'import violent', 'nsfw sex' ])
+ }
+ }
+
+ await checkNSFWFlag({
+ token: servers[0].accessToken,
+ check,
+ nsfwFlagsDisplayed: NSFWFlag.EXPLICIT_SEX,
+ nsfwFlagsWarned: NSFWFlag.VIOLENT
+ })
+
+ await checkNSFWFlag({
+ token: servers[0].accessToken,
+ check,
+ nsfwFlagsBlurred: NSFWFlag.EXPLICIT_SEX,
+ nsfwFlagsWarned: NSFWFlag.VIOLENT
+ })
+ })
+
+ it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () {
+ const { total, data } = await servers[0].videos.listMyVideos()
+ expect(total).to.equal(5)
+
+ expect(data).to.have.lengthOf(5)
+ expect(data.map(v => v.name)).to.have.members([ 'not nsfw', 'nsfw simple', 'nsfw sex', 'import violent', 'live violent' ])
})
it('Should display NSFW videos when the nsfw param === true', async function () {
- for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'true' })) {
- expect(body.total).to.equal(1)
+ for (const { total, data } of await getVideosFunctions(servers[0].accessToken, { nsfw: 'true' })) {
+ expect(total).to.equal(4)
- const videos = body.data
- expect(videos).to.have.lengthOf(1)
- expect(videos[0].name).to.equal('nsfw')
+ expect(data).to.have.lengthOf(4)
+ expect(data.map(v => v.name)).to.have.members([ 'nsfw simple', 'nsfw sex', 'import violent', 'live violent' ])
}
})
it('Should hide NSFW videos when the nsfw param === true', async function () {
- for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'false' })) {
+ for (const body of await getVideosFunctions(servers[0].accessToken, { nsfw: 'false' })) {
expect(body.total).to.equal(1)
const videos = body.data
expect(videos).to.have.lengthOf(1)
- expect(videos[0].name).to.equal('normal')
+ expect(videos[0].name).to.equal('not nsfw')
}
})
it('Should display both videos when the nsfw param === both', async function () {
- for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'both' })) {
- expect(body.total).to.equal(2)
+ for (const { total, data } of await getVideosFunctions(servers[0].accessToken, { nsfw: 'both' })) {
+ expect(total).to.equal(5)
- const videos = body.data
- expect(videos).to.have.lengthOf(2)
- expect(videos[0].name).to.equal('normal')
- expect(videos[1].name).to.equal('nsfw')
+ expect(data).to.have.lengthOf(5)
+ expect(data.map(v => v.name)).to.have.members([ 'not nsfw', 'nsfw simple', 'nsfw sex', 'import violent', 'live violent' ])
}
})
})
after(async function () {
- await cleanupTests([ server ])
+ await cleanupTests(servers)
})
})
diff --git a/packages/tests/src/client/index-html.ts b/packages/tests/src/client/index-html.ts
index eab82809c..136684440 100644
--- a/packages/tests/src/client/index-html.ts
+++ b/packages/tests/src/client/index-html.ts
@@ -26,9 +26,9 @@ describe('Test index HTML generation', function () {
}
before(async function () {
- this.timeout(120000);
+ this.timeout(120000)
- ({
+ ;({
servers,
playlistIds,
videoIds,
@@ -44,7 +44,6 @@ describe('Test index HTML generation', function () {
})
describe('Instance tags', function () {
-
it('Should have valid index html tags (title, description...)', async function () {
const config = await servers[0].config.getConfig()
const res = await makeHTMLRequest(servers[0].url, '/videos/browse')
@@ -60,7 +59,7 @@ describe('Test index HTML generation', function () {
shortDescription: 'my short description',
description: 'my super description',
terms: 'my super terms',
- defaultNSFWPolicy: 'blur',
+ defaultNSFWPolicy: 'warn',
defaultClientRoute: '/videos/recently-added',
customizations: {
javascript: 'alert("coucou")',
@@ -85,7 +84,6 @@ describe('Test index HTML generation', function () {
})
describe('Canonical tags', function () {
-
it('Should use the original video URL for the canonical tag', async function () {
for (const basePath of getWatchVideoBasePaths()) {
for (const id of videoIds) {
@@ -126,7 +124,6 @@ describe('Test index HTML generation', function () {
})
describe('Indexation tags', function () {
-
it('Should not index remote videos', async function () {
for (const basePath of getWatchVideoBasePaths()) {
for (const id of videoIds) {
@@ -223,7 +220,6 @@ describe('Test index HTML generation', function () {
})
describe('Check no leaks for private objects', function () {
-
it('Should not display internal/private/password protected video', async function () {
for (const basePath of getWatchVideoBasePaths()) {
for (const id of [ privateVideoId, internalVideoId, passwordProtectedVideoId ]) {
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts
index cc5b513a7..9065cd362 100755
--- a/scripts/i18n/create-custom-files.ts
+++ b/scripts/i18n/create-custom-files.ts
@@ -83,7 +83,14 @@ const playerKeys = {
'Enable {1} subtitle': 'Enable {1} subtitle',
'{1} (auto-generated)': '{1} (auto-generated)',
'Go back': 'Go back',
- 'Audio only': 'Audio only'
+ 'Audio only': 'Audio only',
+ 'Sensitive content': 'Sensitive content',
+ 'This video contains sensitive content.': 'This video contains sensitive content.',
+ 'Learn more': 'Learn more',
+ 'Content warning': 'Content warning',
+ 'Violence': 'Violence',
+ 'Shocking Content': 'Shocking Content',
+ 'Explicit Sex': 'Explicit Sex'
}
Object.assign(playerKeys, videojs)
@@ -114,7 +121,9 @@ Object.values(VIDEO_CATEGORIES)
'By {1}',
'Unavailable video'
])
- .forEach(v => { serverKeys[v] = v })
+ .forEach(v => {
+ serverKeys[v] = v
+ })
// More keys
Object.assign(serverKeys, {
@@ -124,7 +133,9 @@ Object.assign(serverKeys, {
// ISO 639 keys
const languageKeys: any = {}
const languages = buildLanguages()
-Object.keys(languages).forEach(k => { languageKeys[languages[k]] = languages[k] })
+Object.keys(languages).forEach(k => {
+ languageKeys[languages[k]] = languages[k]
+})
Object.assign(serverKeys, languageKeys)
diff --git a/server/core/controllers/api/accounts.ts b/server/core/controllers/api/accounts.ts
index 2fd072293..b935feac6 100644
--- a/server/core/controllers/api/accounts.ts
+++ b/server/core/controllers/api/accounts.ts
@@ -3,7 +3,7 @@ import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
import { getServerActor } from '@server/models/application/application.js'
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
import express from 'express'
-import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
+import { buildNSFWFilters, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
import { getFormattedObjects } from '../../helpers/utils.js'
import { JobQueue } from '../../lib/job-queue/index.js'
import { Hooks } from '../../lib/plugins/hooks.js'
@@ -224,9 +224,9 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
const apiOptions = await Hooks.wrapObject({
...query,
+ ...buildNSFWFilters({ req, res }),
displayOnlyForFollower,
- nsfw: buildNSFWFilter(res, query.nsfw),
accountId: account.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
diff --git a/server/core/controllers/api/overviews.ts b/server/core/controllers/api/overviews.ts
index f5ff54a9f..d0079c3a9 100644
--- a/server/core/controllers/api/overviews.ts
+++ b/server/core/controllers/api/overviews.ts
@@ -1,11 +1,11 @@
-import express from 'express'
-import memoizee from 'memoizee'
+import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { getServerActor } from '@server/models/application/application.js'
import { VideoModel } from '@server/models/video/video.js'
-import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '@peertube/peertube-models'
-import { buildNSFWFilter } from '../../helpers/express-utils.js'
+import express from 'express'
+import memoizee from 'memoizee'
+import { buildNSFWFilters } from '../../helpers/express-utils.js'
import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants.js'
import { apiRateLimiter, asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares/index.js'
import { TagModel } from '../../models/video/tag.js'
@@ -14,11 +14,7 @@ const overviewsRouter = express.Router()
overviewsRouter.use(apiRateLimiter)
-overviewsRouter.get('/videos',
- videosOverviewValidator,
- optionalAuthenticate,
- asyncMiddleware(getVideosOverview)
-)
+overviewsRouter.get('/videos', videosOverviewValidator, optionalAuthenticate, asyncMiddleware(getVideosOverview))
// ---------------------------------------------------------------------------
@@ -115,6 +111,8 @@ async function getVideos (
const serverActor = await getServerActor()
const query = await Hooks.wrapObject({
+ ...buildNSFWFilters({ res }),
+
start: 0,
count: 12,
sort: '-createdAt',
@@ -122,7 +120,6 @@ async function getVideos (
actorId: serverActor.id,
orLocalVideos: true
},
- nsfw: buildNSFWFilter(res),
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos: false,
diff --git a/server/core/controllers/api/search/search-videos.ts b/server/core/controllers/api/search/search-videos.ts
index 2b3d5c4da..dc826ac53 100644
--- a/server/core/controllers/api/search/search-videos.ts
+++ b/server/core/controllers/api/search/search-videos.ts
@@ -1,4 +1,4 @@
-import express from 'express'
+import { HttpStatusCode, ResultList, Video, VideosSearchQueryAfterSanitize } from '@peertube/peertube-models'
import { sanitizeUrl } from '@server/helpers/core-utils.js'
import { pickSearchVideoQuery } from '@server/helpers/query.js'
import { doJSONRequest } from '@server/helpers/requests.js'
@@ -9,8 +9,8 @@ import { getOrCreateAPVideo } from '@server/lib/activitypub/videos/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search.js'
import { getServerActor } from '@server/models/application/application.js'
-import { HttpStatusCode, ResultList, Video, VideosSearchQueryAfterSanitize } from '@peertube/peertube-models'
-import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
+import express from 'express'
+import { buildNSFWFilters, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
@@ -31,7 +31,8 @@ import { searchLocalUrl } from './shared/index.js'
const searchVideosRouter = express.Router()
-searchVideosRouter.get('/videos',
+searchVideosRouter.get(
+ '/videos',
openapiOperationDoc({ operationId: 'searchVideos' }),
paginationValidator,
setDefaultPagination,
@@ -104,21 +105,25 @@ async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: ex
async function searchVideosDB (query: VideosSearchQueryAfterSanitize, req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
- const apiOptions = await Hooks.wrapObject({
- ...query,
+ const apiOptions = await Hooks.wrapObject(
+ {
+ ...query,
+ ...buildNSFWFilters({ req, res }),
- displayOnlyForFollower: {
- actorId: serverActor.id,
- orLocalVideos: true
+ displayOnlyForFollower: {
+ actorId: serverActor.id,
+ orLocalVideos: true
+ },
+
+ countVideos: getCountVideos(req),
+
+ user: res.locals.oauth
+ ? res.locals.oauth.token.User
+ : undefined
},
-
- countVideos: getCountVideos(req),
-
- nsfw: buildNSFWFilter(res, query.nsfw),
- user: res.locals.oauth
- ? res.locals.oauth.token.User
- : undefined
- }, 'filter:api.search.videos.local.list.params', { req, res })
+ 'filter:api.search.videos.local.list.params',
+ { req, res }
+ )
const resultList = await Hooks.wrapPromiseFun(
VideoModel.searchAndPopulateAccountAndServer.bind(VideoModel),
diff --git a/server/core/controllers/api/users/me.ts b/server/core/controllers/api/users/me.ts
index 5f85915bd..fcf7e038b 100644
--- a/server/core/controllers/api/users/me.ts
+++ b/server/core/controllers/api/users/me.ts
@@ -95,13 +95,33 @@ meRouter.get(
asyncMiddleware(listUserVideos)
)
-meRouter.get('/me/videos/:videoId/rating', authenticate, asyncMiddleware(usersVideoRatingValidator), asyncMiddleware(getUserVideoRating))
+meRouter.get(
+ '/me/videos/:videoId/rating',
+ authenticate,
+ asyncMiddleware(usersVideoRatingValidator),
+ asyncMiddleware(getUserVideoRating)
+)
-meRouter.put('/me', authenticate, asyncMiddleware(usersUpdateMeValidator), asyncRetryTransactionMiddleware(updateMe))
+meRouter.put(
+ '/me',
+ authenticate,
+ asyncMiddleware(usersUpdateMeValidator),
+ asyncRetryTransactionMiddleware(updateMe)
+)
-meRouter.post('/me/avatar/pick', authenticate, reqAvatarFile, updateAvatarValidator, asyncRetryTransactionMiddleware(updateMyAvatar))
+meRouter.post(
+ '/me/avatar/pick',
+ authenticate,
+ reqAvatarFile,
+ updateAvatarValidator,
+ asyncRetryTransactionMiddleware(updateMyAvatar)
+)
-meRouter.delete('/me/avatar', authenticate, asyncRetryTransactionMiddleware(deleteMyAvatar))
+meRouter.delete(
+ '/me/avatar',
+ authenticate,
+ asyncRetryTransactionMiddleware(deleteMyAvatar)
+)
// ---------------------------------------------------------------------------
@@ -248,6 +268,10 @@ async function updateMe (req: express.Request, res: express.Response) {
const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly)[] = [
'password',
'nsfwPolicy',
+ 'nsfwFlagsDisplayed',
+ 'nsfwFlagsHidden',
+ 'nsfwFlagsWarned',
+ 'nsfwFlagsBlurred',
'p2pEnabled',
'autoPlayVideo',
'autoPlayNextVideo',
diff --git a/server/core/controllers/api/users/my-subscriptions.ts b/server/core/controllers/api/users/my-subscriptions.ts
index 75690dff2..cea065871 100644
--- a/server/core/controllers/api/users/my-subscriptions.ts
+++ b/server/core/controllers/api/users/my-subscriptions.ts
@@ -1,12 +1,12 @@
-import 'multer'
-import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { handlesToNameAndHost } from '@server/helpers/actors.js'
import { pickCommonVideoQuery } from '@server/helpers/query.js'
import { sendUndoFollow } from '@server/lib/activitypub/send/index.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { VideoChannelModel } from '@server/models/video/video-channel.js'
-import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils.js'
+import express from 'express'
+import 'multer'
+import { buildNSFWFilters, getCountVideos } from '../../../helpers/express-utils.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { sequelizeTypescript } from '../../../initializers/database.js'
import { JobQueue } from '../../../lib/job-queue/index.js'
@@ -34,7 +34,8 @@ import { VideoModel } from '../../../models/video/video.js'
const mySubscriptionsRouter = express.Router()
-mySubscriptionsRouter.get('/me/subscriptions/videos',
+mySubscriptionsRouter.get(
+ '/me/subscriptions/videos',
authenticate,
paginationValidator,
videosSortValidator,
@@ -44,13 +45,10 @@ mySubscriptionsRouter.get('/me/subscriptions/videos',
asyncMiddleware(getUserSubscriptionVideos)
)
-mySubscriptionsRouter.get('/me/subscriptions/exist',
- authenticate,
- areSubscriptionsExistValidator,
- asyncMiddleware(areSubscriptionsExist)
-)
+mySubscriptionsRouter.get('/me/subscriptions/exist', authenticate, areSubscriptionsExistValidator, asyncMiddleware(areSubscriptionsExist))
-mySubscriptionsRouter.get('/me/subscriptions',
+mySubscriptionsRouter.get(
+ '/me/subscriptions',
authenticate,
paginationValidator,
userSubscriptionsSortValidator,
@@ -60,19 +58,12 @@ mySubscriptionsRouter.get('/me/subscriptions',
asyncMiddleware(listUserSubscriptions)
)
-mySubscriptionsRouter.post('/me/subscriptions',
- authenticate,
- userSubscriptionAddValidator,
- addUserSubscription
-)
+mySubscriptionsRouter.post('/me/subscriptions', authenticate, userSubscriptionAddValidator, addUserSubscription)
-mySubscriptionsRouter.get('/me/subscriptions/:uri',
- authenticate,
- userSubscriptionGetValidator,
- asyncMiddleware(getUserSubscription)
-)
+mySubscriptionsRouter.get('/me/subscriptions/:uri', authenticate, userSubscriptionGetValidator, asyncMiddleware(getUserSubscription))
-mySubscriptionsRouter.delete('/me/subscriptions/:uri',
+mySubscriptionsRouter.delete(
+ '/me/subscriptions/:uri',
authenticate,
userSubscriptionGetValidator,
asyncRetryTransactionMiddleware(deleteUserSubscription)
@@ -94,7 +85,7 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
const results = await ActorFollowModel.listSubscriptionsOf(user.Account.Actor.id, sanitizedHandles)
- const existObject: { [id: string ]: boolean } = {}
+ const existObject: { [id: string]: boolean } = {}
for (const sanitizedHandle of sanitizedHandles) {
const obj = results.find(r => {
const server = r.ActorFollowing.Server
@@ -147,8 +138,8 @@ async function deleteUserSubscription (req: express.Request, res: express.Respon
})
return res.type('json')
- .status(HttpStatusCode.NO_CONTENT_204)
- .end()
+ .status(HttpStatusCode.NO_CONTENT_204)
+ .end()
}
async function listUserSubscriptions (req: express.Request, res: express.Response) {
@@ -173,12 +164,12 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
const apiOptions = await Hooks.wrapObject({
...query,
+ ...buildNSFWFilters({ req, res }),
displayOnlyForFollower: {
actorId: user.Account.Actor.id,
orLocalVideos: false
},
- nsfw: buildNSFWFilter(res, query.nsfw),
user,
countVideos
}, 'filter:api.user.me.subscription-videos.list.params')
diff --git a/server/core/controllers/api/video-channel.ts b/server/core/controllers/api/video-channel.ts
index ed0d94292..1365c8a83 100644
--- a/server/core/controllers/api/video-channel.ts
+++ b/server/core/controllers/api/video-channel.ts
@@ -13,7 +13,7 @@ import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
import express from 'express'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger.js'
import { resetSequelizeInstance } from '../../helpers/database-utils.js'
-import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
+import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
import { logger } from '../../helpers/logger.js'
import { getFormattedObjects } from '../../helpers/utils.js'
import { MIMETYPES } from '../../initializers/constants.js'
@@ -395,9 +395,9 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
const apiOptions = await Hooks.wrapObject({
...query,
+ ...buildNSFWFilters({ req, res }),
displayOnlyForFollower,
- nsfw: buildNSFWFilter(res, query.nsfw),
videoChannelId: videoChannelInstance.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
diff --git a/server/core/controllers/api/videos/index.ts b/server/core/controllers/api/videos/index.ts
index c69056df6..5326e421e 100644
--- a/server/core/controllers/api/videos/index.ts
+++ b/server/core/controllers/api/videos/index.ts
@@ -4,7 +4,7 @@ import { openapiOperationDoc } from '@server/middlewares/doc.js'
import { getServerActor } from '@server/models/application/application.js'
import express from 'express'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
-import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils.js'
+import { buildNSFWFilters, getCountVideos } from '../../../helpers/express-utils.js'
import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants.js'
@@ -73,24 +73,13 @@ videosRouter.use('/', storyboardRouter)
videosRouter.use('/', videoSourceRouter)
videosRouter.use('/', videoChaptersRouter)
-videosRouter.get('/categories',
- openapiOperationDoc({ operationId: 'getCategories' }),
- listVideoCategories
-)
-videosRouter.get('/licences',
- openapiOperationDoc({ operationId: 'getLicences' }),
- listVideoLicences
-)
-videosRouter.get('/languages',
- openapiOperationDoc({ operationId: 'getLanguages' }),
- listVideoLanguages
-)
-videosRouter.get('/privacies',
- openapiOperationDoc({ operationId: 'getPrivacies' }),
- listVideoPrivacies
-)
+videosRouter.get('/categories', openapiOperationDoc({ operationId: 'getCategories' }), listVideoCategories)
+videosRouter.get('/licences', openapiOperationDoc({ operationId: 'getLicences' }), listVideoLicences)
+videosRouter.get('/languages', openapiOperationDoc({ operationId: 'getLanguages' }), listVideoLanguages)
+videosRouter.get('/privacies', openapiOperationDoc({ operationId: 'getPrivacies' }), listVideoPrivacies)
-videosRouter.get('/',
+videosRouter.get(
+ '/',
openapiOperationDoc({ operationId: 'getVideos' }),
paginationValidator,
videosSortValidator,
@@ -101,7 +90,8 @@ videosRouter.get('/',
asyncMiddleware(listVideos)
)
-videosRouter.get('/:id',
+videosRouter.get(
+ '/:id',
openapiOperationDoc({ operationId: 'getVideo' }),
optionalAuthenticate,
asyncMiddleware(videosCustomGetValidator('for-api')),
@@ -109,7 +99,8 @@ videosRouter.get('/:id',
asyncMiddleware(getVideo)
)
-videosRouter.delete('/:id',
+videosRouter.delete(
+ '/:id',
openapiOperationDoc({ operationId: 'delVideo' }),
authenticate,
asyncMiddleware(videosRemoveValidator),
@@ -163,12 +154,12 @@ async function listVideos (req: express.Request, res: express.Response) {
const apiOptions = await Hooks.wrapObject({
...query,
+ ...buildNSFWFilters({ req, res }),
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
},
- nsfw: buildNSFWFilter(res, query.nsfw),
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
}, 'filter:api.videos.list.params')
@@ -195,6 +186,6 @@ async function removeVideo (req: express.Request, res: express.Response) {
Hooks.runAction('action:api.video.deleted', { video: videoInstance, req, res })
return res.type('json')
- .status(HttpStatusCode.NO_CONTENT_204)
- .end()
+ .status(HttpStatusCode.NO_CONTENT_204)
+ .end()
}
diff --git a/server/core/controllers/api/videos/update.ts b/server/core/controllers/api/videos/update.ts
index 23c53f7ad..74165d0a6 100644
--- a/server/core/controllers/api/videos/update.ts
+++ b/server/core/controllers/api/videos/update.ts
@@ -1,5 +1,13 @@
import { forceNumber } from '@peertube/peertube-core-utils'
-import { HttpStatusCode, ThumbnailType, VideoCommentPolicy, VideoPrivacy, VideoPrivacyType, VideoUpdate } from '@peertube/peertube-models'
+import {
+ HttpStatusCode,
+ NSFWFlag,
+ ThumbnailType,
+ VideoCommentPolicy,
+ 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'
@@ -35,7 +43,8 @@ const updateRouter = express.Router()
const reqVideoFileUpdate = createReqFiles([ 'thumbnailfile', 'previewfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
-updateRouter.put('/:id',
+updateRouter.put(
+ '/:id',
openapiOperationDoc({ operationId: 'putVideo' }),
authenticate,
reqVideoFileUpdate,
@@ -54,7 +63,7 @@ export {
async function updateVideo (req: express.Request, res: express.Response) {
const videoFromReq = res.locals.videoAll
const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON())
- const videoInfoToUpdate: VideoUpdate = req.body
+ const body: VideoUpdate = req.body
const hadPrivacyForFederation = isPrivacyForFederation(videoFromReq.privacy)
const oldPrivacy = videoFromReq.privacy
@@ -77,6 +86,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
'licence',
'language',
'nsfw',
+ 'nsfwFlags',
+ 'nsfwSummary',
'waitTranscoding',
'support',
'description',
@@ -84,31 +95,36 @@ async function updateVideo (req: express.Request, res: express.Response) {
]
for (const key of keysToUpdate) {
- if (videoInfoToUpdate[key] !== undefined) video.set(key, videoInfoToUpdate[key])
+ if (body[key] !== undefined) video.set(key, body[key])
+ }
+
+ if (!video.nsfw) {
+ video.nsfwFlags = NSFWFlag.NONE
+ video.nsfwSummary = null
}
// Special treatment for comments policy to support deprecated commentsEnabled attribute
- if (videoInfoToUpdate.commentsPolicy !== undefined) {
- video.commentsPolicy = videoInfoToUpdate.commentsPolicy
- } else if (videoInfoToUpdate.commentsEnabled === true) {
+ if (body.commentsPolicy !== undefined) {
+ video.commentsPolicy = body.commentsPolicy
+ } else if (body.commentsEnabled === true) {
video.commentsPolicy = VideoCommentPolicy.ENABLED
- } else if (videoInfoToUpdate.commentsEnabled === false) {
+ } else if (body.commentsEnabled === false) {
video.commentsPolicy = VideoCommentPolicy.DISABLED
}
- if (videoInfoToUpdate.originallyPublishedAt !== undefined) {
- video.originallyPublishedAt = videoInfoToUpdate.originallyPublishedAt
- ? new Date(videoInfoToUpdate.originallyPublishedAt)
+ if (body.originallyPublishedAt !== undefined) {
+ video.originallyPublishedAt = body.originallyPublishedAt
+ ? new Date(body.originallyPublishedAt)
: null
}
// Privacy update?
let isNewVideoForFederation = false
- if (videoInfoToUpdate.privacy !== undefined) {
+ if (body.privacy !== undefined) {
isNewVideoForFederation = await updateVideoPrivacy({
videoInstance: video,
- videoInfoToUpdate,
+ videoInfoToUpdate: body,
hadPrivacyForFederation,
transaction: t
})
@@ -127,8 +143,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
}
// Video tags update?
- if (videoInfoToUpdate.tags !== undefined) {
- await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t })
+ if (body.tags !== undefined) {
+ await setVideoTags({ video: videoInstanceUpdated, tags: body.tags, transaction: t })
}
// Video channel update?
@@ -142,7 +158,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
}
// Schedule an update in the future?
- await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t)
+ await updateSchedule(videoInstanceUpdated, body, t)
if (oldDescription !== video.description) {
await replaceChaptersFromDescriptionIfNeeded({
@@ -181,7 +197,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
await addVideoJobsAfterUpdate({
video: videoInstanceUpdated,
- nameChanged: !!videoInfoToUpdate.name,
+ nameChanged: !!body.name,
oldPrivacy,
isNewVideoForFederation
})
@@ -196,8 +212,8 @@ async function updateVideo (req: express.Request, res: express.Response) {
}
return res.type('json')
- .status(HttpStatusCode.NO_CONTENT_204)
- .end()
+ .status(HttpStatusCode.NO_CONTENT_204)
+ .end()
}
// Return a boolean indicating if the video is considered as "new" for remote instances in the federation
diff --git a/server/core/controllers/feeds/video-feeds.ts b/server/core/controllers/feeds/video-feeds.ts
index ab1f346f7..2c6f485cf 100644
--- a/server/core/controllers/feeds/video-feeds.ts
+++ b/server/core/controllers/feeds/video-feeds.ts
@@ -6,7 +6,7 @@ import { cacheRouteFactory } from '@server/middlewares/index.js'
import { VideoModel } from '@server/models/video/video.js'
import express from 'express'
import { extname } from 'path'
-import { buildNSFWFilter } from '../../helpers/express-utils.js'
+import { buildNSFWFilters } from '../../helpers/express-utils.js'
import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants.js'
import {
asyncMiddleware,
@@ -95,8 +95,9 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
})
const data = await getVideosForFeeds({
+ ...buildNSFWFilters({ req, res }),
+
sort: req.query.sort,
- nsfw: buildNSFWFilter(res, req.query.nsfw),
isLocal: req.query.isLocal,
include: req.query.include | VideoInclude.FILES,
accountId: account?.id,
@@ -124,8 +125,9 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
})
const data = await getVideosForFeeds({
+ ...buildNSFWFilters({ req, res }),
+
sort: req.query.sort,
- nsfw: buildNSFWFilter(res, req.query.nsfw),
isLocal: req.query.isLocal,
include: req.query.include | VideoInclude.FILES,
displayOnlyForFollower: {
diff --git a/server/core/controllers/feeds/video-podcast-feeds.ts b/server/core/controllers/feeds/video-podcast-feeds.ts
index c972e1037..083f3c066 100644
--- a/server/core/controllers/feeds/video-podcast-feeds.ts
+++ b/server/core/controllers/feeds/video-podcast-feeds.ts
@@ -3,7 +3,7 @@ import { CustomTag, CustomXMLNS, LiveItemStatus } from '@peertube/feed/lib/typin
import { buildDownloadFilesUrl, getResolutionLabel, sortObjectComparator } from '@peertube/peertube-core-utils'
import { ActorImageType, VideoFile, VideoInclude, VideoResolution, VideoState } from '@peertube/peertube-models'
import { buildUUIDv5FromURL } from '@peertube/peertube-node-utils'
-import { buildNSFWFilter } from '@server/helpers/express-utils.js'
+import { buildNSFWFilters } from '@server/helpers/express-utils.js'
import { CONFIG } from '@server/initializers/config.js'
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
@@ -64,13 +64,12 @@ async function generateVideoPodcastFeed (req: express.Request, res: express.Resp
const { name, description, imageUrl, ownerImageUrl, email, link, ownerLink } = await buildFeedMetadata({ videoChannel })
- const nsfw = buildNSFWFilter()
+ const nsfwOptions = buildNSFWFilters()
const data = await getVideosForFeeds({
- sort: '-publishedAt',
+ ...nsfwOptions,
- // Only list non-NSFW videos (for Apple)
- nsfw,
+ sort: '-publishedAt',
// Prevent podcast feeds from listing videos in other instances
// helps prevent duplicates when they are indexed -- only the author should control them
@@ -81,7 +80,7 @@ async function generateVideoPodcastFeed (req: express.Request, res: express.Resp
const language = await VideoModel.guessLanguageOrCategoryOfChannel(videoChannel.id, 'language')
const category = await VideoModel.guessLanguageOrCategoryOfChannel(videoChannel.id, 'category')
- const hasNSFW = nsfw !== false
+ const hasNSFW = nsfwOptions.nsfw !== false
? await VideoModel.channelHasNSFWContent(videoChannel.id)
: false
diff --git a/server/core/controllers/sitemap.ts b/server/core/controllers/sitemap.ts
index dddc991e1..1e8eaffb0 100644
--- a/server/core/controllers/sitemap.ts
+++ b/server/core/controllers/sitemap.ts
@@ -1,23 +1,19 @@
+import { VideoFileStream, VideoInclude } from '@peertube/peertube-models'
+import { logger } from '@server/helpers/logger.js'
+import { getServerActor } from '@server/models/application/application.js'
import express from 'express'
import truncate from 'lodash-es/truncate.js'
import { ErrorLevel, SitemapStream, streamToPromise } from 'sitemap'
-import { logger } from '@server/helpers/logger.js'
-import { getServerActor } from '@server/models/application/application.js'
-import { buildNSFWFilter } from '../helpers/express-utils.js'
+import { buildNSFWFilters } from '../helpers/express-utils.js'
import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants.js'
import { apiRateLimiter, asyncMiddleware, cacheRoute } from '../middlewares/index.js'
import { AccountModel } from '../models/account/account.js'
-import { VideoModel } from '../models/video/video.js'
import { VideoChannelModel } from '../models/video/video-channel.js'
-import { VideoFileStream, VideoInclude } from '@peertube/peertube-models'
+import { VideoModel } from '../models/video/video.js'
const sitemapRouter = express.Router()
-sitemapRouter.use('/sitemap.xml',
- apiRateLimiter,
- cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP),
- asyncMiddleware(getSitemap)
-)
+sitemapRouter.use('/sitemap.xml', apiRateLimiter, cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP), asyncMiddleware(getSitemap))
// ---------------------------------------------------------------------------
@@ -81,6 +77,8 @@ async function getSitemapLocalVideoUrls () {
while (hasData && i < 1000) {
const { data } = await VideoModel.listForApi({
+ ...buildNSFWFilters(),
+
start: chunkSize * i,
count: chunkSize,
sort: 'createdAt',
@@ -89,7 +87,6 @@ async function getSitemapLocalVideoUrls () {
orLocalVideos: true
},
isLocal: true,
- nsfw: buildNSFWFilter(),
countVideos: false,
include: VideoInclude.FILES | VideoInclude.TAGS
})
diff --git a/server/core/helpers/activity-pub-utils.ts b/server/core/helpers/activity-pub-utils.ts
index d3c5667de..9c70be3a1 100644
--- a/server/core/helpers/activity-pub-utils.ts
+++ b/server/core/helpers/activity-pub-utils.ts
@@ -5,7 +5,7 @@ import { buildDigest } from './peertube-crypto.js'
import type { signJsonLDObject } from './peertube-jsonld.js'
import { doJSONRequest } from './requests.js'
-export type ContextFilter = (arg: T) => Promise
+export type ContextFilter = (arg: T) => Promise
export function buildGlobalHTTPHeaders (
body: any,
@@ -18,11 +18,11 @@ export function buildGlobalHTTPHeaders (
}
}
-export async function activityPubContextify (data: T, type: ContextType, contextFilter: ContextFilter) {
+export async function activityPubContextify (data: T, type: ContextType, contextFilter: ContextFilter) {
return { ...await getContextData(type, contextFilter), ...data }
}
-export async function signAndContextify (options: {
+export async function signAndContextify (options: {
byActor: { url: string, privateKey: string }
data: T
contextType: ContextType | null
@@ -65,9 +65,9 @@ export function hasAPPublic (toOrCC: string[]) {
// Private
// ---------------------------------------------------------------------------
-type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string }) }
+type ContextValue = { [id: string]: string | { '@type': string, '@id': string } }
-const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = {
+const contextStore: { [id in ContextType]: (string | { [id: string]: string })[] } = {
Video: buildContext({
Hashtag: 'as:Hashtag',
category: 'sc:category',
@@ -93,6 +93,7 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
},
Infohash: 'pt:Infohash',
+ SensitiveTag: 'pt:SensitiveTag',
tileWidth: {
'@type': 'sc:Number',
diff --git a/server/core/helpers/custom-validators/activitypub/videos.ts b/server/core/helpers/custom-validators/activitypub/videos.ts
index b95b8cd87..806808e44 100644
--- a/server/core/helpers/custom-validators/activitypub/videos.ts
+++ b/server/core/helpers/custom-validators/activitypub/videos.ts
@@ -155,7 +155,10 @@ export function isAPCaptionUrlObject (url: any): url is ActivityCaptionUrlObject
function setValidRemoteTags (video: VideoObject) {
if (Array.isArray(video.tag) === false) video.tag = []
- video.tag = video.tag.filter(t => t.type === 'Hashtag' && isVideoTagValid(t.name))
+ video.tag = video.tag.filter(t => {
+ return (t.type === 'Hashtag' && isVideoTagValid(t.name)) ||
+ (t.type === 'SensitiveTag' && !!t.name)
+ })
return true
}
diff --git a/server/core/helpers/custom-validators/videos.ts b/server/core/helpers/custom-validators/videos.ts
index b7959202f..1f0e586ab 100644
--- a/server/core/helpers/custom-validators/videos.ts
+++ b/server/core/helpers/custom-validators/videos.ts
@@ -98,8 +98,8 @@ export function isVideoFileMimeTypeValid (files: UploadFilesForCheck, field = 'v
}
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
- .map(v => v.replace('.', ''))
- .join('|')
+ .map(v => v.replace('.', ''))
+ .join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})`
export function isVideoImageValid (files: UploadFilesForCheck, field: string, optional = true) {
@@ -193,3 +193,11 @@ export function isValidPasswordProtectedPrivacy (req: Request, res: Response) {
return true
}
+
+export function isNSFWFlagsValid (value: number) {
+ return value === null || (exists(value) && validator.default.isInt('' + value))
+}
+
+export function isNSFWSummaryValid (value: any) {
+ return value === null || (exists(value) && validator.default.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.NSFW_SUMMARY))
+}
diff --git a/server/core/helpers/express-utils.ts b/server/core/helpers/express-utils.ts
index 773cad2b2..24ac9c4d6 100644
--- a/server/core/helpers/express-utils.ts
+++ b/server/core/helpers/express-utils.ts
@@ -1,6 +1,7 @@
+import { VideosCommonQuery } from '@peertube/peertube-models'
+import { getLowercaseExtension } from '@peertube/peertube-node-utils'
import express, { RequestHandler } from 'express'
import multer, { diskStorage } from 'multer'
-import { getLowercaseExtension } from '@peertube/peertube-node-utils'
import { CONFIG } from '../initializers/config.js'
import { REMOTE_SCHEME } from '../initializers/constants.js'
import { isArray } from './custom-validators/misc.js'
@@ -8,14 +9,33 @@ import { logger } from './logger.js'
import { deleteFileAndCatch, generateRandomString } from './utils.js'
import { getExtFromMimetype } from './video.js'
-function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
- if (paramNSFW === 'true') return true
- if (paramNSFW === 'false') return false
- if (paramNSFW === 'both') return undefined
+// ---------------------------------------------------------------------------
+// Extract NSFW Filters options to list videos
+// ---------------------------------------------------------------------------
- if (res?.locals.oauth) {
- const user = res.locals.oauth.token.User
+export function buildNSFWFilters (options: {
+ req?: express.Request
+ res?: express.Response
+} = {}) {
+ return {
+ nsfw: buildNSFWFilter(options),
+ nsfwFlagsIncluded: buildNSFWFlagsIncluded(options),
+ nsfwFlagsExcluded: buildNSFWFlagsExcluded(options)
+ }
+}
+function buildNSFWFilter (options: {
+ req?: express.Request
+ res?: express.Response
+}) {
+ const query = options.req?.query.nsfw as VideosCommonQuery['nsfw']
+ const user = options.res?.locals.oauth?.token.User
+
+ if (query === 'true') return true
+ if (query === 'false') return false
+ if (query === 'both') return undefined
+
+ if (user) {
// User does not want NSFW videos
if (user.nsfwPolicy === 'do_not_list') return false
@@ -29,7 +49,35 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
return null
}
-function cleanUpReqFiles (req: express.Request) {
+function buildNSFWFlagsIncluded (options: {
+ req?: express.Request
+ res?: express.Response
+}) {
+ const query = options.req?.query.nsfwFlagsIncluded as VideosCommonQuery['nsfwFlagsIncluded']
+ const user = options.res?.locals.oauth?.token.User
+
+ if (query) return query
+ if (user) return user.nsfwFlagsWarned | user.nsfwFlagsBlurred | user.nsfwFlagsDisplayed
+
+ return undefined
+}
+
+function buildNSFWFlagsExcluded (options: {
+ req?: express.Request
+ res?: express.Response
+}) {
+ const query = options.req?.query.nsfwFlagsExcluded as VideosCommonQuery['nsfwFlagsExcluded']
+ const user = options.res?.locals.oauth?.token.User
+
+ if (query) return query
+ if (user) return user.nsfwFlagsHidden
+
+ return undefined
+}
+
+// ---------------------------------------------------------------------------
+
+export function cleanUpReqFiles (req: express.Request) {
const filesObject = req.files
if (!filesObject) return
@@ -45,7 +93,7 @@ function cleanUpReqFiles (req: express.Request) {
}
}
-function getHostWithPort (host: string) {
+export function getHostWithPort (host: string) {
const splitted = host.split(':')
// The port was not specified
@@ -58,7 +106,7 @@ function getHostWithPort (host: string) {
return host
}
-function createReqFiles (
+export function createReqFiles (
fieldNames: string[],
mimeTypes: { [id: string]: string | string[] },
destination = CONFIG.STORAGE.TMP_DIR
@@ -84,7 +132,7 @@ function createReqFiles (
return multer({ storage }).fields(fields)
}
-function createAnyReqFiles (
+export function createAnyReqFiles (
mimeTypes: { [id: string]: string | string[] },
fileFilter: (req: express.Request, file: Express.Multer.File, cb: (err: Error, result: boolean) => void) => void
): RequestHandler {
@@ -101,29 +149,19 @@ function createAnyReqFiles (
return multer({ storage, fileFilter }).any()
}
-function isUserAbleToSearchRemoteURI (res: express.Response) {
+export function isUserAbleToSearchRemoteURI (res: express.Response) {
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
(CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
}
-function getCountVideos (req: express.Request) {
+export function getCountVideos (req: express.Request) {
return req.query.skipCount !== true
}
// ---------------------------------------------------------------------------
-
-export {
- buildNSFWFilter,
- getHostWithPort,
- createAnyReqFiles,
- isUserAbleToSearchRemoteURI,
- createReqFiles,
- cleanUpReqFiles,
- getCountVideos
-}
-
+// Private
// ---------------------------------------------------------------------------
async function generateReqFilename (
diff --git a/server/core/helpers/query.ts b/server/core/helpers/query.ts
index 086ca93df..0a9153a5c 100644
--- a/server/core/helpers/query.ts
+++ b/server/core/helpers/query.ts
@@ -12,6 +12,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
'count',
'sort',
'nsfw',
+ 'nsfwFlagsIncluded',
+ 'nsfwFlagsExcluded',
'isLive',
'categoryOneOf',
'licenceOneOf',
diff --git a/server/core/initializers/checker-after-init.ts b/server/core/initializers/checker-after-init.ts
index c76c2fb81..d3b430dd9 100644
--- a/server/core/initializers/checker-after-init.ts
+++ b/server/core/initializers/checker-after-init.ts
@@ -25,8 +25,8 @@ async function checkActivityPubUrls () {
logger.warn(
'It seems PeerTube was started (and created some data) with another domain name. ' +
- 'This means you will not be able to federate! ' +
- 'Please use %s %s npm run update-host to fix this.',
+ 'This means you will not be able to federate! ' +
+ 'Please use %s %s npm run update-host to fix this.',
NODE_CONFIG_DIR ? `NODE_CONFIG_DIR=${NODE_CONFIG_DIR}` : '',
NODE_ENV ? `NODE_ENV=${NODE_ENV}` : ''
)
@@ -35,7 +35,6 @@ async function checkActivityPubUrls () {
// Some checks on configuration files or throw if there is an error
function checkConfig () {
-
const configFiles = config.util.getConfigSources().map(s => s.name).join(' -> ')
logger.info('Using following configuration file hierarchy: %s.', configFiles)
@@ -102,7 +101,11 @@ async function checkFFmpegVersion () {
export {
applicationExist,
- checkActivityPubUrls, checkConfig, checkFFmpegVersion, clientsExist, usersExist
+ checkActivityPubUrls,
+ checkConfig,
+ checkFFmpegVersion,
+ clientsExist,
+ usersExist
}
// ---------------------------------------------------------------------------
@@ -149,8 +152,10 @@ function checkEmailConfig () {
}
if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_APPROVAL) {
- // eslint-disable-next-line max-len
- logger.warn('SMTP is not configured but signup approval is enabled: PeerTube will not be able to send an email to the user upon acceptance/rejection of the registration request')
+ logger.warn(
+ 'SMTP is not configured but signup approval is enabled: ' +
+ 'PeerTube will not be able to send an email to the user upon acceptance/rejection of the registration request'
+ )
}
if (CONFIG.CONTACT_FORM.ENABLED) {
@@ -162,7 +167,7 @@ function checkEmailConfig () {
function checkNSFWPolicyConfig () {
const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
- const available = [ 'do_not_list', 'blur', 'display' ]
+ const available = [ 'do_not_list', 'warn', 'blur', 'display' ]
if (available.includes(defaultNSFWPolicy) === false) {
throw new Error('NSFW policy setting should be ' + available.join(' or ') + ' instead of ' + defaultNSFWPolicy)
}
@@ -211,7 +216,7 @@ function checkRemoteRedundancyConfig () {
function checkStorageConfig () {
// Check storage directory locations
if (isProdInstance()) {
- const configStorage = config.get<{ [ name: string ]: string }>('storage')
+ const configStorage = config.get<{ [name: string]: string }>('storage')
for (const key of Object.keys(configStorage)) {
if (configStorage[key].startsWith('storage/')) {
@@ -324,7 +329,6 @@ function checkObjectStorageConfig () {
}
if (CONFIG.TRANSCODING.ORIGINAL_FILE.KEEP) {
-
if (!CONFIG.OBJECT_STORAGE.ORIGINAL_VIDEO_FILES.BUCKET_NAME) {
throw new Error('original_video_files_bucket should be set when object storage support is enabled.')
}
@@ -359,7 +363,10 @@ function checkObjectStorageConfig () {
if (CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART > parseBytes('250MB')) {
// eslint-disable-next-line max-len
- logger.warn(`Object storage max upload part seems to have a big value (${CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART} bytes). Consider using a lower one (like 100MB).`)
+ logger.warn(
+ `Object storage max upload part seems to have a big value (${CONFIG.OBJECT_STORAGE.MAX_UPLOAD_PART} bytes). ` +
+ `Consider using a lower one (like 100MB).`
+ )
}
}
diff --git a/server/core/initializers/constants.ts b/server/core/initializers/constants.ts
index 65b49a8a2..7f5027d7c 100644
--- a/server/core/initializers/constants.ts
+++ b/server/core/initializers/constants.ts
@@ -46,7 +46,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
// ---------------------------------------------------------------------------
-export const LAST_MIGRATION_VERSION = 890
+export const LAST_MIGRATION_VERSION = 895
// ---------------------------------------------------------------------------
@@ -417,6 +417,7 @@ export const CONSTRAINTS_FIELDS = {
LANGUAGE: { min: 1, max: 10 }, // Length
TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length
DESCRIPTION: { min: 3, max: 10000 }, // Length
+ NSFW_SUMMARY: { min: 3, max: 250 }, // Length
SUPPORT: { min: 3, max: 1000 }, // Length
IMAGE: {
EXTNAME: [ '.png', '.jpg', '.jpeg', '.webp' ],
@@ -837,6 +838,7 @@ export const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
export const NSFW_POLICY_TYPES: { [id: string]: NSFWPolicyType } = {
DO_NOT_LIST: 'do_not_list',
+ WARN: 'warn',
BLUR: 'blur',
DISPLAY: 'display'
}
diff --git a/server/core/initializers/migrations/0895-nsfw-flags.ts b/server/core/initializers/migrations/0895-nsfw-flags.ts
new file mode 100644
index 000000000..6a27e8b99
--- /dev/null
+++ b/server/core/initializers/migrations/0895-nsfw-flags.ts
@@ -0,0 +1,58 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise {
+ const { transaction } = utils
+
+ {
+ await utils.queryInterface.addColumn('video', 'nsfwSummary', {
+ type: Sequelize.STRING,
+ defaultValue: null,
+ allowNull: true
+ }, { transaction })
+
+ await utils.queryInterface.addColumn('video', 'nsfwFlags', {
+ type: Sequelize.INTEGER,
+ defaultValue: 0,
+ allowNull: false
+ }, { transaction })
+ }
+
+ {
+ await utils.queryInterface.addColumn('user', 'nsfwFlagsDisplayed', {
+ type: Sequelize.INTEGER,
+ defaultValue: 0,
+ allowNull: false
+ }, { transaction })
+
+ await utils.queryInterface.addColumn('user', 'nsfwFlagsHidden', {
+ type: Sequelize.INTEGER,
+ defaultValue: 0,
+ allowNull: false
+ }, { transaction })
+
+ await utils.queryInterface.addColumn('user', 'nsfwFlagsWarned', {
+ type: Sequelize.INTEGER,
+ defaultValue: 0,
+ allowNull: false
+ }, { transaction })
+
+ await utils.queryInterface.addColumn('user', 'nsfwFlagsBlurred', {
+ type: Sequelize.INTEGER,
+ defaultValue: 0,
+ allowNull: false
+ }, { transaction })
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ down,
+ up
+}
diff --git a/server/core/lib/activitypub/videos/shared/creator.ts b/server/core/lib/activitypub/videos/shared/creator.ts
index 676074343..f53ba0379 100644
--- a/server/core/lib/activitypub/videos/shared/creator.ts
+++ b/server/core/lib/activitypub/videos/shared/creator.ts
@@ -18,7 +18,7 @@ export class APVideoCreator extends APVideoAbstractBuilder {
}
async create () {
- logger.debug('Adding remote video %s.', this.videoObject.id, this.lTags())
+ logger.debug('Adding remote video %s.', this.videoObject.id, { ...this.videoObject, ...this.lTags() })
const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
const channel = channelActor.VideoChannel
diff --git a/server/core/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/core/lib/activitypub/videos/shared/object-to-model-attributes.ts
index 5920ad4ec..6d577c6a4 100644
--- a/server/core/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ b/server/core/lib/activitypub/videos/shared/object-to-model-attributes.ts
@@ -5,9 +5,12 @@ import {
ActivityMagnetUrlObject,
ActivityPlaylistSegmentHashesObject,
ActivityPlaylistUrlObject,
+ ActivitySensitiveTagObject,
ActivityTagObject,
ActivityUrlObject,
ActivityVideoUrlObject,
+ NSFWFlag,
+ stringToNSFWFlag,
VideoFileFormatFlag,
VideoFileStream,
VideoObject,
@@ -28,7 +31,7 @@ import { VideoCaptionModel } from '@server/models/video/video-caption.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist.js'
import { FilteredModelAttributes } from '@server/types/index.js'
-import { MChannelId, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId, isStreamingPlaylist } from '@server/types/models/index.js'
+import { isStreamingPlaylist, MChannelId, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models/index.js'
import { decode as magnetUriDecode } from 'magnet-uri'
import { basename, extname } from 'path'
import { getDurationFromActivityStream } from '../../activity.js'
@@ -271,7 +274,14 @@ export function getVideoAttributesFromObject (videoChannel: MChannelId, videoObj
language,
description,
support,
+
nsfw: videoObject.sensitive,
+ nsfwSummary: videoObject.sensitive
+ ? videoObject.summary
+ : null,
+ nsfwFlags: videoObject.sensitive
+ ? getNSFWFlags(videoObject.tag)
+ : NSFWFlag.NONE,
commentsPolicy: videoObject.commentsPolicy,
@@ -320,8 +330,12 @@ function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
}
-function isAPHashTagObject (url: any): url is ActivityHashTagObject {
- return url && url.type === 'Hashtag'
+function isAPHashTagObject (tag: any): tag is ActivityHashTagObject {
+ return tag && tag.type === 'Hashtag'
+}
+
+function isAPSensitiveTagObject (tag: any): tag is ActivitySensitiveTagObject {
+ return tag && tag.type === 'SensitiveTag'
}
function getTorrentRelatedInfo (options: {
@@ -361,3 +375,10 @@ function getTorrentRelatedInfo (options: {
infoHash: magnetParsed.infoHash
}
}
+
+function getNSFWFlags (tags: ActivityTagObject[]) {
+ return tags.filter(t => isAPSensitiveTagObject(t))
+ .map(t => stringToNSFWFlag(t.name))
+ .filter(t => !!t)
+ .reduce((acc, t) => acc | t, 0)
+}
diff --git a/server/core/lib/activitypub/videos/updater.ts b/server/core/lib/activitypub/videos/updater.ts
index ca8b01617..40356d3d5 100644
--- a/server/core/lib/activitypub/videos/updater.ts
+++ b/server/core/lib/activitypub/videos/updater.ts
@@ -122,6 +122,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
private updateVideo (channel: MChannelId, transaction?: Transaction, overrideTo?: string[]) {
const to = overrideTo || this.videoObject.to
const videoData = getVideoAttributesFromObject(channel, this.videoObject, to)
+
this.video.name = videoData.name
this.video.uuid = videoData.uuid
this.video.url = videoData.url
@@ -131,6 +132,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
this.video.description = videoData.description
this.video.support = videoData.support
this.video.nsfw = videoData.nsfw
+ this.video.nsfwSummary = videoData.nsfwSummary
+ this.video.nsfwFlags = videoData.nsfwFlags
this.video.commentsPolicy = videoData.commentsPolicy
this.video.downloadEnabled = videoData.downloadEnabled
this.video.waitTranscoding = videoData.waitTranscoding
diff --git a/server/core/lib/local-video-creator.ts b/server/core/lib/local-video-creator.ts
index 1c4c574d1..a61a6e2df 100644
--- a/server/core/lib/local-video-creator.ts
+++ b/server/core/lib/local-video-creator.ts
@@ -2,6 +2,7 @@ import { buildAspectRatio } from '@peertube/peertube-core-utils'
import {
LiveVideoCreate,
LiveVideoLatencyMode,
+ NSFWFlag,
ThumbnailType,
ThumbnailType_Type,
VideoCreate,
@@ -56,9 +57,9 @@ export type ThumbnailOptions = {
type ChaptersOption = { timecode: number, title: string }[]
type VideoAttributeHookFilter =
- 'filter:api.video.user-import.video-attribute.result' |
- 'filter:api.video.upload.video-attribute.result' |
- 'filter:api.video.live.video-attribute.result'
+ | 'filter:api.video.user-import.video-attribute.result'
+ | 'filter:api.video.upload.video-attribute.result'
+ | 'filter:api.video.live.video-attribute.result'
export class LocalVideoCreator {
private readonly lTags: LoggerTagsFn
@@ -76,28 +77,30 @@ export class LocalVideoCreator {
private videoFile: MVideoFile
private videoPath: string
- constructor (private readonly options: {
- lTags: LoggerTagsFn
+ constructor (
+ private readonly options: {
+ lTags: LoggerTagsFn
- videoFile: {
- path: string
- probe: FfprobeData
+ videoFile: {
+ path: string
+ probe: FfprobeData
+ }
+
+ videoAttributes: VideoAttributes
+ liveAttributes: LiveAttributes
+
+ channel: MChannelAccountLight
+ user: MUser
+ videoAttributeResultHook: VideoAttributeHookFilter
+ thumbnails: ThumbnailOptions
+
+ chapters: ChaptersOption | undefined
+ fallbackChapters: {
+ fromDescription: boolean
+ finalFallback: ChaptersOption | undefined
+ }
}
-
- videoAttributes: VideoAttributes
- liveAttributes: LiveAttributes
-
- channel: MChannelAccountLight
- user: MUser
- videoAttributeResultHook: VideoAttributeHookFilter
- thumbnails: ThumbnailOptions
-
- chapters: ChaptersOption | undefined
- fallbackChapters: {
- fromDescription: boolean
- finalFallback: ChaptersOption | undefined
- }
- }) {
+ ) {
this.videoFilePath = options.videoFile?.path
this.videoFileProbe = options.videoFile?.probe
@@ -284,7 +287,11 @@ export class LocalVideoCreator {
commentsPolicy: buildCommentsPolicy(videoInfo),
downloadEnabled: videoInfo.downloadEnabled ?? CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED,
waitTranscoding: videoInfo.waitTranscoding || false,
+
nsfw: videoInfo.nsfw || false,
+ nsfwSummary: videoInfo.nsfwSummary,
+ nsfwFlags: videoInfo.nsfwFlags || NSFWFlag.NONE,
+
description: videoInfo.description,
support: videoInfo.support,
privacy: videoInfo.privacy || VideoPrivacy.PRIVATE,
diff --git a/server/core/lib/video-pre-import.ts b/server/core/lib/video-pre-import.ts
index e17bf2797..f1b3132f1 100644
--- a/server/core/lib/video-pre-import.ts
+++ b/server/core/lib/video-pre-import.ts
@@ -1,4 +1,5 @@
import {
+ NSFWFlag,
ThumbnailType,
ThumbnailType_Type,
VideoImportCreate,
@@ -28,7 +29,8 @@ import {
MThumbnail,
MUser,
MVideo,
- MVideoAccountDefault, MVideoImportFormattable,
+ MVideoAccountDefault,
+ MVideoImportFormattable,
MVideoTag,
MVideoThumbnail,
MVideoWithBlacklistLight
@@ -80,7 +82,9 @@ async function insertFromImportIntoDB (parameters: {
const sequelizeOptions = { transaction: t }
// eslint-disable-next-line max-len
- const videoCreated = await video.save(sequelizeOptions) as (MVideoAccountDefault & MVideoWithBlacklistLight & MVideoTag & MVideoThumbnail)
+ const videoCreated = await video.save(
+ sequelizeOptions
+ ) as (MVideoAccountDefault & MVideoWithBlacklistLight & MVideoTag & MVideoThumbnail)
videoCreated.VideoChannel = videoChannel
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
@@ -132,6 +136,8 @@ async function buildVideoFromImport ({ channelId, importData, importDataOverride
waitTranscoding: importDataOverride?.waitTranscoding ?? true,
state: VideoState.TO_IMPORT,
nsfw: importDataOverride?.nsfw || importData.nsfw || false,
+ nsfwFlags: importDataOverride?.nsfwFlags || NSFWFlag.NONE,
+ nsfwSummary: importDataOverride?.nsfwSummary || null,
description: importDataOverride?.description || importData.description,
support: importDataOverride?.support || null,
privacy: importDataOverride?.privacy || VideoPrivacy.PRIVATE,
@@ -178,7 +184,9 @@ async function buildYoutubeDLImport (options: {
youtubeDLInfo = await youtubeDL.getInfoForDownload()
} catch (err) {
throw YoutubeDlImportError.fromError(
- err, YoutubeDlImportError.CODE.FETCH_ERROR, `Cannot fetch information from import for URL ${targetUrl}`
+ err,
+ YoutubeDlImportError.CODE.FETCH_ERROR,
+ `Cannot fetch information from import for URL ${targetUrl}`
)
}
@@ -272,9 +280,7 @@ async function buildYoutubeDLImport (options: {
// ---------------------------------------------------------------------------
-export {
- YoutubeDlImportError, buildVideoFromImport, buildYoutubeDLImport, insertFromImportIntoDB
-}
+export { buildVideoFromImport, buildYoutubeDLImport, insertFromImportIntoDB, YoutubeDlImportError }
// ---------------------------------------------------------------------------
diff --git a/server/core/middlewares/validators/users/users.ts b/server/core/middlewares/validators/users/users.ts
index 341c0c766..b95c7c53a 100644
--- a/server/core/middlewares/validators/users/users.ts
+++ b/server/core/middlewares/validators/users/users.ts
@@ -1,5 +1,5 @@
import { arrayify, forceNumber } from '@peertube/peertube-core-utils'
-import { HttpStatusCode, ServerErrorCode, UserRole } from '@peertube/peertube-models'
+import { HttpStatusCode, ServerErrorCode, UserRole, UserUpdateMe } from '@peertube/peertube-models'
import { isStringArray } from '@server/helpers/custom-validators/search.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { MUser } from '@server/types/models/user/user.js'
@@ -42,6 +42,7 @@ import {
doesVideoExist,
isValidVideoIdParam
} from '../shared/index.js'
+import { isNSFWFlagsValid } from '@server/helpers/custom-validators/videos.js'
export const usersListValidator = [
query('blocked')
@@ -232,9 +233,23 @@ export const usersUpdateMeValidator = [
body('email')
.optional()
.isEmail(),
+
body('nsfwPolicy')
.optional()
.custom(isUserNSFWPolicyValid),
+ body('nsfwFlagsDisplayed')
+ .optional()
+ .custom(isNSFWFlagsValid),
+ body('nsfwFlagsHidden')
+ .optional()
+ .custom(isNSFWFlagsValid),
+ body('nsfwFlagsWarned')
+ .optional()
+ .custom(isNSFWFlagsValid),
+ body('nsfwFlagsBlurred')
+ .optional()
+ .custom(isNSFWFlagsValid),
+
body('autoPlayVideo')
.optional()
.custom(isUserAutoPlayVideoValid),
@@ -268,16 +283,32 @@ export const usersUpdateMeValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = res.locals.oauth.token.User
- if (req.body.password || req.body.email) {
+ const body = req.body as UserUpdateMe
+
+ if (
+ ((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsWarned || 0)) !== 0 ||
+ ((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsDisplayed || 0)) !== 0 ||
+ ((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsHidden || 0)) !== 0 ||
+ ((body.nsfwFlagsDisplayed || 0) & (body.nsfwFlagsHidden || 0)) !== 0 ||
+ ((body.nsfwFlagsDisplayed || 0) & (body.nsfwFlagsWarned || 0)) !== 0 ||
+ ((body.nsfwFlagsHidden || 0) & (body.nsfwFlagsWarned || 0)) !== 0
+ ) {
+ return res.fail({
+ status: HttpStatusCode.BAD_REQUEST_400,
+ message: 'Cannot use same flags in nsfwFlagsDisplayed, nsfwFlagsHidden, nsfwFlagsBlurred and nsfwFlagsWarned at the same time'
+ })
+ }
+
+ if (body.password || body.email) {
if (user.pluginAuth !== null) {
return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
}
- if (!req.body.currentPassword) {
+ if (!body.currentPassword) {
return res.fail({ message: 'currentPassword parameter is missing' })
}
- if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
+ if (await user.isPasswordMatch(body.currentPassword) !== true) {
return res.fail({
status: HttpStatusCode.UNAUTHORIZED_401,
message: 'currentPassword is invalid.',
@@ -288,7 +319,7 @@ export const usersUpdateMeValidator = [
if (areValidationErrors(req, res, { omitBodyLog: true })) return
- if (req.body.email && req.body.email !== user.email && !await checkEmailDoesNotAlreadyExist(req.body.email, res)) return
+ if (body.email && body.email !== user.email && !await checkEmailDoesNotAlreadyExist(body.email, res)) return
return next()
}
diff --git a/server/core/middlewares/validators/videos/video-imports.ts b/server/core/middlewares/validators/videos/video-imports.ts
index c9fceadae..d1829d4a7 100644
--- a/server/core/middlewares/validators/videos/video-imports.ts
+++ b/server/core/middlewares/validators/videos/video-imports.ts
@@ -1,11 +1,11 @@
-import express from 'express'
-import { body, param, query } from 'express-validator'
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode, UserRight, VideoImportCreate, VideoImportState } from '@peertube/peertube-models'
import { isResolvingToUnicastOnly } from '@server/helpers/dns.js'
import { isPreImportVideoAccepted } from '@server/lib/moderation.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { MUserAccountId, MVideoImport } from '@server/types/models/index.js'
+import express from 'express'
+import { body, param, query } from 'express-validator'
import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports.js'
import { isValidPasswordProtectedPrivacy, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos.js'
@@ -14,7 +14,7 @@ import { logger } from '../../../helpers/logger.js'
import { CONFIG } from '../../../initializers/config.js'
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
import { areValidationErrors, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared/index.js'
-import { getCommonVideoEditAttributes } from './videos.js'
+import { areErrorsInNSFW, getCommonVideoEditAttributes } from './videos.js'
const videoImportAddValidator = getCommonVideoEditAttributes().concat([
body('channelId')
@@ -30,7 +30,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
.custom((value, { req }) => isVideoImportTorrentFile(req.files))
.withMessage(
'This torrent file is not supported or too large. Please, make sure it is of the following type: ' +
- CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ')
+ CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ')
),
body('name')
.optional()
@@ -47,6 +47,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
const torrentFile = req.files?.['torrentfile'] ? req.files['torrentfile'][0] : undefined
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
+ if (areErrorsInNSFW(req, res)) return cleanUpReqFiles(req)
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
@@ -153,10 +154,10 @@ const videoImportCancelValidator = [
// ---------------------------------------------------------------------------
export {
+ getMyVideoImportsValidator,
videoImportAddValidator,
videoImportCancelValidator,
- videoImportDeleteValidator,
- getMyVideoImportsValidator
+ videoImportDeleteValidator
}
// ---------------------------------------------------------------------------
diff --git a/server/core/middlewares/validators/videos/video-live.ts b/server/core/middlewares/validators/videos/video-live.ts
index 74ebe51ff..270ea96b5 100644
--- a/server/core/middlewares/validators/videos/video-live.ts
+++ b/server/core/middlewares/validators/videos/video-live.ts
@@ -28,7 +28,7 @@ import {
doesVideoExist,
isValidVideoIdParam
} from '../shared/index.js'
-import { getCommonVideoEditAttributes } from './videos.js'
+import { areErrorsInNSFW, getCommonVideoEditAttributes } from './videos.js'
const videoLiveGetValidator = [
isValidVideoIdParam('videoId'),
@@ -88,6 +88,7 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
+ if (areErrorsInNSFW(req, res)) return cleanUpReqFiles(req)
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
@@ -300,7 +301,6 @@ function checkLiveSettingsReplayConsistency (options: {
// We now save replays of this live, so replay settings are mandatory
if (res.locals.videoLive.saveReplay !== true && body.saveReplay === true) {
-
if (!exists(body.replaySettings)) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
diff --git a/server/core/middlewares/validators/videos/videos.ts b/server/core/middlewares/validators/videos/videos.ts
index 2dfc6a5c4..6f8ad8860 100644
--- a/server/core/middlewares/validators/videos/videos.ts
+++ b/server/core/middlewares/validators/videos/videos.ts
@@ -1,12 +1,21 @@
import { arrayify } from '@peertube/peertube-core-utils'
-import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@peertube/peertube-models'
+import {
+ HttpStatusCode,
+ ServerErrorCode,
+ UserRight,
+ VideoCreateUpdateCommon,
+ VideosCommonQuery,
+ VideoState
+} from '@peertube/peertube-models'
+import { isHostValid } from '@server/helpers/custom-validators/servers.js'
+import { VideoLoadType } from '@server/lib/model-loaders/video.js'
import { Redis } from '@server/lib/redis.js'
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
import { getServerActor } from '@server/models/application/application.js'
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
import { MUserAccountId, MVideoFullLight } from '@server/types/models/index.js'
import express from 'express'
-import { ValidationChain, body, param, query } from 'express-validator'
+import { body, param, query, ValidationChain } from 'express-validator'
import {
exists,
hasArrayLength,
@@ -23,6 +32,8 @@ import {
import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search.js'
import {
areVideoTagsValid,
+ isNSFWFlagsValid,
+ isNSFWSummaryValid,
isScheduleVideoUpdatePrivacyValid,
isValidPasswordProtectedPrivacy,
isVideoCategoryValid,
@@ -45,7 +56,8 @@ import { CONFIG } from '../../../initializers/config.js'
import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants.js'
import { VideoModel } from '../../../models/video/video.js'
import {
- areValidationErrors, checkCanAccessVideoStaticFiles,
+ areValidationErrors,
+ checkCanAccessVideoStaticFiles,
checkCanSeeVideo,
checkUserCanManageVideo,
doesVideoChannelOfAccountExist,
@@ -55,8 +67,6 @@ import {
isValidVideoPasswordHeader
} from '../shared/index.js'
import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
-import { VideoLoadType } from '@server/lib/model-loaders/video.js'
-import { isHostValid } from '@server/helpers/custom-validators/servers.js'
export const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
body('videofile')
@@ -138,7 +148,6 @@ export const videosAddResumableValidator = [
*
* Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
* see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
- *
*/
export const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
body('filename')
@@ -206,6 +215,7 @@ export const videosUpdateValidator = getCommonVideoEditAttributes().concat([
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
+ if (areErrorsInNSFW(req, res)) return cleanUpReqFiles(req)
if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
@@ -352,12 +362,12 @@ export function getCommonVideoEditAttributes () {
body('thumbnailfile')
.custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage(
'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
- CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
+ CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
),
body('previewfile')
.custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage(
'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
- CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
+ CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
),
body('category')
@@ -376,6 +386,14 @@ export function getCommonVideoEditAttributes () {
.optional()
.customSanitizer(toBooleanOrNull)
.custom(isBooleanValid).withMessage('Should have a valid nsfw boolean'),
+ body('nsfwFlags')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isNSFWFlagsValid),
+ body('nsfwSummary')
+ .optional()
+ .customSanitizer(toValueOrNull)
+ .custom(isNSFWSummaryValid),
body('waitTranscoding')
.optional()
.customSanitizer(toBooleanOrNull)
@@ -398,7 +416,7 @@ export function getCommonVideoEditAttributes () {
.custom(areVideoTagsValid)
.withMessage(
`Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
- `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
+ `${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
),
// TODO: remove, deprecated in PeerTube 6.2
body('commentsEnabled')
@@ -458,6 +476,14 @@ export const commonVideosFiltersValidator = [
query('nsfw')
.optional()
.custom(isBooleanBothQueryValid),
+ query('nsfwFlagsIncluded')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isNSFWFlagsValid),
+ query('nsfwFlagsExcluded')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isNSFWFlagsValid),
query('isLive')
.optional()
.customSanitizer(toBooleanOrNull)
@@ -499,10 +525,19 @@ export const commonVideosFiltersValidator = [
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
+ const query = req.query as VideosCommonQuery
+
+ if (((query.nsfwFlagsExcluded || 0) & (query.nsfwFlagsIncluded || 0)) !== 0) {
+ return res.fail({
+ status: HttpStatusCode.BAD_REQUEST_400,
+ message: 'Cannot use same flags in nsfwFlagsIncluded and nsfwFlagsExcluded at the same time'
+ })
+ }
+
const user = res.locals.oauth?.token.User
if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
- if (req.query.include || req.query.privacyOneOf || req.query.autoTagOneOf) {
+ if (query.include || query.privacyOneOf || query.autoTagOneOf) {
return res.fail({
status: HttpStatusCode.UNAUTHORIZED_401,
message: 'You are not allowed to see all videos, specify a custom include or auto tags filter.'
@@ -510,7 +545,7 @@ export const commonVideosFiltersValidator = [
}
}
- if (!user && exists(req.query.excludeAlreadyWatched)) {
+ if (!user && exists(query.excludeAlreadyWatched)) {
res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Cannot use excludeAlreadyWatched parameter when auth token is not provided'
@@ -530,6 +565,24 @@ export const commonVideosFiltersValidator = [
}
]
+export function areErrorsInNSFW (req: express.Request, res: express.Response) {
+ const body = req.body as VideoCreateUpdateCommon
+
+ if (!body.nsfw) {
+ if (body.nsfwFlags) {
+ res.fail({ message: 'Cannot set nsfwFlags if the video is not NSFW.' })
+ return true
+ }
+
+ if (body.nsfwSummary) {
+ res.fail({ message: 'Cannot set nsfwSummary if the video is not NSFW.' })
+ return true
+ }
+ }
+
+ return false
+}
+
// ---------------------------------------------------------------------------
// Private
// ---------------------------------------------------------------------------
@@ -557,6 +610,7 @@ async function commonVideoChecksPass (options: {
const { req, res, user } = options
if (areErrorsInScheduleUpdate(req, res)) return false
+ if (areErrorsInNSFW(req, res)) return false
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
diff --git a/server/core/models/user/user.ts b/server/core/models/user/user.ts
index 77937932f..e6d2e0362 100644
--- a/server/core/models/user/user.ts
+++ b/server/core/models/user/user.ts
@@ -77,6 +77,7 @@ import { VideoPlaylistModel } from '../video/video-playlist.js'
import { VideoModel } from '../video/video.js'
import { UserNotificationSettingModel } from './user-notification-setting.js'
import { UserExportModel } from './user-export.js'
+import { isNSFWFlagsValid } from '@server/helpers/custom-validators/videos.js'
enum ScopeNames {
FOR_ME_API = 'FOR_ME_API',
@@ -316,6 +317,30 @@ export class UserModel extends SequelizeModel {
@Column(DataType.ENUM(...Object.values(NSFW_POLICY_TYPES)))
nsfwPolicy: NSFWPolicyType
+ @AllowNull(false)
+ @Default(0)
+ @Is('UserNSFWFlagsDisplayed', value => throwIfNotValid(value, isNSFWFlagsValid, 'NSFW flags'))
+ @Column
+ nsfwFlagsDisplayed: number
+
+ @AllowNull(false)
+ @Default(0)
+ @Is('UserNSFWFlagsHidden', value => throwIfNotValid(value, isNSFWFlagsValid, 'NSFW flags'))
+ @Column
+ nsfwFlagsHidden: number
+
+ @AllowNull(false)
+ @Default(0)
+ @Is('nsfwFlagsBlurred', value => throwIfNotValid(value, isNSFWFlagsValid, 'NSFW flags'))
+ @Column
+ nsfwFlagsBlurred: number
+
+ @AllowNull(false)
+ @Default(0)
+ @Is('UserNSFWFlagsWarned', value => throwIfNotValid(value, isNSFWFlagsValid, 'NSFW flags'))
+ @Column
+ nsfwFlagsWarned: number
+
@AllowNull(false)
@Is('p2pEnabled', value => throwIfNotValid(value, isUserP2PEnabledValid, 'P2P enabled'))
@Column
@@ -974,6 +999,10 @@ export class UserModel extends SequelizeModel {
emailVerified: this.emailVerified,
nsfwPolicy: this.nsfwPolicy,
+ nsfwFlagsDisplayed: this.nsfwFlagsDisplayed,
+ nsfwFlagsHidden: this.nsfwFlagsHidden,
+ nsfwFlagsWarned: this.nsfwFlagsWarned,
+ nsfwFlagsBlurred: this.nsfwFlagsBlurred,
p2pEnabled: this.p2pEnabled,
diff --git a/server/core/models/video/formatter/video-activity-pub-format.ts b/server/core/models/video/formatter/video-activity-pub-format.ts
index 9bdd6f43d..9e2e90885 100644
--- a/server/core/models/video/formatter/video-activity-pub-format.ts
+++ b/server/core/models/video/formatter/video-activity-pub-format.ts
@@ -1,10 +1,13 @@
import {
+ ActivityHashTagObject,
ActivityIconObject,
ActivityPlaylistUrlObject,
ActivityPubStoryboard,
+ ActivitySensitiveTagObject,
ActivityTagObject,
ActivityTrackerUrlObject,
ActivityUrlObject,
+ nsfwFlagsToString,
VideoCommentPolicy,
VideoObject
} from '@peertube/peertube-models'
@@ -69,7 +72,10 @@ export function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
licence,
language,
views: video.views,
+
sensitive: video.nsfw,
+ summary: video.nsfwSummary,
+
waitTranscoding: video.waitTranscoding,
state: video.state,
@@ -281,13 +287,26 @@ function buildTrackerUrls (video: MVideoAP): ActivityTrackerUrlObject[] {
// ---------------------------------------------------------------------------
-function buildTags (video: MVideoAP) {
- if (!isArray(video.Tags)) return []
+function buildTags (video: MVideoAP): (ActivitySensitiveTagObject | ActivityHashTagObject)[] {
+ const tags = isArray(video.Tags)
+ ? video.Tags
+ : []
- return video.Tags.map(t => ({
- type: 'Hashtag' as 'Hashtag',
- name: t.name
- }))
+ return [
+ ...tags.map(t =>
+ ({
+ type: 'Hashtag' as 'Hashtag',
+ name: t.name
+ }) as ActivityHashTagObject
+ ),
+
+ ...nsfwFlagsToString(video.nsfwFlags).map(f =>
+ ({
+ type: 'SensitiveTag' as 'SensitiveTag',
+ name: f
+ }) as ActivitySensitiveTagObject
+ )
+ ]
}
function buildIcon (video: MVideoAP): ActivityIconObject[] {
diff --git a/server/core/models/video/formatter/video-api-format.ts b/server/core/models/video/formatter/video-api-format.ts
index b6c4cbf49..16c16f43f 100644
--- a/server/core/models/video/formatter/video-api-format.ts
+++ b/server/core/models/video/formatter/video-api-format.ts
@@ -93,7 +93,10 @@ export function videoModelToFormattedJSON (video: MVideoFormattable, options: Vi
id: video.privacy,
label: getPrivacyLabel(video.privacy)
},
+
nsfw: video.nsfw,
+ nsfwFlags: video.nsfwFlags,
+ nsfwSummary: video.nsfwSummary,
truncatedDescription: video.getTruncatedDescription(),
description: options && options.completeDescription === true
diff --git a/server/core/models/video/sql/video/shared/video-table-attributes.ts b/server/core/models/video/sql/video/shared/video-table-attributes.ts
index dce270968..3c13d8e28 100644
--- a/server/core/models/video/sql/video/shared/video-table-attributes.ts
+++ b/server/core/models/video/sql/video/shared/video-table-attributes.ts
@@ -271,6 +271,8 @@ export class VideoTableAttributes {
'language',
'privacy',
'nsfw',
+ 'nsfwSummary',
+ 'nsfwFlags',
'description',
'support',
'duration',
diff --git a/server/core/models/video/sql/video/videos-id-list-query-builder.ts b/server/core/models/video/sql/video/videos-id-list-query-builder.ts
index 78653440d..321b7fed5 100644
--- a/server/core/models/video/sql/video/videos-id-list-query-builder.ts
+++ b/server/core/models/video/sql/video/videos-id-list-query-builder.ts
@@ -30,6 +30,9 @@ export type BuildVideosListQueryOptions = {
sort: string
nsfw?: boolean
+ nsfwFlagsIncluded?: number
+ nsfwFlagsExcluded?: number
+
host?: string
isLive?: boolean
isLocal?: boolean
@@ -223,9 +226,11 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
}
if (options.nsfw === true) {
- this.whereNSFW()
+ this.whereNSFW(options.nsfwFlagsExcluded)
} else if (options.nsfw === false) {
- this.whereSFW()
+ this.whereSFW(options.nsfwFlagsIncluded)
+ } else if (options.nsfwFlagsExcluded) {
+ this.whereNSFWFlagsExcluded(options.nsfwFlagsExcluded)
}
if (options.isLive === true) {
@@ -541,12 +546,31 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
}
}
- private whereNSFW () {
- this.and.push('"video"."nsfw" IS TRUE')
+ private whereNSFW (nsfwFlagsExcluded?: number) {
+ let filter = '"video"."nsfw" IS TRUE'
+
+ if (nsfwFlagsExcluded) {
+ filter += ' AND "video"."nsfwFlags" & :nsfwFlagsExcluded = 0'
+ this.replacements.nsfwFlagsExcluded = nsfwFlagsExcluded
+ }
+
+ this.and.push(filter)
}
- private whereSFW () {
- this.and.push('"video"."nsfw" IS FALSE')
+ private whereSFW (nsfwFlagsIncluded?: number) {
+ let filter = '"video"."nsfw" IS FALSE'
+
+ if (nsfwFlagsIncluded) {
+ filter = `(${filter} OR "video"."nsfwFlags" & :nsfwFlagsIncluded != 0)`
+ this.replacements.nsfwFlagsIncluded = nsfwFlagsIncluded
+ }
+
+ this.and.push(filter)
+ }
+
+ private whereNSFWFlagsExcluded (nsfwFlagsExcluded: number) {
+ this.and.push('"video"."nsfwFlags" & :nsfwFlagsExcluded = 0')
+ this.replacements.nsfwFlagsExcluded = nsfwFlagsExcluded
}
private whereLive () {
diff --git a/server/core/models/video/video.ts b/server/core/models/video/video.ts
index e6268297f..dff10b3f6 100644
--- a/server/core/models/video/video.ts
+++ b/server/core/models/video/video.ts
@@ -68,6 +68,8 @@ import { peertubeTruncate } from '../../helpers/core-utils.js'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
import { isArray, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc.js'
import {
+ isNSFWFlagsValid,
+ isNSFWSummaryValid,
isVideoDescriptionValid,
isVideoDurationValid,
isVideoNameValid,
@@ -469,6 +471,18 @@ export class VideoModel extends SequelizeModel {
@Column
nsfw: boolean
+ @AllowNull(false)
+ @Default(0)
+ @Is('VideoNSFWFlags', value => throwIfNotValid(value, isNSFWFlagsValid, 'NSFW flags'))
+ @Column
+ nsfwFlags: number // NSFWFlagType
+
+ @AllowNull(true)
+ @Default(null)
+ @Is('VideoNSFWSummary', value => throwIfNotValid(value, isNSFWSummaryValid, 'NSFW summary'))
+ @Column
+ nsfwSummary: string
+
@AllowNull(true)
@Default(null)
@Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description', true))
@@ -1044,6 +1058,9 @@ export class VideoModel extends SequelizeModel {
sort: string
nsfw: boolean
+ nsfwFlagsIncluded?: number
+ nsfwFlagsExcluded?: number
+
isLive?: boolean
isLocal?: boolean
include?: VideoIncludeType
@@ -1104,6 +1121,8 @@ export class VideoModel extends SequelizeModel {
'count',
'sort',
'nsfw',
+ 'nsfwFlagsIncluded',
+ 'nsfwFlagsExcluded',
'isLive',
'categoryOneOf',
'licenceOneOf',
@@ -1143,6 +1162,9 @@ export class VideoModel extends SequelizeModel {
sort: string
nsfw?: boolean
+ nsfwFlagsIncluded?: number
+ nsfwFlagsExcluded?: number
+
isLive?: boolean
isLocal?: boolean
include?: VideoIncludeType
@@ -1189,6 +1211,8 @@ export class VideoModel extends SequelizeModel {
...pick(options, [
'include',
'nsfw',
+ 'nsfwFlagsIncluded',
+ 'nsfwFlagsExcluded',
'isLive',
'categoryOneOf',
'licenceOneOf',
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 28daa03ce..2b6577f41 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -786,24 +786,27 @@ paths:
operationId: getAccountVideos
parameters:
- $ref: '#/components/parameters/name'
- - $ref: '#/components/parameters/categoryOneOf'
- - $ref: '#/components/parameters/isLive'
- - $ref: '#/components/parameters/tagsOneOf'
- - $ref: '#/components/parameters/tagsAllOf'
- - $ref: '#/components/parameters/licenceOneOf'
- - $ref: '#/components/parameters/languageOneOf'
- - $ref: '#/components/parameters/autoTagOneOfVideo'
- - $ref: '#/components/parameters/nsfw'
- - $ref: '#/components/parameters/host'
- - $ref: '#/components/parameters/isLocal'
- - $ref: '#/components/parameters/include'
- - $ref: '#/components/parameters/privacyOneOf'
- - $ref: '#/components/parameters/hasHLSFiles'
- - $ref: '#/components/parameters/hasWebVideoFiles'
- - $ref: '#/components/parameters/skipCount'
+ # Common video filter parameters
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
+ - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/videosSort'
+ - $ref: '#/components/parameters/nsfw'
+ - $ref: '#/components/parameters/nsfwFlagsIncluded'
+ - $ref: '#/components/parameters/nsfwFlagsExcluded'
+ - $ref: '#/components/parameters/isLive'
+ - $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/licenceOneOf'
+ - $ref: '#/components/parameters/languageOneOf'
+ - $ref: '#/components/parameters/tagsOneOf'
+ - $ref: '#/components/parameters/tagsAllOf'
+ - $ref: '#/components/parameters/isLocal'
+ - $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebVideoFiles'
+ - $ref: '#/components/parameters/host'
+ - $ref: '#/components/parameters/autoTagOneOfVideo'
+ - $ref: '#/components/parameters/privacyOneOf'
- $ref: '#/components/parameters/excludeAlreadyWatched'
- $ref: '#/components/parameters/search'
responses:
@@ -2226,27 +2229,30 @@ paths:
- My User
- Videos
parameters:
- - $ref: '#/components/parameters/categoryOneOf'
- - $ref: '#/components/parameters/isLive'
- - $ref: '#/components/parameters/tagsOneOf'
- - $ref: '#/components/parameters/tagsAllOf'
- - $ref: '#/components/parameters/licenceOneOf'
- - $ref: '#/components/parameters/languageOneOf'
- - $ref: '#/components/parameters/host'
- - $ref: '#/components/parameters/autoTagOneOfVideo'
- - $ref: '#/components/parameters/nsfw'
- - $ref: '#/components/parameters/isLocal'
- - $ref: '#/components/parameters/include'
- - $ref: '#/components/parameters/privacyOneOf'
- - $ref: '#/components/parameters/hasHLSFiles'
- - $ref: '#/components/parameters/hasWebVideoFiles'
- - $ref: '#/components/parameters/skipCount'
+ - $ref: '#/components/parameters/channelNameOneOf'
+ # Common video filter parameters
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
+ - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/videosSort'
+ - $ref: '#/components/parameters/nsfw'
+ - $ref: '#/components/parameters/nsfwFlagsIncluded'
+ - $ref: '#/components/parameters/nsfwFlagsExcluded'
+ - $ref: '#/components/parameters/isLive'
+ - $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/licenceOneOf'
+ - $ref: '#/components/parameters/languageOneOf'
+ - $ref: '#/components/parameters/tagsOneOf'
+ - $ref: '#/components/parameters/tagsAllOf'
+ - $ref: '#/components/parameters/isLocal'
+ - $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebVideoFiles'
+ - $ref: '#/components/parameters/host'
+ - $ref: '#/components/parameters/autoTagOneOfVideo'
+ - $ref: '#/components/parameters/privacyOneOf'
- $ref: '#/components/parameters/excludeAlreadyWatched'
- $ref: '#/components/parameters/search'
- - $ref: '#/components/parameters/channelNameOneOf'
responses:
'200':
description: successful operation
@@ -2329,24 +2335,27 @@ paths:
- My Subscriptions
- Videos
parameters:
- - $ref: '#/components/parameters/categoryOneOf'
- - $ref: '#/components/parameters/isLive'
- - $ref: '#/components/parameters/tagsOneOf'
- - $ref: '#/components/parameters/tagsAllOf'
- - $ref: '#/components/parameters/licenceOneOf'
- - $ref: '#/components/parameters/languageOneOf'
- - $ref: '#/components/parameters/host'
- - $ref: '#/components/parameters/autoTagOneOfVideo'
- - $ref: '#/components/parameters/nsfw'
- - $ref: '#/components/parameters/isLocal'
- - $ref: '#/components/parameters/include'
- - $ref: '#/components/parameters/privacyOneOf'
- - $ref: '#/components/parameters/hasHLSFiles'
- - $ref: '#/components/parameters/hasWebVideoFiles'
- - $ref: '#/components/parameters/skipCount'
+ # Common video filter parameters
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
+ - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/videosSort'
+ - $ref: '#/components/parameters/nsfw'
+ - $ref: '#/components/parameters/nsfwFlagsIncluded'
+ - $ref: '#/components/parameters/nsfwFlagsExcluded'
+ - $ref: '#/components/parameters/isLive'
+ - $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/licenceOneOf'
+ - $ref: '#/components/parameters/languageOneOf'
+ - $ref: '#/components/parameters/tagsOneOf'
+ - $ref: '#/components/parameters/tagsAllOf'
+ - $ref: '#/components/parameters/isLocal'
+ - $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebVideoFiles'
+ - $ref: '#/components/parameters/host'
+ - $ref: '#/components/parameters/autoTagOneOfVideo'
+ - $ref: '#/components/parameters/privacyOneOf'
- $ref: '#/components/parameters/excludeAlreadyWatched'
- $ref: '#/components/parameters/search'
responses:
@@ -2904,24 +2913,27 @@ paths:
tags:
- Video
parameters:
- - $ref: '#/components/parameters/categoryOneOf'
- - $ref: '#/components/parameters/isLive'
- - $ref: '#/components/parameters/tagsOneOf'
- - $ref: '#/components/parameters/tagsAllOf'
- - $ref: '#/components/parameters/licenceOneOf'
- - $ref: '#/components/parameters/languageOneOf'
- - $ref: '#/components/parameters/host'
- - $ref: '#/components/parameters/autoTagOneOfVideo'
- - $ref: '#/components/parameters/nsfw'
- - $ref: '#/components/parameters/isLocal'
- - $ref: '#/components/parameters/include'
- - $ref: '#/components/parameters/privacyOneOf'
- - $ref: '#/components/parameters/hasHLSFiles'
- - $ref: '#/components/parameters/hasWebVideoFiles'
- - $ref: '#/components/parameters/skipCount'
+ # Common video filter parameters
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
+ - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/videosSort'
+ - $ref: '#/components/parameters/nsfw'
+ - $ref: '#/components/parameters/nsfwFlagsIncluded'
+ - $ref: '#/components/parameters/nsfwFlagsExcluded'
+ - $ref: '#/components/parameters/isLive'
+ - $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/licenceOneOf'
+ - $ref: '#/components/parameters/languageOneOf'
+ - $ref: '#/components/parameters/tagsOneOf'
+ - $ref: '#/components/parameters/tagsAllOf'
+ - $ref: '#/components/parameters/isLocal'
+ - $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebVideoFiles'
+ - $ref: '#/components/parameters/host'
+ - $ref: '#/components/parameters/autoTagOneOfVideo'
+ - $ref: '#/components/parameters/privacyOneOf'
- $ref: '#/components/parameters/excludeAlreadyWatched'
- $ref: '#/components/parameters/search'
responses:
@@ -3056,6 +3068,10 @@ paths:
nsfw:
description: Whether or not this video contains sensitive content
type: boolean
+ nsfwSummary:
+ description: More information about the sensitive content of the video
+ nsfwFlags:
+ $ref: '#/components/schemas/NSFWFlag'
name:
description: Video name
type: string
@@ -3643,6 +3659,10 @@ paths:
nsfw:
description: Whether or not this live video/replay contains sensitive content
type: boolean
+ nsfwSummary:
+ description: More information about the sensitive content of the video
+ nsfwFlags:
+ $ref: '#/components/schemas/NSFWFlag'
name:
description: Live video/replay name
type: string
@@ -4634,24 +4654,27 @@ paths:
- Video Channels
parameters:
- $ref: '#/components/parameters/channelHandle'
- - $ref: '#/components/parameters/categoryOneOf'
- - $ref: '#/components/parameters/isLive'
- - $ref: '#/components/parameters/tagsOneOf'
- - $ref: '#/components/parameters/tagsAllOf'
- - $ref: '#/components/parameters/licenceOneOf'
- - $ref: '#/components/parameters/languageOneOf'
- - $ref: '#/components/parameters/host'
- - $ref: '#/components/parameters/autoTagOneOfVideo'
- - $ref: '#/components/parameters/nsfw'
- - $ref: '#/components/parameters/isLocal'
- - $ref: '#/components/parameters/include'
- - $ref: '#/components/parameters/privacyOneOf'
- - $ref: '#/components/parameters/hasHLSFiles'
- - $ref: '#/components/parameters/hasWebVideoFiles'
- - $ref: '#/components/parameters/skipCount'
+ # Common video filter parameters
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
+ - $ref: '#/components/parameters/skipCount'
- $ref: '#/components/parameters/videosSort'
+ - $ref: '#/components/parameters/nsfw'
+ - $ref: '#/components/parameters/nsfwFlagsIncluded'
+ - $ref: '#/components/parameters/nsfwFlagsExcluded'
+ - $ref: '#/components/parameters/isLive'
+ - $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/licenceOneOf'
+ - $ref: '#/components/parameters/languageOneOf'
+ - $ref: '#/components/parameters/tagsOneOf'
+ - $ref: '#/components/parameters/tagsAllOf'
+ - $ref: '#/components/parameters/isLocal'
+ - $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebVideoFiles'
+ - $ref: '#/components/parameters/host'
+ - $ref: '#/components/parameters/autoTagOneOfVideo'
+ - $ref: '#/components/parameters/privacyOneOf'
- $ref: '#/components/parameters/excludeAlreadyWatched'
- $ref: '#/components/parameters/search'
responses:
@@ -5639,27 +5662,30 @@ paths:
you can use the REST API to fetch the complete video information and interact with it.
schema:
type: string
- - $ref: '#/components/parameters/categoryOneOf'
- - $ref: '#/components/parameters/isLive'
- - $ref: '#/components/parameters/tagsOneOf'
- - $ref: '#/components/parameters/tagsAllOf'
- - $ref: '#/components/parameters/licenceOneOf'
- - $ref: '#/components/parameters/languageOneOf'
- - $ref: '#/components/parameters/autoTagOneOfVideo'
- - $ref: '#/components/parameters/nsfw'
- - $ref: '#/components/parameters/isLocal'
- - $ref: '#/components/parameters/include'
- - $ref: '#/components/parameters/privacyOneOf'
- $ref: '#/components/parameters/uuids'
- - $ref: '#/components/parameters/hasHLSFiles'
- - $ref: '#/components/parameters/hasWebVideoFiles'
- - $ref: '#/components/parameters/skipCount'
+ - $ref: '#/components/parameters/searchTarget'
+ # Common video filter parameters
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
- - $ref: '#/components/parameters/searchTarget'
- - $ref: '#/components/parameters/videosSearchSort'
- - $ref: '#/components/parameters/excludeAlreadyWatched'
+ - $ref: '#/components/parameters/skipCount'
+ - $ref: '#/components/parameters/videosSort'
+ - $ref: '#/components/parameters/nsfw'
+ - $ref: '#/components/parameters/nsfwFlagsIncluded'
+ - $ref: '#/components/parameters/nsfwFlagsExcluded'
+ - $ref: '#/components/parameters/isLive'
+ - $ref: '#/components/parameters/categoryOneOf'
+ - $ref: '#/components/parameters/licenceOneOf'
+ - $ref: '#/components/parameters/languageOneOf'
+ - $ref: '#/components/parameters/tagsOneOf'
+ - $ref: '#/components/parameters/tagsAllOf'
+ - $ref: '#/components/parameters/isLocal'
+ - $ref: '#/components/parameters/include'
+ - $ref: '#/components/parameters/hasHLSFiles'
+ - $ref: '#/components/parameters/hasWebVideoFiles'
- $ref: '#/components/parameters/host'
+ - $ref: '#/components/parameters/autoTagOneOfVideo'
+ - $ref: '#/components/parameters/privacyOneOf'
+ - $ref: '#/components/parameters/excludeAlreadyWatched'
- name: startDate
in: query
description: Get videos that are published after this date
@@ -7655,6 +7681,18 @@ components:
enum:
- 'true'
- 'false'
+ nsfwFlagsIncluded:
+ name: nsfwFlagsIncluded
+ in: query
+ required: false
+ schema:
+ $ref: '#/components/schemas/NSFWFlag'
+ nsfwFlagsExcluded:
+ name: nsfwFlagsExcluded
+ in: query
+ required: false
+ schema:
+ $ref: '#/components/schemas/NSFWFlag'
isLocal:
name: isLocal
in: query
@@ -8127,9 +8165,28 @@ components:
type: string
enum:
- display
- - blur
+ - warn
- do_not_list
+ NSFWFlag:
+ type: integer
+ enum:
+ - 0
+ - 1
+ - 2
+ - 4
+ description: >
+
+ NSFW flags (can be combined using bitwise or operator)
+
+ - `0` NONE
+
+ - `1` VIOLENT
+
+ - `2` SHOCKING_DISTURBING
+
+ - `4` EXPLICIT_SEX
+
UserRole:
type: integer
enum:
@@ -8530,6 +8587,12 @@ components:
type: integer
nsfw:
type: boolean
+ nsfwFlags:
+ allOf:
+ - $ref: '#/components/schemas/NSFWFlag'
+ nsfwSummary:
+ type: string
+ description: "**PeerTube >= 7.2** More information about the sensitive content of the video"
waitTranscoding:
type: boolean
nullable: true
@@ -10043,6 +10106,7 @@ components:
properties:
id:
$ref: '#/components/schemas/id'
+
VideoUploadRequestCommon:
properties:
name:
@@ -10082,6 +10146,10 @@ components:
nsfw:
description: Whether or not this video contains sensitive content
type: boolean
+ nsfwSummary:
+ description: More information about the sensitive content of the video
+ nsfwFlags:
+ $ref: '#/components/schemas/NSFWFlag'
tags:
description: Video tags (maximum 5 tags each between 2 and 30 characters)
type: array
@@ -10258,8 +10326,18 @@ components:
type: boolean
noWelcomeModal:
type: boolean
+
nsfwPolicy:
$ref: '#/components/schemas/NSFWPolicy'
+ nsfwFlagsDisplayed:
+ $ref: '#/components/schemas/NSFWFlag'
+ nsfwFlagsHidden:
+ $ref: '#/components/schemas/NSFWFlag'
+ nsfwFlagsWarned:
+ $ref: '#/components/schemas/NSFWFlag'
+ nsfwFlagsBlurred:
+ $ref: '#/components/schemas/NSFWFlag'
+
role:
type: object
properties:
@@ -10381,13 +10459,21 @@ components:
description: new name of the user in its representations
minLength: 3
maxLength: 120
- displayNSFW:
+ nsfwPolicy:
type: string
description: new NSFW display policy
enum:
- 'true'
- 'false'
- both
+ nsfwFlagsDisplayed:
+ $ref: '#/components/schemas/NSFWFlag'
+ nsfwFlagsHidden:
+ $ref: '#/components/schemas/NSFWFlag'
+ nsfwFlagsWarned:
+ $ref: '#/components/schemas/NSFWFlag'
+ nsfwFlagsBlurred:
+ $ref: '#/components/schemas/NSFWFlag'
p2pEnabled:
type: boolean
description: whether to enable P2P in the player or not