mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Implement auto tag on comments and videos
* Comments and videos can be automatically tagged using core rules or watched word lists * These tags can be used to automatically filter videos and comments * Introduce a new video comment policy where comments must be approved first * Comments may have to be approved if the user auto block them using core rules or watched word lists * Implement FEP-5624 to federate reply control policies
This commit is contained in:
parent
b3e39df59e
commit
29329d6c45
241 changed files with 8090 additions and 1399 deletions
|
@ -0,0 +1,68 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
AutomaticTagAvailable,
|
||||
CommentAutomaticTagPolicies,
|
||||
CommentAutomaticTagPoliciesUpdate,
|
||||
HttpStatusCode
|
||||
} from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class AutomaticTagsCommand extends AbstractCommand {
|
||||
|
||||
getCommentPolicies (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
}) {
|
||||
const path = '/api/v1/automatic-tags/policies/accounts/' + options.accountName + '/comments'
|
||||
|
||||
return this.getRequestBody<CommentAutomaticTagPolicies>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateCommentPolicies (options: OverrideCommandOptions & CommentAutomaticTagPoliciesUpdate & {
|
||||
accountName: string
|
||||
}) {
|
||||
const path = '/api/v1/automatic-tags/policies/accounts/' + options.accountName + '/comments'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'review' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getAccountAvailable (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
}) {
|
||||
const path = '/api/v1/automatic-tags/accounts/' + options.accountName + '/available'
|
||||
|
||||
return this.getRequestBody<AutomaticTagAvailable>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getServerAvailable (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/automatic-tags/server/available'
|
||||
|
||||
return this.getRequestBody<AutomaticTagAvailable>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
export * from './abuses-command.js'
|
||||
export * from './automatic-tags-command.js'
|
||||
export * from './watched-words-command.js'
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
ResultList, WatchedWordsList
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class WatchedWordsCommand extends AbstractCommand {
|
||||
|
||||
listWordsLists (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
|
||||
accountName?: string
|
||||
}) {
|
||||
const query = {
|
||||
sort: '-createdAt',
|
||||
|
||||
...pick(options, [ 'start', 'count', 'sort' ])
|
||||
}
|
||||
|
||||
return this.getRequestBody<ResultList<WatchedWordsList>>({
|
||||
...options,
|
||||
|
||||
path: this.buildAPIBasePath(options.accountName),
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
createList (options: OverrideCommandOptions & {
|
||||
listName: string
|
||||
words: string[]
|
||||
accountName?: string
|
||||
}) {
|
||||
const body = pick(options, [ 'listName', 'words' ])
|
||||
|
||||
return unwrapBody<{ watchedWordsList: { id: number } }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: this.buildAPIBasePath(options.accountName),
|
||||
fields: body,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
updateList (options: OverrideCommandOptions & {
|
||||
listId: number
|
||||
accountName?: string
|
||||
listName?: string
|
||||
words?: string[]
|
||||
}) {
|
||||
const body = pick(options, [ 'listName', 'words' ])
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path: this.buildAPIBasePath(options.accountName) + '/' + options.listId,
|
||||
fields: body,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
deleteList (options: OverrideCommandOptions & {
|
||||
listId: number
|
||||
accountName?: string
|
||||
}) {
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path: this.buildAPIBasePath(options.accountName) + '/' + options.listId,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
private buildAPIBasePath (accountName?: string) {
|
||||
return accountName
|
||||
? '/api/v1/watched-words/accounts/' + accountName + '/lists'
|
||||
: '/api/v1/watched-words/server/lists'
|
||||
}
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
import { ChildProcess, fork } from 'child_process'
|
||||
import { copy } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { randomInt } from '@peertube/peertube-core-utils'
|
||||
import { Video, VideoChannel, VideoChannelSync, VideoCreateResult, VideoDetails } from '@peertube/peertube-models'
|
||||
import { parallelTests, root } from '@peertube/peertube-node-utils'
|
||||
import { ChildProcess, fork } from 'child_process'
|
||||
import { copy } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { BulkCommand } from '../bulk/index.js'
|
||||
import { CLICommand } from '../cli/index.js'
|
||||
import { CustomPagesCommand } from '../custom-pages/index.js'
|
||||
import { FeedCommand } from '../feeds/index.js'
|
||||
import { LogsCommand } from '../logs/index.js'
|
||||
import { AbusesCommand } from '../moderation/index.js'
|
||||
import { AbusesCommand, AutomaticTagsCommand, WatchedWordsCommand } from '../moderation/index.js'
|
||||
import { OverviewsCommand } from '../overviews/index.js'
|
||||
import { RunnerJobsCommand, RunnerRegistrationTokensCommand, RunnersCommand } from '../runners/index.js'
|
||||
import { SearchCommand } from '../search/index.js'
|
||||
|
@ -17,35 +17,35 @@ import { SocketIOCommand } from '../socket/index.js'
|
|||
import {
|
||||
AccountsCommand,
|
||||
BlocklistCommand,
|
||||
UserExportsCommand,
|
||||
LoginCommand,
|
||||
NotificationsCommand,
|
||||
RegistrationsCommand,
|
||||
SubscriptionsCommand,
|
||||
TwoFactorCommand,
|
||||
UsersCommand,
|
||||
UserImportsCommand
|
||||
UserExportsCommand,
|
||||
UserImportsCommand,
|
||||
UsersCommand
|
||||
} from '../users/index.js'
|
||||
import {
|
||||
BlacklistCommand,
|
||||
CaptionsCommand,
|
||||
ChangeOwnershipCommand,
|
||||
ChannelsCommand,
|
||||
ChannelSyncsCommand,
|
||||
ChannelsCommand,
|
||||
ChaptersCommand,
|
||||
CommentsCommand,
|
||||
HistoryCommand,
|
||||
VideoImportsCommand,
|
||||
LiveCommand,
|
||||
PlaylistsCommand,
|
||||
ServicesCommand,
|
||||
StoryboardCommand,
|
||||
StreamingPlaylistsCommand,
|
||||
VideoImportsCommand,
|
||||
VideoPasswordsCommand,
|
||||
VideosCommand,
|
||||
VideoStatsCommand,
|
||||
VideoStudioCommand,
|
||||
VideoTokenCommand,
|
||||
VideosCommand,
|
||||
ViewsCommand
|
||||
} from '../videos/index.js'
|
||||
import { ConfigCommand } from './config-command.js'
|
||||
|
@ -163,6 +163,9 @@ export class PeerTubeServer {
|
|||
runnerRegistrationTokens?: RunnerRegistrationTokensCommand
|
||||
runnerJobs?: RunnerJobsCommand
|
||||
|
||||
watchedWordsLists?: WatchedWordsCommand
|
||||
autoTags?: AutomaticTagsCommand
|
||||
|
||||
constructor (options: { serverNumber: number } | { url: string }) {
|
||||
if ((options as any).url) {
|
||||
this.setUrl((options as any).url)
|
||||
|
@ -458,5 +461,8 @@ export class PeerTubeServer {
|
|||
this.runnerRegistrationTokens = new RunnerRegistrationTokensCommand(this)
|
||||
this.runnerJobs = new RunnerJobsCommand(this)
|
||||
this.videoPasswords = new VideoPasswordsCommand(this)
|
||||
|
||||
this.watchedWordsLists = new WatchedWordsCommand(this)
|
||||
this.autoTags = new AutomaticTagsCommand(this)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,45 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@peertube/peertube-models'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
ResultList,
|
||||
VideoComment,
|
||||
VideoCommentForAdminOrUser,
|
||||
VideoCommentThreads,
|
||||
VideoCommentThreadTree
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
type ListForAdminOrAccountCommonOptions = {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
searchAccount?: string
|
||||
searchVideo?: string
|
||||
videoId?: string | number
|
||||
videoChannelId?: string | number
|
||||
autoTagOneOf?: string[]
|
||||
}
|
||||
|
||||
export class CommentsCommand extends AbstractCommand {
|
||||
|
||||
private lastVideoId: number | string
|
||||
private lastThreadId: number
|
||||
private lastReplyId: number
|
||||
|
||||
listForAdmin (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
listForAdmin (options: OverrideCommandOptions & ListForAdminOrAccountCommonOptions & {
|
||||
isLocal?: boolean
|
||||
onLocalVideo?: boolean
|
||||
search?: string
|
||||
searchAccount?: string
|
||||
searchVideo?: string
|
||||
} = {}) {
|
||||
const { sort = '-createdAt' } = options
|
||||
const path = '/api/v1/videos/comments'
|
||||
|
||||
const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'onLocalVideo', 'search', 'searchAccount', 'searchVideo' ]) }
|
||||
const query = {
|
||||
...this.buildListForAdminOrAccountQuery(options),
|
||||
...pick(options, [ 'isLocal', 'onLocalVideo' ])
|
||||
}
|
||||
|
||||
return this.getRequestBody<ResultList<VideoComment>>({
|
||||
return this.getRequestBody<ResultList<VideoCommentForAdminOrUser>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
|
@ -34,6 +49,35 @@ export class CommentsCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
listCommentsOnMyVideos (options: OverrideCommandOptions & ListForAdminOrAccountCommonOptions & {
|
||||
isHeldForReview?: boolean
|
||||
} = {}) {
|
||||
const path = '/api/v1/users/me/videos/comments'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoCommentForAdminOrUser>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
...this.buildListForAdminOrAccountQuery(options),
|
||||
|
||||
isHeldForReview: options.isHeldForReview
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
private buildListForAdminOrAccountQuery (options: ListForAdminOrAccountCommonOptions) {
|
||||
return {
|
||||
sort: '-createdAt',
|
||||
|
||||
...pick(options, [ 'start', 'count', 'search', 'searchAccount', 'searchVideo', 'sort', 'videoId', 'videoChannelId', 'autoTagOneOf' ])
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
listThreads (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
videoPassword?: string
|
||||
|
@ -71,6 +115,16 @@ export class CommentsCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
async getThreadOf (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
}) {
|
||||
const { videoId, text } = options
|
||||
const threadId = await this.findCommentId({ videoId, text })
|
||||
|
||||
return this.getThread({ ...options, videoId, threadId })
|
||||
}
|
||||
|
||||
async createThread (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
|
@ -136,11 +190,13 @@ export class CommentsCommand extends AbstractCommand {
|
|||
text: string
|
||||
}) {
|
||||
const { videoId, text } = options
|
||||
const { data } = await this.listThreads({ videoId, count: 25, sort: '-createdAt' })
|
||||
const { data } = await this.listForAdmin({ videoId, count: 25, sort: '-createdAt' })
|
||||
|
||||
return data.find(c => c.text === text).id
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
commentId: number
|
||||
|
@ -156,4 +212,34 @@ export class CommentsCommand extends AbstractCommand {
|
|||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async deleteAllComments (options: OverrideCommandOptions & {
|
||||
videoUUID: string
|
||||
}) {
|
||||
const { data } = await this.listForAdmin({ ...options, start: 0, count: 20 })
|
||||
|
||||
for (const comment of data) {
|
||||
if (comment?.video.uuid !== options.videoUUID) continue
|
||||
|
||||
await this.delete({ videoId: options.videoUUID, commentId: comment.id, ...options })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
approve (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
commentId: number
|
||||
}) {
|
||||
const { videoId, commentId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId + '/approve'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
HttpStatusCodeType, ResultList,
|
||||
UserVideoRateType,
|
||||
Video,
|
||||
VideoCommentPolicy,
|
||||
VideoCreate,
|
||||
VideoCreateResult,
|
||||
VideoDetails,
|
||||
|
@ -229,6 +230,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
search?: string
|
||||
isLive?: boolean
|
||||
channelId?: number
|
||||
autoTagOneOf?: string[]
|
||||
} = {}) {
|
||||
const path = '/api/v1/users/me/videos'
|
||||
|
||||
|
@ -236,7 +238,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId', 'autoTagOneOf' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
@ -282,7 +284,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
}
|
||||
|
||||
listAllForAdmin (options: OverrideCommandOptions & VideosCommonQuery = {}) {
|
||||
const include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER
|
||||
const include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER | VideoInclude.AUTOMATIC_TAGS
|
||||
const nsfw = 'both'
|
||||
const privacyOneOf = getAllPrivacies()
|
||||
|
||||
|
@ -429,7 +431,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
support: 'my super support text',
|
||||
tags: [ 'tag' ],
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
commentsEnabled: true,
|
||||
commentsPolicy: VideoCommentPolicy.ENABLED,
|
||||
downloadEnabled: true,
|
||||
fixture: 'video_short.webm',
|
||||
|
||||
|
@ -619,7 +621,8 @@ export class VideosCommand extends AbstractCommand {
|
|||
'tagsAllOf',
|
||||
'isLocal',
|
||||
'include',
|
||||
'skipCount'
|
||||
'skipCount',
|
||||
'autoTagOneOf'
|
||||
])
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue