Add video abuse to activity pub

pull/128/head
Chocobozzz 2017-11-15 15:12:23 +01:00
parent 59c857da59
commit 8e13fa7d09
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
13 changed files with 114 additions and 68 deletions

View File

@ -11,6 +11,7 @@
<p-column field="reason" header="Reason"></p-column>
<p-column field="reporterServerHost" header="Reporter server host"></p-column>
<p-column field="reporterUsername" header="Reporter username"></p-column>
<p-column field="videoName" header="Video name"></p-column>
<p-column header="Video" styleClass="action-cell">
<ng-template pTemplate="body" let-videoAbuse="rowData">
<a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoId }}</a>

View File

@ -18,6 +18,7 @@ import {
} from '../../../middlewares'
import { VideoInstance } from '../../../models'
import { VideoAbuseCreate, UserRight } from '../../../../shared'
import { sendVideoAbuse } from '../../../lib/index'
const abuseVideoRouter = express.Router()
@ -63,28 +64,21 @@ async function reportVideoAbuseRetryWrapper (req: express.Request, res: express.
async function reportVideoAbuse (req: express.Request, res: express.Response) {
const videoInstance = res.locals.video as VideoInstance
const reporterUsername = res.locals.oauth.token.User.username
const reporterAccount = res.locals.oauth.token.User.Account
const body: VideoAbuseCreate = req.body
const abuseToCreate = {
reporterUsername,
reporterAccountId: reporterAccount.id,
reason: body.reason,
videoId: videoInstance.id,
reporterServerId: null // This is our server that reported this abuse
videoId: videoInstance.id
}
await db.sequelize.transaction(async t => {
const abuse = await db.VideoAbuse.create(abuseToCreate, { transaction: t })
// We send the information to the destination server
if (videoInstance.isOwned() === false) {
const reportData = {
reporterUsername,
reportReason: abuse.reason,
videoUUID: videoInstance.uuid
}
const videoAbuseInstance = await db.VideoAbuse.create(abuseToCreate, { transaction: t })
// await friends.reportAbuseVideoToFriend(reportData, videoInstance, t)
// TODO: send abuse to origin server
// We send the video abuse to the origin server
if (videoInstance.isOwned() === false) {
await sendVideoAbuse(reporterAccount, videoAbuseInstance, videoInstance, t)
}
})

View File

@ -22,10 +22,11 @@ function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObjec
return doRequestAndSaveToFile(options, thumbnailPath)
}
function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account', id: string) {
function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) {
if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + id
else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + id
else if (type === 'account') return CONFIG.WEBSERVER.URL + '/account/' + id
else if (type === 'videoAbuse') return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + id
return ''
}
@ -134,7 +135,8 @@ function activityPubContextify <T> (data: T) {
'nsfw': 'as:sensitive',
'language': 'http://schema.org/inLanguage',
'views': 'http://schema.org/Number',
'size': 'http://schema.org/Number'
'size': 'http://schema.org/Number',
'VideoChannel': 'https://peertu.be/ns/VideoChannel'
}
]
})

View File

@ -112,10 +112,6 @@ function isVideoAbuseReasonValid (value: string) {
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
}
function isVideoAbuseReporterUsernameValid (value: string) {
return isUserUsernameValid(value)
}
function isVideoViewsValid (value: string) {
return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.VIEWS)
}
@ -209,7 +205,6 @@ export {
isVideoThumbnailDataValid,
isVideoFileExtnameValid,
isVideoAbuseReasonValid,
isVideoAbuseReporterUsernameValid,
isVideoFile,
isVideoViewsValid,
isVideoLikesValid,

View File

@ -1,11 +1,9 @@
import { ActivityCreate, VideoChannelObject, VideoTorrentObject } from '../../../shared'
import { ActivityAdd } from '../../../shared/models/activitypub/activity'
import { generateThumbnailFromUrl, logger, retryTransactionWrapper } from '../../helpers'
import { database as db } from '../../initializers'
import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
import Bluebird = require('bluebird')
import { AccountInstance } from '../../models/account/account-interface'
import { ActivityCreate, VideoChannelObject } from '../../../shared'
import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object'
import { logger, retryTransactionWrapper } from '../../helpers'
import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub'
import { database as db } from '../../initializers'
import { AccountInstance } from '../../models/account/account-interface'
async function processCreateActivity (activity: ActivityCreate) {
const activityObject = activity.object
@ -14,6 +12,8 @@ async function processCreateActivity (activity: ActivityCreate) {
if (activityType === 'VideoChannel') {
return processCreateVideoChannel(account, activityObject as VideoChannelObject)
} else if (activityType === 'Flag') {
return processCreateVideoAbuse(account, activityObject as VideoAbuseObject)
}
logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
@ -62,3 +62,34 @@ async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCr
logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)
}
function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
const options = {
arguments: [ account, videoAbuseToCreateData ],
errorMessage: 'Cannot insert the remote video abuse with many retries.'
}
return retryTransactionWrapper(addRemoteVideoAbuse, options)
}
async function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) {
logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object)
return db.sequelize.transaction(async t => {
const video = await db.Video.loadByUrl(videoAbuseToCreateData.object, t)
if (!video) {
logger.warn('Unknown video %s for remote video abuse.', videoAbuseToCreateData.object)
return
}
const videoAbuseData = {
reporterAccountId: account.id,
reason: videoAbuseToCreateData.content,
videoId: video.id
}
await db.VideoAbuse.create(videoAbuseData)
logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
})
}

View File

@ -9,6 +9,8 @@ import {
import { httpRequestJobScheduler } from '../jobs'
import { signObject, activityPubContextify } from '../../helpers'
import { Activity } from '../../../shared'
import { VideoAbuseInstance } from '../../models/video/video-abuse-interface'
import { getActivityPubUrl } from '../../helpers/activitypub'
async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
const videoChannelObject = videoChannel.toActivityPubObject()
@ -56,16 +58,28 @@ async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transac
return broadcastToFollowers(data, account, t)
}
async function sendVideoAbuse (
fromAccount: AccountInstance,
videoAbuse: VideoAbuseInstance,
video: VideoInstance,
t: Sequelize.Transaction
) {
const url = getActivityPubUrl('videoAbuse', videoAbuse.id.toString())
const data = await createActivityData(url, fromAccount, video.url)
return unicastTo(data, video.VideoChannel.Account.sharedInboxUrl, t)
}
async function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
const data = await acceptActivityData(fromAccount)
return unicastTo(data, toAccount, t)
return unicastTo(data, toAccount.inboxUrl, t)
}
async function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
const data = await followActivityData(toAccount.url, fromAccount)
return unicastTo(data, toAccount, t)
return unicastTo(data, toAccount.inboxUrl, t)
}
// ---------------------------------------------------------------------------
@ -79,7 +93,8 @@ export {
sendDeleteVideo,
sendDeleteAccount,
sendAccept,
sendFollow
sendFollow,
sendVideoAbuse
}
// ---------------------------------------------------------------------------
@ -95,9 +110,9 @@ async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t:
return httpRequestJobScheduler.createJob(t, 'httpRequestBroadcastHandler', jobPayload)
}
async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) {
async function unicastTo (data: any, toAccountUrl: string, t: Sequelize.Transaction) {
const jobPayload = {
uris: [ toAccount.inboxUrl ],
uris: [ toAccountUrl ],
body: data
}

View File

@ -1,11 +1,10 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { ServerInstance } from '../server/server-interface'
import * as Sequelize from 'sequelize'
import { ResultList } from '../../../shared'
// Don't use barrel, import just what we need
import { VideoAbuse as FormattedVideoAbuse } from '../../../shared/models/videos/video-abuse.model'
import { AccountInstance } from '../account/account-interface'
import { ServerInstance } from '../server/server-interface'
import { VideoInstance } from './video-interface'
export namespace VideoAbuseMethods {
export type ToFormattedJSON = (this: VideoAbuseInstance) => FormattedVideoAbuse
@ -18,9 +17,12 @@ export interface VideoAbuseClass {
}
export interface VideoAbuseAttributes {
reporterUsername: string
reason: string
videoId: number
reporterAccountId: number
Account?: AccountInstance
Video?: VideoInstance
}
export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance<VideoAbuseAttributes> {

View File

@ -1,7 +1,7 @@
import * as Sequelize from 'sequelize'
import { CONFIG } from '../../initializers'
import { isVideoAbuseReporterUsernameValid, isVideoAbuseReasonValid } from '../../helpers'
import { isVideoAbuseReasonValid } from '../../helpers'
import { addMethodsToModel, getSort } from '../utils'
import {
@ -18,16 +18,6 @@ let listForApi: VideoAbuseMethods.ListForApi
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
VideoAbuse = sequelize.define<VideoAbuseInstance, VideoAbuseAttributes>('VideoAbuse',
{
reporterUsername: {
type: DataTypes.STRING,
allowNull: false,
validate: {
reporterUsernameValid: value => {
const res = isVideoAbuseReporterUsernameValid(value)
if (res === false) throw new Error('Video abuse reporter username is not valid.')
}
}
},
reason: {
type: DataTypes.STRING,
allowNull: false,
@ -45,7 +35,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
fields: [ 'videoId' ]
},
{
fields: [ 'reporterServerId' ]
fields: [ 'reporterAccountId' ]
}
]
}
@ -69,8 +59,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
toFormattedJSON = function (this: VideoAbuseInstance) {
let reporterServerHost
if (this.Server) {
reporterServerHost = this.Server.host
if (this.Account.Server) {
reporterServerHost = this.Account.Server.host
} else {
// It means it's our video
reporterServerHost = CONFIG.WEBSERVER.HOST
@ -78,10 +68,12 @@ toFormattedJSON = function (this: VideoAbuseInstance) {
const json = {
id: this.id,
reporterServerHost,
reason: this.reason,
reporterUsername: this.reporterUsername,
videoId: this.videoId,
reporterUsername: this.Account.name,
reporterServerHost,
videoId: this.Video.id,
videoUUID: this.Video.uuid,
videoName: this.Video.name,
createdAt: this.createdAt
}
@ -91,9 +83,9 @@ toFormattedJSON = function (this: VideoAbuseInstance) {
// ------------------------------ STATICS ------------------------------
function associate (models) {
VideoAbuse.belongsTo(models.Server, {
VideoAbuse.belongsTo(models.Account, {
foreignKey: {
name: 'reporterServerId',
name: 'reporterAccountId',
allowNull: true
},
onDelete: 'CASCADE'
@ -115,8 +107,18 @@ listForApi = function (start: number, count: number, sort: string) {
order: [ getSort(sort) ],
include: [
{
model: VideoAbuse['sequelize'].models.Server,
required: false
model: VideoAbuse['sequelize'].models.Account,
required: true,
include: [
{
model: VideoAbuse['sequelize'].models.Server,
required: false
}
]
},
{
model: VideoAbuse['sequelize'].models.Video,
required: true
}
]
}

View File

@ -1,8 +1,6 @@
import {
VideoChannelObject,
VideoTorrentObject
} from './objects'
import { VideoChannelObject, VideoTorrentObject } from './objects'
import { ActivityPubSignature } from './activitypub-signature'
import { VideoAbuseObject } from './objects/video-abuse-object'
export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | ActivityFlag |
ActivityDelete | ActivityFollow | ActivityAccept
@ -21,7 +19,7 @@ export interface BaseActivity {
export interface ActivityCreate extends BaseActivity {
type: 'Create'
object: VideoChannelObject
object: VideoChannelObject | VideoAbuseObject
}
export interface ActivityAdd extends BaseActivity {

View File

@ -1,3 +1,4 @@
export * from './common-objects'
export * from './video-abuse-object'
export * from './video-channel-object'
export * from './video-torrent-object'

View File

@ -0,0 +1,5 @@
export interface VideoAbuseObject {
type: 'Flag',
content: string
object: string
}

View File

@ -1,5 +1,3 @@
import { ActivityIdentifierObject } from './common-objects'
export interface VideoChannelObject {
type: 'VideoChannel'
id: string

View File

@ -1,8 +1,10 @@
export interface VideoAbuse {
id: number
reporterServerHost: string
reason: string
reporterUsername: string
reporterServerHost: string
videoId: number
videoUUID: string
videoName: string
createdAt: Date
}