Chocobozzz/PeerTube

View on GitHub
server/core/lib/moderation.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import express, { VideoLegacyUploadFile } from 'express'
import { PathLike } from 'fs-extra/esm'
import { Transaction } from 'sequelize'
import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger.js'
import { afterCommitIfTransaction } from '@server/helpers/database-utils.js'
import { logger } from '@server/helpers/logger.js'
import { AbuseModel } from '@server/models/abuse/abuse.js'
import { VideoAbuseModel } from '@server/models/abuse/video-abuse.js'
import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { FilteredModelAttributes } from '@server/types/index.js'
import {
  MAbuseFull,
  MAccountDefault,
  MAccountLight,
  MComment,
  MCommentAbuseAccountVideo,
  MCommentOwnerVideo,
  MUser,
  MUserDefault,
  MVideoAbuseVideoFull,
  MVideoAccountLightBlacklistAllFiles
} from '@server/types/models/index.js'
import { LiveVideoCreate, VideoCommentCreate, VideoCreate, VideoImportCreate } from '@peertube/peertube-models'
import { UserModel } from '../models/user/user.js'
import { VideoCommentModel } from '../models/video/video-comment.js'
import { VideoModel } from '../models/video/video.js'
import { sendAbuse } from './activitypub/send/send-flag.js'
import { Notifier } from './notifier/index.js'

export type AcceptResult = {
  accepted: boolean
  errorMessage?: string
}

// ---------------------------------------------------------------------------

// Stub function that can be filtered by plugins
function isLocalVideoFileAccepted (object: {
  videoBody: VideoCreate
  videoFile: VideoLegacyUploadFile
  user: MUserDefault
}): AcceptResult {
  return { accepted: true }
}

// ---------------------------------------------------------------------------

// Stub function that can be filtered by plugins
function isLocalLiveVideoAccepted (object: {
  liveVideoBody: LiveVideoCreate
  user: UserModel
}): AcceptResult {
  return { accepted: true }
}

// ---------------------------------------------------------------------------

// Stub function that can be filtered by plugins
function isLocalVideoThreadAccepted (_object: {
  req: express.Request
  commentBody: VideoCommentCreate
  video: VideoModel
  user: UserModel
}): AcceptResult {
  return { accepted: true }
}

// Stub function that can be filtered by plugins
function isLocalVideoCommentReplyAccepted (_object: {
  req: express.Request
  commentBody: VideoCommentCreate
  parentComment: VideoCommentModel
  video: VideoModel
  user: UserModel
}): AcceptResult {
  return { accepted: true }
}

// ---------------------------------------------------------------------------

// Stub function that can be filtered by plugins
function isRemoteVideoCommentAccepted (_object: {
  comment: MComment
}): AcceptResult {
  return { accepted: true }
}

// ---------------------------------------------------------------------------

// Stub function that can be filtered by plugins
function isPreImportVideoAccepted (object: {
  videoImportBody: VideoImportCreate
  user: MUser
}): AcceptResult {
  return { accepted: true }
}

// Stub function that can be filtered by plugins
function isPostImportVideoAccepted (object: {
  videoFilePath: PathLike
  videoFile: VideoFileModel
  user: MUser
}): AcceptResult {
  return { accepted: true }
}

// ---------------------------------------------------------------------------

async function createVideoAbuse (options: {
  baseAbuse: FilteredModelAttributes<AbuseModel>
  videoInstance: MVideoAccountLightBlacklistAllFiles
  startAt: number
  endAt: number
  transaction: Transaction
  reporterAccount: MAccountDefault
  skipNotification: boolean
}) {
  const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount, skipNotification } = options

  const associateFun = async (abuseInstance: MAbuseFull) => {
    const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({
      abuseId: abuseInstance.id,
      videoId: videoInstance.id,
      startAt,
      endAt
    }, { transaction })

    videoAbuseInstance.Video = videoInstance
    abuseInstance.VideoAbuse = videoAbuseInstance

    return { isOwned: videoInstance.isOwned() }
  }

  return createAbuse({
    base: baseAbuse,
    reporterAccount,
    flaggedAccount: videoInstance.VideoChannel.Account,
    transaction,
    skipNotification,
    associateFun
  })
}

function createVideoCommentAbuse (options: {
  baseAbuse: FilteredModelAttributes<AbuseModel>
  commentInstance: MCommentOwnerVideo
  transaction: Transaction
  reporterAccount: MAccountDefault
  skipNotification: boolean
}) {
  const { baseAbuse, commentInstance, transaction, reporterAccount, skipNotification } = options

  const associateFun = async (abuseInstance: MAbuseFull) => {
    const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({
      abuseId: abuseInstance.id,
      videoCommentId: commentInstance.id
    }, { transaction })

    commentAbuseInstance.VideoComment = commentInstance
    abuseInstance.VideoCommentAbuse = commentAbuseInstance

    return { isOwned: commentInstance.isOwned() }
  }

  return createAbuse({
    base: baseAbuse,
    reporterAccount,
    flaggedAccount: commentInstance.Account,
    transaction,
    skipNotification,
    associateFun
  })
}

function createAccountAbuse (options: {
  baseAbuse: FilteredModelAttributes<AbuseModel>
  accountInstance: MAccountDefault
  transaction: Transaction
  reporterAccount: MAccountDefault
  skipNotification: boolean
}) {
  const { baseAbuse, accountInstance, transaction, reporterAccount, skipNotification } = options

  const associateFun = () => {
    return Promise.resolve({ isOwned: accountInstance.isOwned() })
  }

  return createAbuse({
    base: baseAbuse,
    reporterAccount,
    flaggedAccount: accountInstance,
    transaction,
    skipNotification,
    associateFun
  })
}

// ---------------------------------------------------------------------------

export {
  isLocalLiveVideoAccepted,

  isLocalVideoFileAccepted,
  isLocalVideoThreadAccepted,
  isRemoteVideoCommentAccepted,
  isLocalVideoCommentReplyAccepted,
  isPreImportVideoAccepted,
  isPostImportVideoAccepted,

  createAbuse,
  createVideoAbuse,
  createVideoCommentAbuse,
  createAccountAbuse
}

// ---------------------------------------------------------------------------

async function createAbuse (options: {
  base: FilteredModelAttributes<AbuseModel>
  reporterAccount: MAccountDefault
  flaggedAccount: MAccountLight
  associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean }>
  skipNotification: boolean
  transaction: Transaction
}) {
  const { base, reporterAccount, flaggedAccount, associateFun, transaction, skipNotification } = options
  const auditLogger = auditLoggerFactory('abuse')

  const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id })
  const abuseInstance: MAbuseFull = await AbuseModel.create(abuseAttributes, { transaction })

  abuseInstance.ReporterAccount = reporterAccount
  abuseInstance.FlaggedAccount = flaggedAccount

  const { isOwned } = await associateFun(abuseInstance)

  if (isOwned === false) {
    sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
  }

  const abuseJSON = abuseInstance.toFormattedAdminJSON()
  auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON))

  if (!skipNotification) {
    afterCommitIfTransaction(transaction, () => {
      Notifier.Instance.notifyOnNewAbuse({
        abuse: abuseJSON,
        abuseInstance,
        reporter: reporterAccount.Actor.getIdentifier()
      })
    })
  }

  logger.info('Abuse report %d created.', abuseInstance.id)

  return abuseJSON
}