diff --git a/apps/peertube-cli/src/peertube-upload.ts b/apps/peertube-cli/src/peertube-upload.ts index 6a5950883..357870449 100644 --- a/apps/peertube-cli/src/peertube-upload.ts +++ b/apps/peertube-cli/src/peertube-upload.ts @@ -1,9 +1,9 @@ +import { Command } from '@commander-js/extra-typings' +import { VideoCommentPolicy, VideoPrivacy, VideoPrivacyType } from '@peertube/peertube-models' +import { PeerTubeServer } from '@peertube/peertube-server-commands' import { access, constants } from 'fs/promises' import { isAbsolute } from 'path' import { inspect } from 'util' -import { Command } from '@commander-js/extra-typings' -import { VideoPrivacy } from '@peertube/peertube-models' -import { PeerTubeServer } from '@peertube/peertube-server-commands' import { assignToken, buildServer, getServerCredentials, listOptions } from './shared/index.js' type UploadOptions = { @@ -14,13 +14,13 @@ type UploadOptions = { preview?: string file?: string videoName?: string - category?: string - licence?: string + category?: number + licence?: number language?: string - tags?: string + tags?: string[] nsfw?: true videoDescription?: string - privacy?: number + privacy?: VideoPrivacyType channelName?: string noCommentsEnabled?: true support?: string @@ -41,13 +41,13 @@ export function defineUploadProgram () { .option('--preview ', 'Preview path') .option('-f, --file ', 'Video absolute file path') .option('-n, --video-name ', 'Video name') - .option('-c, --category ', 'Category number') - .option('-l, --licence ', 'Licence number') + .option('-c, --category ', 'Category number', parseInt) + .option('-l, --licence ', 'Licence number', parseInt) .option('-L, --language ', 'Language ISO 639 code (fr or en...)') .option('-t, --tags ', 'Video tags', listOptions) .option('-N, --nsfw', 'Video is Not Safe For Work') .option('-d, --video-description ', 'Video description') - .option('-P, --privacy ', 'Privacy', parseInt) + .option('-P, --privacy ', 'Privacy', v => parseInt(v) as VideoPrivacyType) .option('-C, --channel-name ', 'Channel name') .option('--no-comments-enabled', 'Disable video comments') .option('-s, --support ', 'Video support text') @@ -120,10 +120,9 @@ async function run (options: UploadOptions) { } } -async function buildVideoAttributesFromCommander (server: PeerTubeServer, options: UploadOptions, defaultAttributes: any = {}) { +async function buildVideoAttributesFromCommander (server: PeerTubeServer, options: UploadOptions) { const defaultBooleanAttributes = { nsfw: false, - commentsEnabled: true, downloadEnabled: true, waitTranscoding: true } @@ -133,25 +132,29 @@ async function buildVideoAttributesFromCommander (server: PeerTubeServer, option for (const key of Object.keys(defaultBooleanAttributes)) { if (options[key] !== undefined) { booleanAttributes[key] = options[key] - } else if (defaultAttributes[key] !== undefined) { - booleanAttributes[key] = defaultAttributes[key] } else { booleanAttributes[key] = defaultBooleanAttributes[key] } } const videoAttributes = { - name: options.videoName || defaultAttributes.name, - category: options.category || defaultAttributes.category || undefined, - licence: options.licence || defaultAttributes.licence || undefined, - language: options.language || defaultAttributes.language || undefined, - privacy: options.privacy || defaultAttributes.privacy || VideoPrivacy.PUBLIC, - support: options.support || defaultAttributes.support || undefined, - description: options.videoDescription || defaultAttributes.description || undefined, - tags: options.tags || defaultAttributes.tags || undefined - } + name: options.videoName, + category: options.category || undefined, + licence: options.licence || undefined, + language: options.language || undefined, + privacy: options.privacy || VideoPrivacy.PUBLIC, + support: options.support || undefined, + description: options.videoDescription || undefined, + tags: options.tags || undefined, - Object.assign(videoAttributes, booleanAttributes) + commentsPolicy: options.noCommentsEnabled !== undefined + ? options.noCommentsEnabled === true + ? VideoCommentPolicy.DISABLED + : VideoCommentPolicy.ENABLED + : undefined, + + ...booleanAttributes + } if (options.channelName) { const videoChannel = await server.channels.get({ channelName: options.channelName }) diff --git a/apps/peertube-cli/src/shared/cli.ts b/apps/peertube-cli/src/shared/cli.ts index 080eb8237..256f4bcdb 100644 --- a/apps/peertube-cli/src/shared/cli.ts +++ b/apps/peertube-cli/src/shared/cli.ts @@ -120,7 +120,7 @@ function getRemoteObjectOrDie ( return { url, username, password } } -function listOptions (val: any) { +function listOptions (val: string) { return val.split(',') } diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 766a4ec52..cb3b0a262 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts @@ -153,6 +153,14 @@ export class AdminComponent implements OnInit { }) } + if (this.hasServerWatchedWordsRight()) { + moderationItems.children.push({ + label: $localize`Watched words`, + routerLink: '/admin/moderation/watched-words/list', + iconName: 'eye-open' + }) + } + if (moderationItems.children.length !== 0) this.menuEntries.push(moderationItems) } @@ -241,6 +249,10 @@ export class AdminComponent implements OnInit { return this.auth.getUser().hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST) } + private hasServerWatchedWordsRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_INSTANCE_WATCHED_WORDS) + } + private hasConfigRight () { return this.auth.getUser().hasRight(UserRight.MANAGE_CONFIGURATION) } diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts index c642d2a3a..d57a49f7d 100644 --- a/client/src/app/+admin/follows/following-list/follow-modal.component.ts +++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts @@ -1,15 +1,16 @@ +import { NgClass, NgIf } from '@angular/common' import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { Notifier } from '@app/core' import { formatICU } from '@app/helpers' -import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' +import { UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' import { FormReactive } from '@app/shared/shared-forms/form-reactive' import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service' +import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' -import { NgClass, NgIf } from '@angular/common' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { splitAndGetNotEmpty } from '@root-helpers/string' import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component' -import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service' @Component({ selector: 'my-follow-modal', diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index f0494de7b..d354f8dcc 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts @@ -5,6 +5,7 @@ import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list import { UserRightGuard } from '@app/core' import { UserRight } from '@peertube/peertube-models' import { RegistrationListComponent } from './registration-list' +import { WatchedWordsListAdminComponent } from './watched-words-list/watched-words-list-admin.component' export const ModerationRoutes: Routes = [ { @@ -114,6 +115,18 @@ export const ModerationRoutes: Routes = [ title: $localize`Muted instances` } } + }, + + { + path: 'watched-words/list', + component: WatchedWordsListAdminComponent, + canActivate: [ UserRightGuard ], + data: { + userRight: UserRight.MANAGE_INSTANCE_WATCHED_WORDS, + meta: { + title: $localize`Watched words` + } + } } ] } diff --git a/client/src/app/+admin/moderation/watched-words-list/watched-words-list-admin.component.html b/client/src/app/+admin/moderation/watched-words-list/watched-words-list-admin.component.html new file mode 100644 index 000000000..230cd365f --- /dev/null +++ b/client/src/app/+admin/moderation/watched-words-list/watched-words-list-admin.component.html @@ -0,0 +1,10 @@ +

+ + Instance watched words lists +

+ +Video name/description and comments that contain any of the watched words are automatically tagged with the name of the list. +These automatic tags can be used to filter comments and videos. + + + diff --git a/client/src/app/+admin/moderation/watched-words-list/watched-words-list-admin.component.ts b/client/src/app/+admin/moderation/watched-words-list/watched-words-list-admin.component.ts new file mode 100644 index 000000000..2c15c3840 --- /dev/null +++ b/client/src/app/+admin/moderation/watched-words-list/watched-words-list-admin.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core' +import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component' +import { WatchedWordsListAdminOwnerComponent } from '@app/shared/standalone-watched-words/watched-words-list-admin-owner.component' + +@Component({ + templateUrl: './watched-words-list-admin.component.html', + standalone: true, + imports: [ + GlobalIconComponent, + WatchedWordsListAdminOwnerComponent + ] +}) +export class WatchedWordsListAdminComponent { } diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.html b/client/src/app/+admin/overview/comments/video-comment-list.component.html index 183854af7..0b87f0f38 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.html +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html @@ -7,110 +7,5 @@ This view also shows comments from muted accounts. - - -
-
- - -
- -
- - - -
-
-
- - - - - - - - More information - - - Actions - - Account - Video - Comment - Date - - - - - - - - - - - - - - - - - - - - -
- -
- {{ videoComment.account.displayName }} - {{ videoComment.by }} -
-
-
- - - - Commented video - - {{ videoComment.video.name }} - - - -
- - - {{ videoComment.createdAt | date: 'short' }} - -
- - - - -
- - -
- - - - -
- No comments found matching current filters. - No comments found. -
- - -
-
+ diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.scss b/client/src/app/+admin/overview/comments/video-comment-list.component.scss index 2777bf6d1..d7deffa29 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.scss +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.scss @@ -7,54 +7,3 @@ my-feed { display: inline-block; width: 15px; } - -my-global-icon { - width: 24px; - height: 24px; -} - -.video { - display: flex; - flex-direction: column; - - em { - font-size: 11px; - } - - a { - @include ellipsis; - - color: pvar(--mainForegroundColor); - } -} - -.comment-html { - ::ng-deep { - > div { - max-height: 22px; - } - - div, - p { - @include ellipsis; - } - - p { - margin: 0; - } - } -} - -.right-form { - display: flex; - - > *:not(:last-child) { - @include margin-right(10px); - } -} - -@media screen and (max-width: $primeng-breakpoint) { - .video { - align-items: flex-start !important; - } -} diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts index f0fe8880e..5d5e7b89d 100644 --- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts @@ -1,54 +1,22 @@ -import { SortMeta, SharedModule } from 'primeng/api' -import { Component, OnInit } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' -import { FeedFormat, UserRight } from '@peertube/peertube-models' -import { formatICU } from '@app/helpers' -import { AutoColspanDirective } from '../../../shared/shared-main/angular/auto-colspan.directive' -import { ActorAvatarComponent } from '../../../shared/shared-actor-image/actor-avatar.component' -import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component' -import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap' -import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component' -import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../../shared/shared-forms/advanced-input-filter.component' -import { ActionDropdownComponent, DropdownAction } from '../../../shared/shared-main/buttons/action-dropdown.component' -import { NgIf, NgClass, DatePipe } from '@angular/common' -import { TableModule } from 'primeng/table' -import { FeedComponent } from '../../../shared/shared-main/feeds/feed.component' -import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component' -import { VideoCommentAdmin } from '@app/shared/shared-video-comment/video-comment.model' -import { BulkService } from '@app/shared/shared-moderation/bulk.service' +import { Component } from '@angular/core' import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service' +import { FeedFormat } from '@peertube/peertube-models' +import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component' +import { FeedComponent } from '../../../shared/shared-main/feeds/feed.component' +import { VideoCommentListAdminOwnerComponent } from '../../../shared/shared-video-comment/video-comment-list-admin-owner.component' @Component({ selector: 'my-video-comment-list', templateUrl: './video-comment-list.component.html', - styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ], + styleUrls: [ './video-comment-list.component.scss' ], standalone: true, imports: [ GlobalIconComponent, FeedComponent, - TableModule, - SharedModule, - NgIf, - ActionDropdownComponent, - AdvancedInputFilterComponent, - ButtonComponent, - NgbTooltip, - TableExpanderIconComponent, - NgClass, - ActorAvatarComponent, - AutoColspanDirective, - DatePipe + VideoCommentListAdminOwnerComponent ] }) -export class VideoCommentListComponent extends RestTable implements OnInit { - comments: VideoCommentAdmin[] - totalRecords = 0 - sort: SortMeta = { field: 'createdAt', order: -1 } - pagination: RestPagination = { count: this.rowsPerPage, start: 0 } - - videoCommentActions: DropdownAction[][] = [] - +export class VideoCommentListComponent { syndicationItems = [ { format: FeedFormat.RSS, @@ -66,154 +34,4 @@ export class VideoCommentListComponent extends RestTable imp url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase() } ] - - bulkActions: DropdownAction[] = [] - - inputFilters: AdvancedInputFilter[] = [ - { - title: $localize`Advanced filters`, - children: [ - { - value: 'local:true', - label: $localize`Local comments` - }, - { - value: 'local:false', - label: $localize`Remote comments` - }, - { - value: 'localVideo:true', - label: $localize`Comments on local videos` - } - ] - } - ] - - get authUser () { - return this.auth.getUser() - } - - constructor ( - protected router: Router, - protected route: ActivatedRoute, - private auth: AuthService, - private notifier: Notifier, - private confirmService: ConfirmService, - private videoCommentService: VideoCommentService, - private markdownRenderer: MarkdownService, - private bulkService: BulkService - ) { - super() - - this.videoCommentActions = [ - [ - { - label: $localize`Delete this comment`, - handler: comment => this.deleteComment(comment), - isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) - }, - - { - label: $localize`Delete all comments of this account`, - description: $localize`Comments are deleted after a few minutes`, - handler: comment => this.deleteUserComments(comment), - isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) - } - ] - ] - } - - ngOnInit () { - this.initialize() - - this.bulkActions = [ - { - label: $localize`Delete`, - handler: comments => this.removeComments(comments), - isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT), - iconName: 'delete' - } - ] - } - - getIdentifier () { - return 'VideoCommentListComponent' - } - - toHtml (text: string) { - return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true }) - } - - protected reloadDataInternal () { - this.videoCommentService.getAdminVideoComments({ - pagination: this.pagination, - sort: this.sort, - search: this.search - }).subscribe({ - next: async resultList => { - this.totalRecords = resultList.total - - this.comments = [] - - for (const c of resultList.data) { - this.comments.push( - new VideoCommentAdmin(c, await this.toHtml(c.text)) - ) - } - }, - - error: err => this.notifier.error(err.message) - }) - } - - private removeComments (comments: VideoCommentAdmin[]) { - const commentArgs = comments.map(c => ({ videoId: c.video.id, commentId: c.id })) - - this.videoCommentService.deleteVideoComments(commentArgs) - .subscribe({ - next: () => { - this.notifier.success( - formatICU( - $localize`{count, plural, =1 {1 comment deleted.} other {{count} comments deleted.}}`, - { count: commentArgs.length } - ) - ) - - this.reloadData() - }, - - error: err => this.notifier.error(err.message), - - complete: () => this.selectedRows = [] - }) - } - - private deleteComment (comment: VideoCommentAdmin) { - this.videoCommentService.deleteVideoComment(comment.video.id, comment.id) - .subscribe({ - next: () => this.reloadData(), - - error: err => this.notifier.error(err.message) - }) - } - - private async deleteUserComments (comment: VideoCommentAdmin) { - const message = $localize`Do you really want to delete all comments of ${comment.by}?` - const res = await this.confirmService.confirm(message, $localize`Delete`) - if (res === false) return - - const options = { - accountName: comment.by, - scope: 'instance' as 'instance' - } - - this.bulkService.removeCommentsOf(options) - .subscribe({ - next: () => { - this.notifier.success($localize`Comments of ${options.accountName} will be deleted in a few minutes`) - }, - - error: err => this.notifier.error(err.message) - }) - } } diff --git a/client/src/app/+admin/overview/videos/video-admin.service.ts b/client/src/app/+admin/overview/videos/video-admin.service.ts index 6a45eb201..8ab91be96 100644 --- a/client/src/app/+admin/overview/videos/video-admin.service.ts +++ b/client/src/app/+admin/overview/videos/video-admin.service.ts @@ -113,7 +113,8 @@ export class VideoAdminService { VideoInclude.BLOCKED_OWNER | VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.FILES | - VideoInclude.SOURCE + VideoInclude.SOURCE | + VideoInclude.AUTOMATIC_TAGS let privacyOneOf = getAllPrivacies() @@ -143,6 +144,10 @@ export class VideoAdminService { excludePublic: { prefix: 'excludePublic', handler: () => true + }, + autoTagOneOf: { + prefix: 'autoTag:', + multiple: true } }) 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 0bdccbab8..1dd0953f8 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.html +++ b/client/src/app/+admin/overview/videos/video-list.component.html @@ -70,22 +70,34 @@ - @if (video.isLocal) { - Local - } @else { - Remote - } +
+ @if (video.isLocal) { + Local + } @else { + Remote + } - {{ video.privacy.label }} + {{ video.privacy.label }} - NSFW + NSFW - {{ video.state.label }} + {{ video.state.label }} - Account muted - Server muted + Account muted + Server muted - Blocked + Blocked +
+ +
+ @for (tag of video.automaticTags; track tag) { + {{ tag }} + } +
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 30868499d..3e6325e32 100644 --- a/client/src/app/+admin/overview/videos/video-list.component.ts +++ b/client/src/app/+admin/overview/videos/video-list.component.ts @@ -1,6 +1,6 @@ import { DatePipe, NgClass, NgFor, NgIf } from '@angular/common' import { Component, OnInit, ViewChild } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' +import { ActivatedRoute, Router, RouterLink } from '@angular/router' import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' import { formatICU, getAbsoluteAPIUrl } from '@app/helpers' import { Video } from '@app/shared/shared-main/video/video.model' @@ -51,6 +51,7 @@ import { VideoAdminService } from './video-admin.service' EmbedComponent, VideoBlockComponent, DatePipe, + RouterLink, BytesPipe ] }) @@ -256,6 +257,14 @@ export class VideoListComponent extends RestTable