1
0
Fork 0
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:
Chocobozzz 2024-03-29 14:25:03 +01:00 committed by Chocobozzz
parent b3e39df59e
commit 29329d6c45
241 changed files with 8090 additions and 1399 deletions

View file

@ -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
})
}
}

View file

@ -1 +1,3 @@
export * from './abuses-command.js'
export * from './automatic-tags-command.js'
export * from './watched-words-command.js'

View file

@ -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'
}
}

View file

@ -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)
}
}

View file

@ -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
})
}
}

View file

@ -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'
])
}