Check activities host

This commit is contained in:
Chocobozzz 2018-11-14 15:01:28 +01:00
parent df66d81583
commit 5c6d985fae
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
37 changed files with 403 additions and 127 deletions

View File

@ -6,11 +6,11 @@ import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } fr
import { ResultList } from '../../../../../shared/models/result-list.model' import { ResultList } from '../../../../../shared/models/result-list.model'
import { import {
UserVideoRate, UserVideoRate,
UserVideoRateType,
UserVideoRateUpdate, UserVideoRateUpdate,
VideoConstant, VideoConstant,
VideoFilter, VideoFilter,
VideoPrivacy, VideoPrivacy,
VideoRateType,
VideoUpdate VideoUpdate
} from '../../../../../shared/models/videos' } from '../../../../../shared/models/videos'
import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
@ -332,7 +332,7 @@ export class VideoService implements VideosProvider {
return privacies return privacies
} }
private setVideoRate (id: number, rateType: VideoRateType) { private setVideoRate (id: number, rateType: UserVideoRateType) {
const url = VideoService.BASE_VIDEO_URL + id + '/rate' const url = VideoService.BASE_VIDEO_URL + id + '/rate'
const body: UserVideoRateUpdate = { const body: UserVideoRateUpdate = {
rating: rateType rating: rateType

View File

@ -450,7 +450,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.checkUserRating() this.checkUserRating()
} }
private setRating (nextRating: VideoRateType) { private setRating (nextRating: UserVideoRateType) {
let method let method
switch (nextRating) { switch (nextRating) {
case 'like': case 'like':
@ -476,7 +476,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
) )
} }
private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
let likesToIncrement = 0 let likesToIncrement = 0
let dislikesToIncrement = 0 let dislikesToIncrement = 0

View File

@ -4,7 +4,7 @@ import { VideoModel } from '../server/models/video/video'
import { ActorModel } from '../server/models/activitypub/actor' import { ActorModel } from '../server/models/activitypub/actor'
import { import {
getAccountActivityPubUrl, getAccountActivityPubUrl,
getAnnounceActivityPubUrl, getVideoAnnounceActivityPubUrl,
getVideoActivityPubUrl, getVideoChannelActivityPubUrl, getVideoActivityPubUrl, getVideoChannelActivityPubUrl,
getVideoCommentActivityPubUrl getVideoCommentActivityPubUrl
} from '../server/lib/activitypub' } from '../server/lib/activitypub'
@ -78,7 +78,7 @@ async function run () {
console.log('Updating video share ' + videoShare.url) console.log('Updating video share ' + videoShare.url)
videoShare.url = getAnnounceActivityPubUrl(videoShare.Video.url, videoShare.Actor) videoShare.url = getVideoAnnounceActivityPubUrl(videoShare.Actor, videoShare.Video)
await videoShare.save() await videoShare.save()
} }

View File

@ -3,17 +3,22 @@ import * as express from 'express'
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers'
import { buildAnnounceWithVideoAudience } from '../../lib/activitypub/send' import { buildAnnounceWithVideoAudience, buildDislikeActivity, buildLikeActivity } from '../../lib/activitypub/send'
import { audiencify, getAudience } from '../../lib/activitypub/audience' import { audiencify, getAudience } from '../../lib/activitypub/audience'
import { buildCreateActivity } from '../../lib/activitypub/send/send-create' import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
import { import {
asyncMiddleware, asyncMiddleware,
videosShareValidator,
executeIfActivityPub, executeIfActivityPub,
localAccountValidator, localAccountValidator,
localVideoChannelValidator, localVideoChannelValidator,
videosCustomGetValidator videosCustomGetValidator
} from '../../middlewares' } from '../../middlewares'
import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' import {
getAccountVideoRateValidator,
videoCommentGetValidator,
videosGetValidator
} from '../../middlewares/validators'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor' import { ActorModel } from '../../models/activitypub/actor'
import { ActorFollowModel } from '../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../models/activitypub/actor-follow'
@ -25,6 +30,7 @@ import { cacheRoute } from '../../middlewares/cache'
import { activityPubResponse } from './utils' import { activityPubResponse } from './utils'
import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { import {
getRateUrl,
getVideoCommentsActivityPubUrl, getVideoCommentsActivityPubUrl,
getVideoDislikesActivityPubUrl, getVideoDislikesActivityPubUrl,
getVideoLikesActivityPubUrl, getVideoLikesActivityPubUrl,
@ -48,6 +54,14 @@ activityPubClientRouter.get('/accounts?/:name/following',
executeIfActivityPub(asyncMiddleware(localAccountValidator)), executeIfActivityPub(asyncMiddleware(localAccountValidator)),
executeIfActivityPub(asyncMiddleware(accountFollowingController)) executeIfActivityPub(asyncMiddleware(accountFollowingController))
) )
activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))),
executeIfActivityPub(getAccountVideoRate('like'))
)
activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))),
executeIfActivityPub(getAccountVideoRate('dislike'))
)
activityPubClientRouter.get('/videos/watch/:id', activityPubClientRouter.get('/videos/watch/:id',
executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))),
@ -62,7 +76,7 @@ activityPubClientRouter.get('/videos/watch/:id/announces',
executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))),
executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) executeIfActivityPub(asyncMiddleware(videoAnnouncesController))
) )
activityPubClientRouter.get('/videos/watch/:id/announces/:accountId', activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
executeIfActivityPub(asyncMiddleware(videosShareValidator)), executeIfActivityPub(asyncMiddleware(videosShareValidator)),
executeIfActivityPub(asyncMiddleware(videoAnnounceController)) executeIfActivityPub(asyncMiddleware(videoAnnounceController))
) )
@ -133,6 +147,20 @@ async function accountFollowingController (req: express.Request, res: express.Re
return activityPubResponse(activityPubContextify(activityPubResult), res) return activityPubResponse(activityPubContextify(activityPubResult), res)
} }
function getAccountVideoRate (rateType: VideoRateType) {
return (req: express.Request, res: express.Response) => {
const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate
const byActor = accountVideoRate.Account.Actor
const url = getRateUrl(rateType, byActor, accountVideoRate.Video)
const APObject = rateType === 'like'
? buildLikeActivity(url, byActor, accountVideoRate.Video)
: buildCreateActivity(url, byActor, buildDislikeActivity(url, byActor, accountVideoRate.Video))
return activityPubResponse(activityPubContextify(APObject), res)
}
}
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video const video: VideoModel = res.locals.video
@ -276,7 +304,7 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: Video
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
return { return {
total: result.count, total: result.count,
data: result.rows.map(r => r.Account.Actor.url) data: result.rows.map(r => r.url)
} }
} }
return activityPubCollectionPagination(url, handler, req.query.page) return activityPubCollectionPagination(url, handler, req.query.page)

View File

@ -43,11 +43,13 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => {
processActivities(task.activities, task.signatureActor, task.inboxActor) const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
processActivities(task.activities, options)
.then(() => cb()) .then(() => cb())
}) })
function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) { function inboxController (req: express.Request, res: express.Response) {
const rootActivity: RootActivity = req.body const rootActivity: RootActivity = req.body
let activities: Activity[] = [] let activities: Activity[] = []

View File

@ -2,8 +2,8 @@ import * as express from 'express'
import { UserVideoRateUpdate } from '../../../../shared' import { UserVideoRateUpdate } from '../../../../shared'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
import { sendVideoRateChange } from '../../../lib/activitypub' import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
rateVideoRouter.put('/:id/rate', rateVideoRouter.put('/:id/rate',
authenticate, authenticate,
asyncMiddleware(videoRateValidator), asyncMiddleware(videoUpdateRateValidator),
asyncRetryTransactionMiddleware(rateVideo) asyncRetryTransactionMiddleware(rateVideo)
) )
@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
const body: UserVideoRateUpdate = req.body const body: UserVideoRateUpdate = req.body
const rateType = body.rating const rateType = body.rating
const videoInstance: VideoModel = res.locals.video const videoInstance: VideoModel = res.locals.video
const userAccount: AccountModel = res.locals.oauth.token.User.Account
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = { transaction: t } const sequelizeOptions = { transaction: t }
const accountInstance = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) const accountInstance = await AccountModel.load(userAccount.id, t)
const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t)
let likesToIncrement = 0 let likesToIncrement = 0
@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
// There was a previous rate, update it // There was a previous rate, update it
if (previousRate) { if (previousRate) {
// We will remove the previous rate, so we will need to update the video count attribute // We will remove the previous rate, so we will need to update the video count attribute
if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- if (previousRate.type === 'like') likesToIncrement--
else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- else if (previousRate.type === 'dislike') dislikesToIncrement--
if (rateType === 'none') { // Destroy previous rate if (rateType === 'none') { // Destroy previous rate
await previousRate.destroy(sequelizeOptions) await previousRate.destroy(sequelizeOptions)
} else { // Update previous rate } else { // Update previous rate
previousRate.type = rateType previousRate.type = rateType
previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
await previousRate.save(sequelizeOptions) await previousRate.save(sequelizeOptions)
} }
} else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
const query = { const query = {
accountId: accountInstance.id, accountId: accountInstance.id,
videoId: videoInstance.id, videoId: videoInstance.id,
type: rateType type: rateType,
url: getRateUrl(rateType, userAccount.Actor, videoInstance)
} }
await AccountVideoRateModel.create(query, sequelizeOptions) await AccountVideoRateModel.create(query, sequelizeOptions)

View File

@ -6,6 +6,7 @@ import { ACTIVITY_PUB } from '../initializers'
import { ActorModel } from '../models/activitypub/actor' import { ActorModel } from '../models/activitypub/actor'
import { signJsonLDObject } from './peertube-crypto' import { signJsonLDObject } from './peertube-crypto'
import { pageToStartAndCount } from './core-utils' import { pageToStartAndCount } from './core-utils'
import { parse } from 'url'
function activityPubContextify <T> (data: T) { function activityPubContextify <T> (data: T) {
return Object.assign(data, { return Object.assign(data, {
@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
return activityActor.id return activityActor.id
} }
function checkUrlsSameHost (url1: string, url2: string) {
const idHost = parse(url1).host
const actorHost = parse(url2).host
return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
checkUrlsSameHost,
getActorUrl, getActorUrl,
activityPubContextify, activityPubContextify,
activityPubCollectionPagination, activityPubCollectionPagination,

View File

@ -3,7 +3,7 @@ import { createWriteStream } from 'fs-extra'
import * as request from 'request' import * as request from 'request'
import { ACTIVITY_PUB } from '../initializers' import { ACTIVITY_PUB } from '../initializers'
function doRequest ( function doRequest <T> (
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
): Bluebird<{ response: request.RequestResponse, body: any }> { ): Bluebird<{ response: request.RequestResponse, body: any }> {
if (requestOptions.activityPub === true) { if (requestOptions.activityPub === true) {
@ -11,7 +11,7 @@ function doRequest (
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
} }
return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => { return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
}) })
} }

View File

@ -16,7 +16,7 @@ let config: IConfig = require('config')
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 285 const LAST_MIGRATION_VERSION = 290
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -336,6 +336,9 @@ const CONSTRAINTS_FIELDS = {
VIDEOS_REDUNDANCY: { VIDEOS_REDUNDANCY: {
URL: { min: 3, max: 2000 } // Length URL: { min: 3, max: 2000 } // Length
}, },
VIDEO_RATES: {
URL: { min: 3, max: 2000 } // Length
},
VIDEOS: { VIDEOS: {
NAME: { min: 3, max: 120 }, // Length NAME: { min: 3, max: 120 }, // Length
LANGUAGE: { min: 1, max: 10 }, // Length LANGUAGE: { min: 1, max: 10 }, // Length

View File

@ -0,0 +1,46 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction,
queryInterface: Sequelize.QueryInterface,
sequelize: Sequelize.Sequelize,
db: any
}): Promise<void> {
{
const data = {
type: Sequelize.STRING(2000),
allowNull: true
}
await utils.queryInterface.addColumn('accountVideoRate', 'url', data)
}
{
const builtUrlQuery = `SELECT "actor"."url" || '/' || "accountVideoRate"."type" || 's/' || "videoId" ` +
'FROM "accountVideoRate" ' +
'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
'INNER JOIN actor ON actor.id = account."actorId" ' +
'WHERE "base".id = "accountVideoRate".id'
const query = 'UPDATE "accountVideoRate" base SET "url" = (' + builtUrlQuery + ') WHERE "url" IS NULL'
await utils.sequelize.query(query)
}
{
const data = {
type: Sequelize.STRING(2000),
allowNull: false,
defaultValue: null
}
await utils.queryInterface.changeColumn('accountVideoRate', 'url', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -5,7 +5,7 @@ import * as url from 'url'
import * as uuidv4 from 'uuid/v4' import * as uuidv4 from 'uuid/v4'
import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
import { getActorUrl } from '../../helpers/activitypub' import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
@ -65,8 +65,12 @@ async function getOrCreateActorAndServerAndModel (
const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
}
try { try {
// Assert we don't recurse another time // Don't recurse another time
ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
} catch (err) { } catch (err) {
logger.error('Cannot get or create account attributed to video channel ' + actor.url) logger.error('Cannot get or create account attributed to video channel ' + actor.url)
@ -297,12 +301,15 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
normalizeActor(requestResult.body) normalizeActor(requestResult.body)
const actorJSON: ActivityPubActor = requestResult.body const actorJSON: ActivityPubActor = requestResult.body
if (isActorObjectValid(actorJSON) === false) { if (isActorObjectValid(actorJSON) === false) {
logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
return { result: undefined, statusCode: requestResult.response.statusCode } return { result: undefined, statusCode: requestResult.response.statusCode }
} }
if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
}
const followersCount = await fetchActorTotalItems(actorJSON.followers) const followersCount = await fetchActorTotalItems(actorJSON.followers)
const followingCount = await fetchActorTotalItems(actorJSON.following) const followingCount = await fetchActorTotalItems(actorJSON.following)

View File

@ -2,6 +2,7 @@ import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
import { doRequest } from '../../helpers/requests' import { doRequest } from '../../helpers/requests'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
logger.info('Crawling ActivityPub data on %s.', uri) logger.info('Crawling ActivityPub data on %s.', uri)
@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
timeout: JOB_REQUEST_TIMEOUT timeout: JOB_REQUEST_TIMEOUT
} }
const response = await doRequest(options) const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
const firstBody = response.body const firstBody = response.body
let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
while (nextLink && i < limit) { while (nextLink && i < limit) {
options.uri = nextLink options.uri = nextLink
const { body } = await doRequest(options) const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
nextLink = body.next nextLink = body.next
i++ i++

View File

@ -1,9 +1 @@
export * from './process' export * from './process'
export * from './process-accept'
export * from './process-announce'
export * from './process-create'
export * from './process-delete'
export * from './process-follow'
export * from './process-like'
export * from './process-undo'
export * from './process-update'

View File

@ -12,6 +12,8 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { Redis } from '../../redis' import { Redis } from '../../redis'
import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateCacheFile } from '../cache-file'
import { immutableAssign } from '../../../tests/utils'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
const activityObject = activity.object const activityObject = activity.object
@ -65,9 +67,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
videoId: video.id, videoId: video.id,
accountId: byAccount.id accountId: byAccount.id
} }
const [ , created ] = await AccountVideoRateModel.findOrCreate({ const [ , created ] = await AccountVideoRateModel.findOrCreate({
where: rate, where: rate,
defaults: rate, defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
transaction: t transaction: t
}) })
if (created === true) await video.increment('dislikes', { transaction: t }) if (created === true) await video.increment('dislikes', { transaction: t })

View File

@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { forwardVideoRelatedActivity } from '../send/utils' import { forwardVideoRelatedActivity } from '../send/utils'
import { getOrCreateVideoAndAccountAndChannel } from '../videos' import { getOrCreateVideoAndAccountAndChannel } from '../videos'
import { immutableAssign } from '../../../tests/utils'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
return retryTransactionWrapper(processLikeVideo, byActor, activity) return retryTransactionWrapper(processLikeVideo, byActor, activity)
@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
} }
const [ , created ] = await AccountVideoRateModel.findOrCreate({ const [ , created ] = await AccountVideoRateModel.findOrCreate({
where: rate, where: rate,
defaults: rate, defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
transaction: t transaction: t
}) })
if (created === true) await video.increment('likes', { transaction: t }) if (created === true) await video.increment('likes', { transaction: t })

View File

@ -55,7 +55,8 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) {
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) let rate = await AccountVideoRateModel.loadByUrl(likeActivity.id, t)
if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t }) await rate.destroy({ transaction: t })
@ -78,7 +79,8 @@ async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo)
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) let rate = await AccountVideoRateModel.loadByUrl(dislike.id, t)
if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
await rate.destroy({ transaction: t }) await rate.destroy({ transaction: t })

View File

@ -1,5 +1,5 @@
import { Activity, ActivityType } from '../../../../shared/models/activitypub' import { Activity, ActivityType } from '../../../../shared/models/activitypub'
import { getActorUrl } from '../../../helpers/activitypub' import { checkUrlsSameHost, getActorUrl } from '../../../helpers/activitypub'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { processAcceptActivity } from './process-accept' import { processAcceptActivity } from './process-accept'
@ -25,11 +25,17 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
Like: processLikeActivity Like: processLikeActivity
} }
async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) { async function processActivities (
activities: Activity[],
options: {
signatureActor?: ActorModel
inboxActor?: ActorModel
outboxUrl?: string
} = {}) {
const actorsCache: { [ url: string ]: ActorModel } = {} const actorsCache: { [ url: string ]: ActorModel } = {}
for (const activity of activities) { for (const activity of activities) {
if (!signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) {
logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type)
continue continue
} }
@ -37,12 +43,17 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
const actorUrl = getActorUrl(activity.actor) const actorUrl = getActorUrl(activity.actor)
// When we fetch remote data, we don't have signature // When we fetch remote data, we don't have signature
if (signatureActor && actorUrl !== signatureActor.url) { if (options.signatureActor && actorUrl !== options.signatureActor.url) {
logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url) logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url)
continue continue
} }
const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) if (options.outboxUrl && checkUrlsSameHost(options.outboxUrl, actorUrl) !== true) {
logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', options.outboxUrl, actorUrl)
continue
}
const byActor = options.signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl)
actorsCache[actorUrl] = byActor actorsCache[actorUrl] = byActor
const activityProcessor = processActivity[activity.type] const activityProcessor = processActivity[activity.type]
@ -52,7 +63,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
} }
try { try {
await activityProcessor(activity, byActor, inboxActor) await activityProcessor(activity, byActor, options.inboxActor)
} catch (err) { } catch (err) {
logger.warn('Cannot process activity %s.', activity.type, { err }) logger.warn('Cannot process activity %s.', activity.type, { err })
} }

View File

@ -95,7 +95,7 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
logger.info('Creating job to send view of %s.', video.url) logger.info('Creating job to send view of %s.', video.url)
const url = getVideoViewActivityPubUrl(byActor, video) const url = getVideoViewActivityPubUrl(byActor, video)
const viewActivity = buildViewActivity(byActor, video) const viewActivity = buildViewActivity(url, byActor, video)
return sendVideoRelatedCreateActivity({ return sendVideoRelatedCreateActivity({
// Use the server actor to send the view // Use the server actor to send the view
@ -111,7 +111,7 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
logger.info('Creating job to dislike %s.', video.url) logger.info('Creating job to dislike %s.', video.url)
const url = getVideoDislikeActivityPubUrl(byActor, video) const url = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = buildDislikeActivity(byActor, video) const dislikeActivity = buildDislikeActivity(url, byActor, video)
return sendVideoRelatedCreateActivity({ return sendVideoRelatedCreateActivity({
byActor, byActor,
@ -136,16 +136,18 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud
) )
} }
function buildDislikeActivity (byActor: ActorModel, video: VideoModel) { function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel) {
return { return {
id: url,
type: 'Dislike', type: 'Dislike',
actor: byActor.url, actor: byActor.url,
object: video.url object: video.url
} }
} }
function buildViewActivity (byActor: ActorModel, video: VideoModel) { function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
return { return {
id: url,
type: 'View', type: 'View',
actor: byActor.url, actor: byActor.url,
object: video.url object: video.url

View File

@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel,
return audiencify( return audiencify(
{ {
type: 'Like' as 'Like',
id: url, id: url,
type: 'Like' as 'Like',
actor: byActor.url, actor: byActor.url,
object: video.url object: video.url
}, },

View File

@ -64,7 +64,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
logger.info('Creating job to undo a dislike of video %s.', video.url) logger.info('Creating job to undo a dislike of video %s.', video.url)
const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
const dislikeActivity = buildDislikeActivity(byActor, video) const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity) const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t })

View File

@ -4,13 +4,14 @@ import { getServerActor } from '../../helpers/utils'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { sendUndoAnnounce, sendVideoAnnounce } from './send' import { sendUndoAnnounce, sendVideoAnnounce } from './send'
import { getAnnounceActivityPubUrl } from './url' import { getVideoAnnounceActivityPubUrl } from './url'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { doRequest } from '../../helpers/requests' import { doRequest } from '../../helpers/requests'
import { getOrCreateActorAndServerAndModel } from './actor' import { getOrCreateActorAndServerAndModel } from './actor'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
if (video.privacy === VideoPrivacy.PRIVATE) return undefined if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
json: true, json: true,
activityPub: true activityPub: true
}) })
if (!body || !body.actor) throw new Error('Body of body actor is invalid') if (!body || !body.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getActorUrl(body.actor)
if (checkUrlsSameHost(shareUrl, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
}
const actorUrl = body.actor
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = { const entry = {
@ -72,7 +77,7 @@ export {
async function shareByServer (video: VideoModel, t: Transaction) { async function shareByServer (video: VideoModel, t: Transaction) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
return VideoShareModel.findOrCreate({ return VideoShareModel.findOrCreate({
defaults: { defaults: {
actorId: serverActor.id, actorId: serverActor.id,
@ -91,7 +96,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
} }
async function shareByVideoChannel (video: VideoModel, t: Transaction) { async function shareByVideoChannel (video: VideoModel, t: Transaction) {
const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
return VideoShareModel.findOrCreate({ return VideoShareModel.findOrCreate({
defaults: { defaults: {
actorId: video.VideoChannel.actorId, actorId: video.VideoChannel.actorId,

View File

@ -33,14 +33,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
} }
function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString() return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString()
} }
function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
return byActor.url + '/likes/' + video.id return byActor.url + '/likes/' + video.id
} }
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
return byActor.url + '/dislikes/' + video.id return byActor.url + '/dislikes/' + video.id
} }
@ -74,8 +74,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
return follower.url + '/accepts/follows/' + me.id return follower.url + '/accepts/follows/' + me.id
} }
function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) { function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return originalUrl + '/announces/' + byActor.id return video.url + '/announces/' + byActor.id
} }
function getDeleteActivityPubUrl (originalUrl: string) { function getDeleteActivityPubUrl (originalUrl: string) {
@ -97,7 +97,7 @@ export {
getVideoAbuseActivityPubUrl, getVideoAbuseActivityPubUrl,
getActorFollowActivityPubUrl, getActorFollowActivityPubUrl,
getActorFollowAcceptActivityPubUrl, getActorFollowAcceptActivityPubUrl,
getAnnounceActivityPubUrl, getVideoAnnounceActivityPubUrl,
getUpdateActivityPubUrl, getUpdateActivityPubUrl,
getUndoActivityPubUrl, getUndoActivityPubUrl,
getVideoViewActivityPubUrl, getVideoViewActivityPubUrl,

View File

@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment'
import { getOrCreateActorAndServerAndModel } from './actor' import { getOrCreateActorAndServerAndModel } from './actor'
import { getOrCreateVideoAndAccountAndChannel } from './videos' import { getOrCreateVideoAndAccountAndChannel } from './videos'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { checkUrlsSameHost } from '../../helpers/activitypub'
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
let originCommentId: number = null let originCommentId: number = null
@ -61,6 +62,14 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
const actorUrl = body.attributedTo const actorUrl = body.attributedTo
if (!actorUrl) return { created: false } if (!actorUrl) return { created: false }
if (checkUrlsSameHost(commentUrl, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${commentUrl}`)
}
if (checkUrlsSameHost(body.id, commentUrl) !== true) {
throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
if (!entry) return { created: false } if (!entry) return { created: false }
@ -134,6 +143,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
const actorUrl = body.attributedTo const actorUrl = body.attributedTo
if (!actorUrl) throw new Error('Miss attributed to in comment') if (!actorUrl) throw new Error('Miss attributed to in comment')
if (checkUrlsSameHost(url, actorUrl) !== true) {
throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
}
if (checkUrlsSameHost(body.id, url) !== true) {
throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const comment = new VideoCommentModel({ const comment = new VideoCommentModel({
url: body.id, url: body.id,

View File

@ -8,13 +8,35 @@ import { getOrCreateActorAndServerAndModel } from './actor'
import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
import { doRequest } from '../../helpers/requests'
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
import { ActorModel } from '../../models/activitypub/actor'
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) { async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) {
let rateCounts = 0 let rateCounts = 0
await Bluebird.map(actorUrls, async actorUrl => { await Bluebird.map(ratesUrl, async rateUrl => {
try { try {
// Fetch url
const { body } = await doRequest({
uri: rateUrl,
json: true,
activityPub: true
})
if (!body || !body.actor) throw new Error('Body or body actor is invalid')
const actorUrl = getActorUrl(body.actor)
if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
}
if (checkUrlsSameHost(body.id, rateUrl) !== true) {
throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
}
const actor = await getOrCreateActorAndServerAndModel(actorUrl) const actor = await getOrCreateActorAndServerAndModel(actorUrl)
const [ , created ] = await AccountVideoRateModel const [ , created ] = await AccountVideoRateModel
.findOrCreate({ .findOrCreate({
where: { where: {
@ -24,13 +46,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
defaults: { defaults: {
videoId: video.id, videoId: video.id,
accountId: actor.Account.id, accountId: actor.Account.id,
type: rate type: rate,
url: body.id
} }
}) })
if (created) rateCounts += 1 if (created) rateCounts += 1
} catch (err) { } catch (err) {
logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err }) logger.warn('Cannot add rate %s.', rateUrl, { err })
} }
}, { concurrency: CRAWL_REQUEST_CONCURRENCY }) }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
@ -62,7 +85,12 @@ async function sendVideoRateChange (account: AccountModel,
if (dislikes > 0) await sendCreateDislike(actor, video, t) if (dislikes > 0) await sendCreateDislike(actor, video, t)
} }
function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) {
return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video)
}
export { export {
getRateUrl,
createRates, createRates,
sendVideoRateChange sendVideoRateChange
} }

View File

@ -29,6 +29,7 @@ import { createRates } from './video-rates'
import { addVideoShares, shareVideoByServerAndChannel } from './share' import { addVideoShares, shareVideoByServerAndChannel } from './share'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
import { checkUrlsSameHost } from '../../helpers/activitypub'
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
// If the video is not private and published, we federate it // If the video is not private and published, we federate it
@ -63,7 +64,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
const { response, body } = await doRequest(options) const { response, body } = await doRequest(options)
if (sanitizeAndCheckVideoTorrentObject(body) === false) { if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
logger.debug('Remote video JSON is not valid.', { body }) logger.debug('Remote video JSON is not valid.', { body })
return { response, videoObject: undefined } return { response, videoObject: undefined }
} }
@ -107,6 +108,10 @@ function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject
const channel = videoObject.attributedTo.find(a => a.type === 'Group') const channel = videoObject.attributedTo.find(a => a.type === 'Group')
if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
if (checkUrlsSameHost(channel.id, videoObject.id) !== true) {
throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`)
}
return getOrCreateActorAndServerAndModel(channel.id, 'all') return getOrCreateActorAndServerAndModel(channel.id, 'all')
} }

View File

@ -23,7 +23,7 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
'activity': items => processActivities(items), 'activity': items => processActivities(items, { outboxUrl: payload.uri }),
'video-likes': items => createRates(items, video, 'like'), 'video-likes': items => createRates(items, video, 'like'),
'video-dislikes': items => createRates(items, video, 'dislike'), 'video-dislikes': items => createRates(items, video, 'dislike'),
'video-shares': items => addVideoShares(items, video), 'video-shares': items => addVideoShares(items, video),

View File

@ -5,4 +5,6 @@ export * from './video-channels'
export * from './video-comments' export * from './video-comments'
export * from './video-imports' export * from './video-imports'
export * from './video-watch' export * from './video-watch'
export * from './video-rates'
export * from './video-shares'
export * from './videos' export * from './videos'

View File

@ -0,0 +1,55 @@
import * as express from 'express'
import 'express-validator'
import { body, param } from 'express-validator/check'
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger'
import { areValidationErrors } from '../utils'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
import { VideoRateType } from '../../../../shared/models/videos'
import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
const videoUpdateRateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoRate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
return next()
}
]
const getAccountVideoRateValidator = function (rateType: VideoRateType) {
return [
param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
if (areValidationErrors(req, res)) return
const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId)
if (!rate) {
return res.status(404)
.json({ error: 'Video rate not found' })
.end()
}
res.locals.accountVideoRate = rate
return next()
}
]
}
// ---------------------------------------------------------------------------
export {
videoUpdateRateValidator,
getAccountVideoRateValidator
}

View File

@ -0,0 +1,38 @@
import * as express from 'express'
import 'express-validator'
import { param } from 'express-validator/check'
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
import { isVideoExist } from '../../../helpers/custom-validators/videos'
import { logger } from '../../../helpers/logger'
import { VideoShareModel } from '../../../models/video/video-share'
import { areValidationErrors } from '../utils'
import { VideoModel } from '../../../models/video/video'
const videosShareValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoShare parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const video: VideoModel = res.locals.video
const share = await VideoShareModel.load(req.params.actorId, video.id)
if (!share) {
return res.status(404)
.end()
}
res.locals.videoShare = share
return next()
}
]
// ---------------------------------------------------------------------------
export {
videosShareValidator
}

View File

@ -26,14 +26,12 @@ import {
isVideoLicenceValid, isVideoLicenceValid,
isVideoNameValid, isVideoNameValid,
isVideoPrivacyValid, isVideoPrivacyValid,
isVideoRatingTypeValid,
isVideoSupportValid, isVideoSupportValid,
isVideoTagsValid isVideoTagsValid
} from '../../../helpers/custom-validators/videos' } from '../../../helpers/custom-validators/videos'
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { CONSTRAINTS_FIELDS } from '../../../initializers' import { CONSTRAINTS_FIELDS } from '../../../initializers'
import { VideoShareModel } from '../../../models/video/video-share'
import { authenticate } from '../../oauth' import { authenticate } from '../../oauth'
import { areValidationErrors } from '../utils' import { areValidationErrors } from '../utils'
import { cleanUpReqFiles } from '../../../helpers/express-utils' import { cleanUpReqFiles } from '../../../helpers/express-utils'
@ -188,41 +186,6 @@ const videosRemoveValidator = [
} }
] ]
const videoRateValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoRate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
return next()
}
]
const videosShareValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoShare parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
if (!share) {
return res.status(404)
.end()
}
res.locals.videoShare = share
return next()
}
]
const videosChangeOwnershipValidator = [ const videosChangeOwnershipValidator = [
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@ -415,9 +378,6 @@ export {
videosGetValidator, videosGetValidator,
videosCustomGetValidator, videosCustomGetValidator,
videosRemoveValidator, videosRemoveValidator,
videosShareValidator,
videoRateValidator,
videosChangeOwnershipValidator, videosChangeOwnershipValidator,
videosTerminateChangeOwnershipValidator, videosTerminateChangeOwnershipValidator,

View File

@ -1,12 +1,14 @@
import { values } from 'lodash' import { values } from 'lodash'
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
import { VideoRateType } from '../../../shared/models/videos' import { VideoRateType } from '../../../shared/models/videos'
import { VIDEO_RATE_TYPES } from '../../initializers' import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers'
import { VideoModel } from '../video/video' import { VideoModel } from '../video/video'
import { AccountModel } from './account' import { AccountModel } from './account'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { throwIfNotValid } from '../utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
/* /*
Account rates per video. Account rates per video.
@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor'
}, },
{ {
fields: [ 'videoId', 'type' ] fields: [ 'videoId', 'type' ]
},
{
fields: [ 'url' ],
unique: true
} }
] ]
}) })
@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
@Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) @Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
type: VideoRateType type: VideoRateType
@AllowNull(false)
@Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
url: string
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
}) })
Account: AccountModel Account: AccountModel
static load (accountId: number, videoId: number, transaction: Transaction) { static load (accountId: number, videoId: number, transaction?: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = { const options: IFindOptions<AccountVideoRateModel> = {
where: { where: {
accountId, accountId,
@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
return AccountVideoRateModel.findOne(options) return AccountVideoRateModel.findOne(options)
} }
static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
videoId,
type: rateType
},
include: [
{
model: AccountModel.unscoped(),
required: true,
include: [
{
attributes: [ 'id', 'url', 'preferredUsername' ],
model: ActorModel.unscoped(),
required: true,
where: {
preferredUsername: accountName
}
}
]
},
{
model: VideoModel.unscoped(),
required: true
}
]
}
if (transaction) options.transaction = transaction
return AccountVideoRateModel.findOne(options)
}
static loadByUrl (url: string, transaction: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
url
}
}
if (transaction) options.transaction = transaction
return AccountVideoRateModel.findOne(options)
}
static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) { static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) {
const query = { const query = {
offset: start, offset: start,

View File

@ -47,7 +47,7 @@ enum ScopeNames {
required: true, required: true,
include: [ include: [
{ {
attributes: [ 'id' ], attributes: [ 'id', 'url' ],
model: () => ActorModel.unscoped(), model: () => ActorModel.unscoped(),
required: true required: true
} }

View File

@ -88,7 +88,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
}) })
Video: VideoModel Video: VideoModel
static load (actorId: number, videoId: number, t: Sequelize.Transaction) { static load (actorId: number, videoId: number, t?: Sequelize.Transaction) {
return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({ return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
where: { where: {
actorId, actorId,

View File

@ -2,7 +2,7 @@
import 'mocha' import 'mocha'
import { flushAndRunMultipleServers, flushTests, killallServers, makeAPRequest, makeFollowRequest, ServerInfo } from '../../utils' import { flushAndRunMultipleServers, flushTests, killallServers, makePOSTAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
import { HTTP_SIGNATURE } from '../../../initializers' import { HTTP_SIGNATURE } from '../../../initializers'
import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
import * as chai from 'chai' import * as chai from 'chai'
@ -63,7 +63,7 @@ describe('Test ActivityPub security', function () {
Digest: buildDigest({ hello: 'coucou' }) Digest: buildDigest({ hello: 'coucou' })
} }
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403) expect(response.statusCode).to.equal(403)
}) })
@ -73,7 +73,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(body) const headers = buildGlobalHeaders(body)
headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403) expect(response.statusCode).to.equal(403)
}) })
@ -85,7 +85,7 @@ describe('Test ActivityPub security', function () {
const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = buildGlobalHeaders(body) const headers = buildGlobalHeaders(body)
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403) expect(response.statusCode).to.equal(403)
}) })
@ -97,7 +97,7 @@ describe('Test ActivityPub security', function () {
const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
const headers = buildGlobalHeaders(body) const headers = buildGlobalHeaders(body)
const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
expect(response.statusCode).to.equal(204) expect(response.statusCode).to.equal(204)
}) })
@ -126,7 +126,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(signedBody) const headers = buildGlobalHeaders(signedBody)
const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403) expect(response.statusCode).to.equal(403)
}) })
@ -147,7 +147,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(signedBody) const headers = buildGlobalHeaders(signedBody)
const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(403) expect(response.statusCode).to.equal(403)
}) })
@ -163,7 +163,7 @@ describe('Test ActivityPub security', function () {
const headers = buildGlobalHeaders(signedBody) const headers = buildGlobalHeaders(signedBody)
const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
expect(response.statusCode).to.equal(204) expect(response.statusCode).to.equal(204)
}) })

View File

@ -3,7 +3,7 @@ import { HTTP_SIGNATURE } from '../../../initializers'
import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
import { activityPubContextify } from '../../../helpers/activitypub' import { activityPubContextify } from '../../../helpers/activitypub'
function makeAPRequest (url: string, body: any, httpSignature: any, headers: any) { function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
const options = { const options = {
method: 'POST', method: 'POST',
uri: url, uri: url,
@ -34,10 +34,10 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
} }
const headers = buildGlobalHeaders(body) const headers = buildGlobalHeaders(body)
return makeAPRequest(to.url, body, httpSignature, headers) return makePOSTAPRequest(to.url, body, httpSignature, headers)
} }
export { export {
makeAPRequest, makePOSTAPRequest,
makeFollowRequest makeFollowRequest
} }

View File

@ -1,5 +1,6 @@
export interface DislikeObject { export interface DislikeObject {
type: 'Dislike', id: string
type: 'Dislike'
actor: string actor: string
object: string object: string
} }

View File

@ -1 +1 @@
export type VideoRateType = 'like' | 'dislike' | 'none' export type VideoRateType = 'like' | 'dislike'