From: Chocobozzz Date: Wed, 28 Sep 2022 09:19:25 +0000 (+0200) Subject: Merge branch 'release/4.3.0' into develop X-Git-Tag: v5.0.0-rc.1~307 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=0d6843decdaecb4f726cba27fdb55fc164d00ba7;hp=1593e0dd5c822d7f8d6b87048db5de4184c4f93c;p=github%2FChocobozzz%2FPeerTube.git Merge branch 'release/4.3.0' into develop --- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae19615c5..1c2f8093a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,6 +75,8 @@ jobs: - name: Run Test # external-plugins tests only run on schedule if: github.event_name == 'schedule' || matrix.test_suite != 'external-plugins' + env: + AKISMET_KEY: ${{ secrets.AKISMET_KEY }} run: npm run ci -- ${{ matrix.test_suite }} - name: Display errors diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index 62b1c4446..366fbd459 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts @@ -98,7 +98,7 @@ export class SearchComponent implements OnInit, OnDestroy { this.search() }, - error: err => this.notifier.error(err.text) + error: err => this.notifier.error(err.message) }) this.userService.getAnonymousOrLoggedUser() diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts index fd3614297..9f4a68736 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts @@ -148,7 +148,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges, error: err => { this.addingComment = false - this.notifier.error(err.text) + this.notifier.error(err.message) } }) } diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index ece6bc5d1..ca46866f5 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -97,7 +97,7 @@ export class AuthService { let errorMessage = err.message if (err.status === HttpStatusCode.FORBIDDEN_403) { - errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${err.text}. + errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${err.message}. Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.` } diff --git a/config/default.yaml b/config/default.yaml index 0b0a54eef..3a0b494fb 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -638,7 +638,8 @@ instance: robots: | User-agent: * Disallow: - # Security.txt rules. To discourage researchers from testing your instance and disable security.txt integration, set this to an empty string + # /.well-known/security.txt rules. This endpoint is cached, so you may have to wait a few hours before viewing your changes + # To discourage researchers from testing your instance and disable security.txt integration, set this to an empty string securitytxt: '# If you would like to report a security issue\n# you may report it to:\nContact: https://github.com/Chocobozzz/PeerTube/blob/develop/SECURITY.md\nContact: mailto:' diff --git a/config/production.yaml.example b/config/production.yaml.example index 209aaa56a..dafc15915 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -648,7 +648,8 @@ instance: robots: | User-agent: * Disallow: - # Security.txt rules. To discourage researchers from testing your instance and disable security.txt integration, set this to an empty string + # /.well-known/security.txt rules. This endpoint is cached, so you may have to wait a few hours before viewing your changes + # To discourage researchers from testing your instance and disable security.txt integration, set this to an empty string securitytxt: '# If you would like to report a security issue\n# you may report it to:\nContact: https://github.com/Chocobozzz/PeerTube/blob/develop/SECURITY.md\nContact: mailto:' diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 76ed37aae..1e6e8956c 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -109,8 +109,10 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc let video: MVideoAccountLightBlacklistAllFiles let created: boolean let comment: MCommentOwnerVideo + try { const resolveThreadResult = await resolveThread({ url: commentObject.id, isVideo: false }) + if (!resolveThreadResult) return // Comment not accepted video = resolveThreadResult.video created = resolveThreadResult.commentCreated diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index 911c7cd30..b65baf0e9 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts @@ -4,7 +4,9 @@ import { logger } from '../../helpers/logger' import { doJSONRequest } from '../../helpers/requests' import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' import { VideoCommentModel } from '../../models/video/video-comment' -import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' +import { MComment, MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' +import { isRemoteVideoCommentAccepted } from '../moderation' +import { Hooks } from '../plugins/hooks' import { getOrCreateAPActor } from './actors' import { checkUrlsSameHost } from './url' import { getOrCreateAPVideo } from './videos' @@ -103,6 +105,10 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) { firstReply.changed('updatedAt', true) firstReply.Video = video + if (await isRemoteCommentAccepted(firstReply) !== true) { + return undefined + } + comments[comments.length - 1] = await firstReply.save() for (let i = comments.length - 2; i >= 0; i--) { @@ -113,6 +119,10 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) { comment.changed('updatedAt', true) comment.Video = video + if (await isRemoteCommentAccepted(comment) !== true) { + return undefined + } + comments[i] = await comment.save() } @@ -169,3 +179,26 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) { commentCreated: true }) } + +async function isRemoteCommentAccepted (comment: MComment) { + // Already created + if (comment.id) return true + + const acceptParameters = { + comment + } + + const acceptedResult = await Hooks.wrapFun( + isRemoteVideoCommentAccepted, + acceptParameters, + 'filter:activity-pub.remote-video-comment.create.accept.result' + ) + + if (!acceptedResult || acceptedResult.accepted !== true) { + logger.info('Refused to create a remote comment.', { acceptedResult, acceptParameters }) + + return false + } + + return true +} diff --git a/server/lib/job-queue/handlers/video-channel-import.ts b/server/lib/job-queue/handlers/video-channel-import.ts index 600292844..c3dd8a688 100644 --- a/server/lib/job-queue/handlers/video-channel-import.ts +++ b/server/lib/job-queue/handlers/video-channel-import.ts @@ -5,7 +5,7 @@ import { synchronizeChannel } from '@server/lib/sync-channel' import { VideoChannelModel } from '@server/models/video/video-channel' import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' import { MChannelSync } from '@server/types/models' -import { VideoChannelImportPayload, VideoChannelSyncState } from '@shared/models' +import { VideoChannelImportPayload } from '@shared/models' export async function processVideoChannelImport (job: Job) { const payload = job.data as VideoChannelImportPayload @@ -32,17 +32,11 @@ export async function processVideoChannelImport (job: Job) { const videoChannel = await VideoChannelModel.loadAndPopulateAccount(payload.videoChannelId) - try { - logger.info(`Starting importing videos from external channel "${payload.externalChannelUrl}" to "${videoChannel.name}" `) - - await synchronizeChannel({ - channel: videoChannel, - externalChannelUrl: payload.externalChannelUrl, - channelSync - }) - } catch (err) { - logger.error(`Failed to import channel ${videoChannel.name}`, { err }) - channelSync.state = VideoChannelSyncState.FAILED - await channelSync.save() - } + logger.info(`Starting importing videos from external channel "${payload.externalChannelUrl}" to "${videoChannel.name}" `) + + await synchronizeChannel({ + channel: videoChannel, + externalChannelUrl: payload.externalChannelUrl, + channelSync + }) } diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts index c23f5b6a6..3cc92ca30 100644 --- a/server/lib/moderation.ts +++ b/server/lib/moderation.ts @@ -1,4 +1,4 @@ -import { VideoUploadFile } from 'express' +import express, { VideoUploadFile } from 'express' import { PathLike } from 'fs-extra' import { Transaction } from 'sequelize/types' import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger' @@ -13,18 +13,15 @@ import { MAbuseFull, MAccountDefault, MAccountLight, + MComment, MCommentAbuseAccountVideo, MCommentOwnerVideo, MUser, MVideoAbuseVideoFull, MVideoAccountLightBlacklistAllFiles } from '@server/types/models' -import { ActivityCreate } from '../../shared/models/activitypub' -import { VideoObject } from '../../shared/models/activitypub/objects' -import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' import { VideoCommentCreate } from '../../shared/models/videos/comment' -import { ActorModel } from '../models/actor/actor' import { UserModel } from '../models/user/user' import { VideoModel } from '../models/video/video' import { VideoCommentModel } from '../models/video/video-comment' @@ -36,7 +33,9 @@ export type AcceptResult = { errorMessage?: string } -// Can be filtered by plugins +// --------------------------------------------------------------------------- + +// Stub function that can be filtered by plugins function isLocalVideoAccepted (object: { videoBody: VideoCreate videoFile: VideoUploadFile @@ -45,6 +44,9 @@ function isLocalVideoAccepted (object: { return { accepted: true } } +// --------------------------------------------------------------------------- + +// Stub function that can be filtered by plugins function isLocalLiveVideoAccepted (object: { liveVideoBody: LiveVideoCreate user: UserModel @@ -52,7 +54,11 @@ function isLocalLiveVideoAccepted (object: { return { accepted: true } } +// --------------------------------------------------------------------------- + +// Stub function that can be filtered by plugins function isLocalVideoThreadAccepted (_object: { + req: express.Request commentBody: VideoCommentCreate video: VideoModel user: UserModel @@ -60,7 +66,9 @@ function isLocalVideoThreadAccepted (_object: { return { accepted: true } } +// Stub function that can be filtered by plugins function isLocalVideoCommentReplyAccepted (_object: { + req: express.Request commentBody: VideoCommentCreate parentComment: VideoCommentModel video: VideoModel @@ -69,22 +77,18 @@ function isLocalVideoCommentReplyAccepted (_object: { return { accepted: true } } -function isRemoteVideoAccepted (_object: { - activity: ActivityCreate - videoAP: VideoObject - byActor: ActorModel -}): AcceptResult { - return { accepted: true } -} +// --------------------------------------------------------------------------- +// Stub function that can be filtered by plugins function isRemoteVideoCommentAccepted (_object: { - activity: ActivityCreate - commentAP: VideoCommentObject - byActor: ActorModel + comment: MComment }): AcceptResult { return { accepted: true } } +// --------------------------------------------------------------------------- + +// Stub function that can be filtered by plugins function isPreImportVideoAccepted (object: { videoImportBody: VideoImportCreate user: MUser @@ -92,6 +96,7 @@ function isPreImportVideoAccepted (object: { return { accepted: true } } +// Stub function that can be filtered by plugins function isPostImportVideoAccepted (object: { videoFilePath: PathLike videoFile: VideoFileModel @@ -100,6 +105,8 @@ function isPostImportVideoAccepted (object: { return { accepted: true } } +// --------------------------------------------------------------------------- + async function createVideoAbuse (options: { baseAbuse: FilteredModelAttributes videoInstance: MVideoAccountLightBlacklistAllFiles @@ -189,12 +196,13 @@ function createAccountAbuse (options: { }) } +// --------------------------------------------------------------------------- + export { isLocalLiveVideoAccepted, isLocalVideoAccepted, isLocalVideoThreadAccepted, - isRemoteVideoAccepted, isRemoteVideoCommentAccepted, isLocalVideoCommentReplyAccepted, isPreImportVideoAccepted, diff --git a/server/lib/schedulers/video-channel-sync-latest-scheduler.ts b/server/lib/schedulers/video-channel-sync-latest-scheduler.ts index a527f68b5..efb957fac 100644 --- a/server/lib/schedulers/video-channel-sync-latest-scheduler.ts +++ b/server/lib/schedulers/video-channel-sync-latest-scheduler.ts @@ -2,7 +2,6 @@ import { logger } from '@server/helpers/logger' import { CONFIG } from '@server/initializers/config' import { VideoChannelModel } from '@server/models/video/video-channel' import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' -import { VideoChannelSyncState } from '@shared/models' import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' import { synchronizeChannel } from '../sync-channel' import { AbstractScheduler } from './abstract-scheduler' @@ -28,26 +27,20 @@ export class VideoChannelSyncLatestScheduler extends AbstractScheduler { for (const sync of channelSyncs) { const channel = await VideoChannelModel.loadAndPopulateAccount(sync.videoChannelId) - try { - logger.info( - 'Creating video import jobs for "%s" sync with external channel "%s"', - channel.Actor.preferredUsername, sync.externalChannelUrl - ) - - const onlyAfter = sync.lastSyncAt || sync.createdAt - - await synchronizeChannel({ - channel, - externalChannelUrl: sync.externalChannelUrl, - videosCountLimit: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.VIDEOS_LIMIT_PER_SYNCHRONIZATION, - channelSync: sync, - onlyAfter - }) - } catch (err) { - logger.error(`Failed to synchronize channel ${channel.Actor.preferredUsername}`, { err }) - sync.state = VideoChannelSyncState.FAILED - await sync.save() - } + logger.info( + 'Creating video import jobs for "%s" sync with external channel "%s"', + channel.Actor.preferredUsername, sync.externalChannelUrl + ) + + const onlyAfter = sync.lastSyncAt || sync.createdAt + + await synchronizeChannel({ + channel, + externalChannelUrl: sync.externalChannelUrl, + videosCountLimit: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.VIDEOS_LIMIT_PER_SYNCHRONIZATION, + channelSync: sync, + onlyAfter + }) } } diff --git a/server/lib/sync-channel.ts b/server/lib/sync-channel.ts index f91599c14..35af91429 100644 --- a/server/lib/sync-channel.ts +++ b/server/lib/sync-channel.ts @@ -24,56 +24,62 @@ export async function synchronizeChannel (options: { await channelSync.save() } - const user = await UserModel.loadByChannelActorId(channel.actorId) - const youtubeDL = new YoutubeDLWrapper( - externalChannelUrl, - ServerConfigManager.Instance.getEnabledResolutions('vod'), - CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION - ) - - const targetUrls = await youtubeDL.getInfoForListImport({ latestVideosCount: videosCountLimit }) - - logger.info( - 'Fetched %d candidate URLs for sync channel %s.', - targetUrls.length, channel.Actor.preferredUsername, { targetUrls } - ) - - if (targetUrls.length === 0) { - if (channelSync) { - channelSync.state = VideoChannelSyncState.SYNCED - await channelSync.save() - } - - return - } + try { + const user = await UserModel.loadByChannelActorId(channel.actorId) + const youtubeDL = new YoutubeDLWrapper( + externalChannelUrl, + ServerConfigManager.Instance.getEnabledResolutions('vod'), + CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION + ) - const children: CreateJobArgument[] = [] + const targetUrls = await youtubeDL.getInfoForListImport({ latestVideosCount: videosCountLimit }) - for (const targetUrl of targetUrls) { - if (await skipImport(channel, targetUrl, onlyAfter)) continue + logger.info( + 'Fetched %d candidate URLs for sync channel %s.', + targetUrls.length, channel.Actor.preferredUsername, { targetUrls } + ) - const { job } = await buildYoutubeDLImport({ - user, - channel, - targetUrl, - channelSync, - importDataOverride: { - privacy: VideoPrivacy.PUBLIC + if (targetUrls.length === 0) { + if (channelSync) { + channelSync.state = VideoChannelSyncState.SYNCED + await channelSync.save() } - }) - children.push(job) - } + return + } + + const children: CreateJobArgument[] = [] + + for (const targetUrl of targetUrls) { + if (await skipImport(channel, targetUrl, onlyAfter)) continue - // Will update the channel sync status - const parent: CreateJobArgument = { - type: 'after-video-channel-import', - payload: { - channelSyncId: channelSync?.id + const { job } = await buildYoutubeDLImport({ + user, + channel, + targetUrl, + channelSync, + importDataOverride: { + privacy: VideoPrivacy.PUBLIC + } + }) + + children.push(job) } - } - await JobQueue.Instance.createJobWithChildren(parent, children) + // Will update the channel sync status + const parent: CreateJobArgument = { + type: 'after-video-channel-import', + payload: { + channelSyncId: channelSync?.id + } + } + + await JobQueue.Instance.createJobWithChildren(parent, children) + } catch (err) { + logger.error(`Failed to import channel ${channel.name}`, { err }) + channelSync.state = VideoChannelSyncState.FAILED + await channelSync.save() + } } // --------------------------------------------------------------------------- diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 69062701b..133feb7bd 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts @@ -208,7 +208,8 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon const acceptParameters = { video, commentBody: req.body, - user: res.locals.oauth.token.User + user: res.locals.oauth.token.User, + req } let acceptedResult: AcceptResult @@ -234,7 +235,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon res.fail({ status: HttpStatusCode.FORBIDDEN_403, - message: acceptedResult?.errorMessage || 'Refused local comment' + message: acceptedResult?.errorMessage || 'Comment has been rejected.' }) return false } diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts index d8a7d576e..fc953f144 100644 --- a/server/tests/api/notifications/moderation-notifications.ts +++ b/server/tests/api/notifications/moderation-notifications.ts @@ -382,7 +382,7 @@ describe('Test moderation notifications', function () { }) it('Should send a notification only to admin when there is a new instance follower', async function () { - this.timeout(20000) + this.timeout(60000) await servers[2].follows.follow({ hosts: [ servers[0].url ] }) diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index d47807a79..2ad749fd4 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts @@ -156,7 +156,7 @@ describe('Test multiple servers', function () { }) it('Should upload the video on server 2 and propagate on each server', async function () { - this.timeout(100000) + this.timeout(240000) const user = { username: 'user1', diff --git a/server/tests/api/videos/video-files.ts b/server/tests/api/videos/video-files.ts index 10277b9cf..c0b886aad 100644 --- a/server/tests/api/videos/video-files.ts +++ b/server/tests/api/videos/video-files.ts @@ -33,7 +33,7 @@ describe('Test videos files', function () { let validId2: string before(async function () { - this.timeout(120_000) + this.timeout(360_000) { const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index 47b8c7b1e..a0c743170 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts @@ -70,7 +70,7 @@ describe('Test video playlists', function () { let commands: PlaylistsCommand[] before(async function () { - this.timeout(120000) + this.timeout(240000) servers = await createMultipleServers(3) diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts index b18c71c94..92f5dab3c 100644 --- a/server/tests/api/videos/video-privacy.ts +++ b/server/tests/api/videos/video-privacy.ts @@ -45,7 +45,7 @@ describe('Test video privacy', function () { describe('Private and internal videos', function () { it('Should upload a private and internal videos on server 1', async function () { - this.timeout(10000) + this.timeout(50000) for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { const attributes = { privacy } diff --git a/server/tests/external-plugins/akismet.ts b/server/tests/external-plugins/akismet.ts new file mode 100644 index 000000000..974bf0011 --- /dev/null +++ b/server/tests/external-plugins/akismet.ts @@ -0,0 +1,160 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { HttpStatusCode } from '@shared/models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@shared/server-commands' + +describe('Official plugin Akismet', function () { + let servers: PeerTubeServer[] + let videoUUID: string + + before(async function () { + this.timeout(30000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + + await servers[0].plugins.install({ + npmName: 'peertube-plugin-akismet' + }) + + if (!process.env.AKISMET_KEY) throw new Error('Missing AKISMET_KEY from env') + + await servers[0].plugins.updateSettings({ + npmName: 'peertube-plugin-akismet', + settings: { + 'akismet-api-key': process.env.AKISMET_KEY + } + }) + + await doubleFollow(servers[0], servers[1]) + }) + + describe('Local threads/replies', function () { + + before(async function () { + const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) + videoUUID = uuid + }) + + it('Should not detect a thread as spam', async function () { + await servers[0].comments.createThread({ videoId: videoUUID, text: 'comment' }) + }) + + it('Should not detect a reply as spam', async function () { + await servers[0].comments.addReplyToLastThread({ text: 'reply' }) + }) + + it('Should detect a thread as spam', async function () { + await servers[0].comments.createThread({ + videoId: videoUUID, + text: 'akismet-guaranteed-spam', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should detect a thread as spam', async function () { + await servers[0].comments.createThread({ videoId: videoUUID, text: 'comment' }) + await servers[0].comments.addReplyToLastThread({ text: 'akismet-guaranteed-spam', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + }) + + describe('Remote threads/replies', function () { + + before(async function () { + this.timeout(60000) + + const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' }) + videoUUID = uuid + + await waitJobs(servers) + }) + + it('Should not detect a thread as spam', async function () { + this.timeout(30000) + + await servers[1].comments.createThread({ videoId: videoUUID, text: 'remote comment 1' }) + await waitJobs(servers) + + const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) + expect(data).to.have.lengthOf(1) + }) + + it('Should not detect a reply as spam', async function () { + this.timeout(30000) + + await servers[1].comments.addReplyToLastThread({ text: 'I agree with you' }) + await waitJobs(servers) + + const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) + expect(data).to.have.lengthOf(1) + + const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId: data[0].id }) + expect(tree.children).to.have.lengthOf(1) + }) + + it('Should detect a thread as spam', async function () { + this.timeout(30000) + + await servers[1].comments.createThread({ videoId: videoUUID, text: 'akismet-guaranteed-spam' }) + await waitJobs(servers) + + const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) + expect(data).to.have.lengthOf(1) + }) + + it('Should detect a thread as spam', async function () { + this.timeout(30000) + + await servers[1].comments.addReplyToLastThread({ text: 'akismet-guaranteed-spam' }) + await waitJobs(servers) + + const { data } = await servers[0].comments.listThreads({ videoId: videoUUID }) + expect(data).to.have.lengthOf(1) + + const thread = data[0] + const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId: thread.id }) + expect(tree.children).to.have.lengthOf(1) + }) + }) + + describe('Signup', function () { + + before(async function () { + await servers[0].config.updateExistingSubConfig({ + newConfig: { + signup: { + enabled: true + } + } + }) + }) + + it('Should allow signup', async function () { + await servers[0].users.register({ + username: 'user1', + displayName: 'user 1' + }) + }) + + it('Should detect a signup as SPAM', async function () { + await servers[0].users.register({ + username: 'user2', + displayName: 'user 2', + email: 'akismet-guaranteed-spam@example.com', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/server/tests/external-plugins/index.ts b/server/tests/external-plugins/index.ts index 31d818b51..815bbf1da 100644 --- a/server/tests/external-plugins/index.ts +++ b/server/tests/external-plugins/index.ts @@ -1,3 +1,4 @@ +import './akismet' import './auth-ldap' import './auto-block-videos' import './auto-mute' diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index 813482a27..19dccf26e 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js @@ -178,6 +178,8 @@ async function register ({ registerHook, registerSetting, settingsManager, stora } }) + // --------------------------------------------------------------------------- + registerHook({ target: 'filter:api.video-thread.create.accept.result', handler: ({ accepted }, { commentBody }) => checkCommentBadWord(accepted, commentBody) @@ -188,6 +190,13 @@ async function register ({ registerHook, registerSetting, settingsManager, stora handler: ({ accepted }, { commentBody }) => checkCommentBadWord(accepted, commentBody) }) + registerHook({ + target: 'filter:activity-pub.remote-video-comment.create.accept.result', + handler: ({ accepted }, { comment }) => checkCommentBadWord(accepted, comment) + }) + + // --------------------------------------------------------------------------- + registerHook({ target: 'filter:api.video-threads.list.params', handler: obj => addToCount(obj) diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index 026c7e856..ae4b3cf5f 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts @@ -64,232 +64,289 @@ describe('Test plugin filter hooks', function () { }) }) - it('Should run filter:api.videos.list.params', async function () { - const { data } = await servers[0].videos.list({ start: 0, count: 2 }) + describe('Videos', function () { - // 2 plugins do +1 to the count parameter - expect(data).to.have.lengthOf(4) - }) + it('Should run filter:api.videos.list.params', async function () { + const { data } = await servers[0].videos.list({ start: 0, count: 2 }) - it('Should run filter:api.videos.list.result', async function () { - const { total } = await servers[0].videos.list({ start: 0, count: 0 }) + // 2 plugins do +1 to the count parameter + expect(data).to.have.lengthOf(4) + }) - // Plugin do +1 to the total result - expect(total).to.equal(11) - }) + it('Should run filter:api.videos.list.result', async function () { + const { total } = await servers[0].videos.list({ start: 0, count: 0 }) - it('Should run filter:api.video-playlist.videos.list.params', async function () { - const { data } = await servers[0].playlists.listVideos({ - count: 2, - playlistId: videoPlaylistUUID + // Plugin do +1 to the total result + expect(total).to.equal(11) }) - // 1 plugin do +1 to the count parameter - expect(data).to.have.lengthOf(3) - }) + it('Should run filter:api.video-playlist.videos.list.params', async function () { + const { data } = await servers[0].playlists.listVideos({ + count: 2, + playlistId: videoPlaylistUUID + }) - it('Should run filter:api.video-playlist.videos.list.result', async function () { - const { total } = await servers[0].playlists.listVideos({ - count: 0, - playlistId: videoPlaylistUUID + // 1 plugin do +1 to the count parameter + expect(data).to.have.lengthOf(3) }) - // Plugin do +1 to the total result - expect(total).to.equal(11) - }) + it('Should run filter:api.video-playlist.videos.list.result', async function () { + const { total } = await servers[0].playlists.listVideos({ + count: 0, + playlistId: videoPlaylistUUID + }) - it('Should run filter:api.accounts.videos.list.params', async function () { - const { data } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) + // Plugin do +1 to the total result + expect(total).to.equal(11) + }) - // 1 plugin do +1 to the count parameter - expect(data).to.have.lengthOf(3) - }) + it('Should run filter:api.accounts.videos.list.params', async function () { + const { data } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) - it('Should run filter:api.accounts.videos.list.result', async function () { - const { total } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) + // 1 plugin do +1 to the count parameter + expect(data).to.have.lengthOf(3) + }) - // Plugin do +2 to the total result - expect(total).to.equal(12) - }) + it('Should run filter:api.accounts.videos.list.result', async function () { + const { total } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) - it('Should run filter:api.video-channels.videos.list.params', async function () { - const { data } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) + // Plugin do +2 to the total result + expect(total).to.equal(12) + }) - // 1 plugin do +3 to the count parameter - expect(data).to.have.lengthOf(5) - }) + it('Should run filter:api.video-channels.videos.list.params', async function () { + const { data } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) - it('Should run filter:api.video-channels.videos.list.result', async function () { - const { total } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) + // 1 plugin do +3 to the count parameter + expect(data).to.have.lengthOf(5) + }) - // Plugin do +3 to the total result - expect(total).to.equal(13) - }) + it('Should run filter:api.video-channels.videos.list.result', async function () { + const { total } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) - it('Should run filter:api.user.me.videos.list.params', async function () { - const { data } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) + // Plugin do +3 to the total result + expect(total).to.equal(13) + }) - // 1 plugin do +4 to the count parameter - expect(data).to.have.lengthOf(6) - }) + it('Should run filter:api.user.me.videos.list.params', async function () { + const { data } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) - it('Should run filter:api.user.me.videos.list.result', async function () { - const { total } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) + // 1 plugin do +4 to the count parameter + expect(data).to.have.lengthOf(6) + }) - // Plugin do +4 to the total result - expect(total).to.equal(14) - }) + it('Should run filter:api.user.me.videos.list.result', async function () { + const { total } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) - it('Should run filter:api.video.get.result', async function () { - const video = await servers[0].videos.get({ id: videoUUID }) - expect(video.name).to.contain('<3') - }) + // Plugin do +4 to the total result + expect(total).to.equal(14) + }) - it('Should run filter:api.video.upload.accept.result', async function () { - await servers[0].videos.upload({ attributes: { name: 'video with bad word' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + it('Should run filter:api.video.get.result', async function () { + const video = await servers[0].videos.get({ id: videoUUID }) + expect(video.name).to.contain('<3') + }) }) - it('Should run filter:api.live-video.create.accept.result', async function () { - const attributes = { - name: 'video with bad word', - privacy: VideoPrivacy.PUBLIC, - channelId: servers[0].store.channel.id - } + describe('Video/live/import accept', function () { - await servers[0].live.create({ fields: attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) - }) - - it('Should run filter:api.video.pre-import-url.accept.result', async function () { - const attributes = { - name: 'normal title', - privacy: VideoPrivacy.PUBLIC, - channelId: servers[0].store.channel.id, - targetUrl: FIXTURE_URLS.goodVideo + 'bad' - } - await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) - }) + it('Should run filter:api.video.upload.accept.result', async function () { + await servers[0].videos.upload({ attributes: { name: 'video with bad word' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) - it('Should run filter:api.video.pre-import-torrent.accept.result', async function () { - const attributes = { - name: 'bad torrent', - privacy: VideoPrivacy.PUBLIC, - channelId: servers[0].store.channel.id, - torrentfile: 'video-720p.torrent' as any - } - await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) - }) + it('Should run filter:api.live-video.create.accept.result', async function () { + const attributes = { + name: 'video with bad word', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id + } - it('Should run filter:api.video.post-import-url.accept.result', async function () { - this.timeout(60000) + await servers[0].live.create({ fields: attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) - let videoImportId: number + it('Should run filter:api.video.pre-import-url.accept.result', async function () { + const attributes = { + name: 'normal title', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id, + targetUrl: FIXTURE_URLS.goodVideo + 'bad' + } + await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) - { + it('Should run filter:api.video.pre-import-torrent.accept.result', async function () { const attributes = { - name: 'title with bad word', + name: 'bad torrent', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].store.channel.id, - targetUrl: FIXTURE_URLS.goodVideo + torrentfile: 'video-720p.torrent' as any } - const body = await servers[0].imports.importVideo({ attributes }) - videoImportId = body.id - } + await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) - await waitJobs(servers) + it('Should run filter:api.video.post-import-url.accept.result', async function () { + this.timeout(60000) - { - const body = await servers[0].imports.getMyVideoImports() - const videoImports = body.data + let videoImportId: number - const videoImport = videoImports.find(i => i.id === videoImportId) + { + const attributes = { + name: 'title with bad word', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id, + targetUrl: FIXTURE_URLS.goodVideo + } + const body = await servers[0].imports.importVideo({ attributes }) + videoImportId = body.id + } - expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) - expect(videoImport.state.label).to.equal('Rejected') - } - }) + await waitJobs(servers) - it('Should run filter:api.video.post-import-torrent.accept.result', async function () { - this.timeout(60000) + { + const body = await servers[0].imports.getMyVideoImports() + const videoImports = body.data - let videoImportId: number + const videoImport = videoImports.find(i => i.id === videoImportId) - { - const attributes = { - name: 'title with bad word', - privacy: VideoPrivacy.PUBLIC, - channelId: servers[0].store.channel.id, - torrentfile: 'video-720p.torrent' as any + expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) + expect(videoImport.state.label).to.equal('Rejected') } - const body = await servers[0].imports.importVideo({ attributes }) - videoImportId = body.id - } + }) - await waitJobs(servers) + it('Should run filter:api.video.post-import-torrent.accept.result', async function () { + this.timeout(60000) - { - const { data: videoImports } = await servers[0].imports.getMyVideoImports() + let videoImportId: number - const videoImport = videoImports.find(i => i.id === videoImportId) + { + const attributes = { + name: 'title with bad word', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id, + torrentfile: 'video-720p.torrent' as any + } + const body = await servers[0].imports.importVideo({ attributes }) + videoImportId = body.id + } - expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) - expect(videoImport.state.label).to.equal('Rejected') - } - }) + await waitJobs(servers) + + { + const { data: videoImports } = await servers[0].imports.getMyVideoImports() + + const videoImport = videoImports.find(i => i.id === videoImportId) - it('Should run filter:api.video-thread.create.accept.result', async function () { - await servers[0].comments.createThread({ - videoId: videoUUID, - text: 'comment with bad word', - expectedStatus: HttpStatusCode.FORBIDDEN_403 + expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) + expect(videoImport.state.label).to.equal('Rejected') + } }) }) - it('Should run filter:api.video-comment-reply.create.accept.result', async function () { - const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' }) - threadId = created.id + describe('Video comments accept', function () { - await servers[0].comments.addReply({ - videoId: videoUUID, - toCommentId: threadId, - text: 'comment with bad word', - expectedStatus: HttpStatusCode.FORBIDDEN_403 + it('Should run filter:api.video-thread.create.accept.result', async function () { + await servers[0].comments.createThread({ + videoId: videoUUID, + text: 'comment with bad word', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) }) - await servers[0].comments.addReply({ - videoId: videoUUID, - toCommentId: threadId, - text: 'comment with good word', - expectedStatus: HttpStatusCode.OK_200 + + it('Should run filter:api.video-comment-reply.create.accept.result', async function () { + const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' }) + threadId = created.id + + await servers[0].comments.addReply({ + videoId: videoUUID, + toCommentId: threadId, + text: 'comment with bad word', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + await servers[0].comments.addReply({ + videoId: videoUUID, + toCommentId: threadId, + text: 'comment with good word', + expectedStatus: HttpStatusCode.OK_200 + }) }) - }) - it('Should run filter:api.video-threads.list.params', async function () { - const { data } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) + it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a thread creation', async function () { + this.timeout(30000) - // our plugin do +1 to the count parameter - expect(data).to.have.lengthOf(1) - }) + await servers[1].comments.createThread({ videoId: videoUUID, text: 'comment with bad word' }) - it('Should run filter:api.video-threads.list.result', async function () { - const { total } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) + await waitJobs(servers) - // Plugin do +1 to the total result - expect(total).to.equal(2) - }) + { + const thread = await servers[0].comments.listThreads({ videoId: videoUUID }) + expect(thread.data).to.have.lengthOf(1) + expect(thread.data[0].text).to.not.include(' bad ') + } + + { + const thread = await servers[1].comments.listThreads({ videoId: videoUUID }) + expect(thread.data).to.have.lengthOf(2) + } + }) - it('Should run filter:api.video-thread-comments.list.params') + it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a reply creation', async function () { + this.timeout(30000) - it('Should run filter:api.video-thread-comments.list.result', async function () { - const thread = await servers[0].comments.getThread({ videoId: videoUUID, threadId }) + const { data } = await servers[1].comments.listThreads({ videoId: videoUUID }) + const threadIdServer2 = data.find(t => t.text === 'thread').id - expect(thread.comment.text.endsWith(' <3')).to.be.true + await servers[1].comments.addReply({ + videoId: videoUUID, + toCommentId: threadIdServer2, + text: 'comment with bad word' + }) + + await waitJobs(servers) + + { + const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId }) + expect(tree.children).to.have.lengthOf(1) + expect(tree.children[0].comment.text).to.not.include(' bad ') + } + + { + const tree = await servers[1].comments.getThread({ videoId: videoUUID, threadId: threadIdServer2 }) + expect(tree.children).to.have.lengthOf(2) + } + }) }) - it('Should run filter:api.overviews.videos.list.{params,result}', async function () { - await servers[0].overviews.getVideos({ page: 1 }) + describe('Video comments', function () { + + it('Should run filter:api.video-threads.list.params', async function () { + const { data } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) + + // our plugin do +1 to the count parameter + expect(data).to.have.lengthOf(1) + }) - // 3 because we get 3 samples per page - await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.params', 3) - await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.result', 3) + it('Should run filter:api.video-threads.list.result', async function () { + const { total } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) + + // Plugin do +1 to the total result + expect(total).to.equal(2) + }) + + it('Should run filter:api.video-thread-comments.list.params') + + it('Should run filter:api.video-thread-comments.list.result', async function () { + const thread = await servers[0].comments.getThread({ videoId: videoUUID, threadId }) + + expect(thread.comment.text.endsWith(' <3')).to.be.true + }) + + it('Should run filter:api.overviews.videos.list.{params,result}', async function () { + await servers[0].overviews.getVideos({ page: 1 }) + + // 3 because we get 3 samples per page + await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.params', 3) + await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.result', 3) + }) }) describe('filter:video.auto-blacklist.result', function () { diff --git a/shared/models/plugins/server/server-hook.model.ts b/shared/models/plugins/server/server-hook.model.ts index 5bf01c4b4..f11d2050b 100644 --- a/shared/models/plugins/server/server-hook.model.ts +++ b/shared/models/plugins/server/server-hook.model.ts @@ -103,7 +103,9 @@ export const serverFilterHookObject = { 'filter:job-queue.process.result': true, 'filter:transcoding.manual.resolutions-to-transcode.result': true, - 'filter:transcoding.auto.resolutions-to-transcode.result': true + 'filter:transcoding.auto.resolutions-to-transcode.result': true, + + 'filter:activity-pub.remote-video-comment.create.accept.result': true } export type ServerFilterHookName = keyof typeof serverFilterHookObject diff --git a/shared/server-commands/users/users-command.ts b/shared/server-commands/users/users-command.ts index d8303848d..e7d021059 100644 --- a/shared/server-commands/users/users-command.ts +++ b/shared/server-commands/users/users-command.ts @@ -217,12 +217,13 @@ export class UsersCommand extends AbstractCommand { username: string password?: string displayName?: string + email?: string channel?: { name: string displayName: string } }) { - const { username, password = 'password', displayName, channel } = options + const { username, password = 'password', displayName, channel, email = username + '@example.com' } = options const path = '/api/v1/users/register' return this.postBodyRequest({ @@ -232,7 +233,7 @@ export class UsersCommand extends AbstractCommand { fields: { username, password, - email: username + '@example.com', + email, displayName, channel }, diff --git a/support/doc/production.md b/support/doc/production.md index 44b2c29b1..64ddd9e48 100644 --- a/support/doc/production.md +++ b/support/doc/production.md @@ -281,7 +281,7 @@ Now your instance is up you can: ### PeerTube instance -**Check the changelog (in particular BREAKING CHANGES!):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md +**Check the changelog (in particular the *IMPORTANT NOTES* section):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md #### Auto