Check activities host
This commit is contained in:
parent
df66d81583
commit
5c6d985fae
|
@ -6,11 +6,11 @@ import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } fr
|
|||
import { ResultList } from '../../../../../shared/models/result-list.model'
|
||||
import {
|
||||
UserVideoRate,
|
||||
UserVideoRateType,
|
||||
UserVideoRateUpdate,
|
||||
VideoConstant,
|
||||
VideoFilter,
|
||||
VideoPrivacy,
|
||||
VideoRateType,
|
||||
VideoUpdate
|
||||
} from '../../../../../shared/models/videos'
|
||||
import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
|
||||
|
@ -332,7 +332,7 @@ export class VideoService implements VideosProvider {
|
|||
return privacies
|
||||
}
|
||||
|
||||
private setVideoRate (id: number, rateType: VideoRateType) {
|
||||
private setVideoRate (id: number, rateType: UserVideoRateType) {
|
||||
const url = VideoService.BASE_VIDEO_URL + id + '/rate'
|
||||
const body: UserVideoRateUpdate = {
|
||||
rating: rateType
|
||||
|
|
|
@ -450,7 +450,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.checkUserRating()
|
||||
}
|
||||
|
||||
private setRating (nextRating: VideoRateType) {
|
||||
private setRating (nextRating: UserVideoRateType) {
|
||||
let method
|
||||
switch (nextRating) {
|
||||
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 dislikesToIncrement = 0
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { VideoModel } from '../server/models/video/video'
|
|||
import { ActorModel } from '../server/models/activitypub/actor'
|
||||
import {
|
||||
getAccountActivityPubUrl,
|
||||
getAnnounceActivityPubUrl,
|
||||
getVideoAnnounceActivityPubUrl,
|
||||
getVideoActivityPubUrl, getVideoChannelActivityPubUrl,
|
||||
getVideoCommentActivityPubUrl
|
||||
} from '../server/lib/activitypub'
|
||||
|
@ -78,7 +78,7 @@ async function run () {
|
|||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,17 +3,22 @@ import * as express from 'express'
|
|||
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
|
||||
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
|
||||
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 { buildCreateActivity } from '../../lib/activitypub/send/send-create'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
videosShareValidator,
|
||||
executeIfActivityPub,
|
||||
localAccountValidator,
|
||||
localVideoChannelValidator,
|
||||
videosCustomGetValidator
|
||||
} from '../../middlewares'
|
||||
import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
|
||||
import {
|
||||
getAccountVideoRateValidator,
|
||||
videoCommentGetValidator,
|
||||
videosGetValidator
|
||||
} from '../../middlewares/validators'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { ActorModel } from '../../models/activitypub/actor'
|
||||
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
||||
|
@ -25,6 +30,7 @@ import { cacheRoute } from '../../middlewares/cache'
|
|||
import { activityPubResponse } from './utils'
|
||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||
import {
|
||||
getRateUrl,
|
||||
getVideoCommentsActivityPubUrl,
|
||||
getVideoDislikesActivityPubUrl,
|
||||
getVideoLikesActivityPubUrl,
|
||||
|
@ -48,6 +54,14 @@ activityPubClientRouter.get('/accounts?/:name/following',
|
|||
executeIfActivityPub(asyncMiddleware(localAccountValidator)),
|
||||
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',
|
||||
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(videoAnnouncesController))
|
||||
)
|
||||
activityPubClientRouter.get('/videos/watch/:id/announces/:accountId',
|
||||
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
|
||||
executeIfActivityPub(asyncMiddleware(videosShareValidator)),
|
||||
executeIfActivityPub(asyncMiddleware(videoAnnounceController))
|
||||
)
|
||||
|
@ -133,6 +147,20 @@ async function accountFollowingController (req: express.Request, res: express.Re
|
|||
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) {
|
||||
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)
|
||||
return {
|
||||
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)
|
||||
|
|
|
@ -43,11 +43,13 @@ export {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
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())
|
||||
})
|
||||
|
||||
function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
function inboxController (req: express.Request, res: express.Response) {
|
||||
const rootActivity: RootActivity = req.body
|
||||
let activities: Activity[] = []
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ import * as express from 'express'
|
|||
import { UserVideoRateUpdate } from '../../../../shared'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
|
||||
import { sendVideoRateChange } from '../../../lib/activitypub'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares'
|
||||
import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
|
||||
import { AccountModel } from '../../../models/account/account'
|
||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
|
@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
|
|||
|
||||
rateVideoRouter.put('/:id/rate',
|
||||
authenticate,
|
||||
asyncMiddleware(videoRateValidator),
|
||||
asyncMiddleware(videoUpdateRateValidator),
|
||||
asyncRetryTransactionMiddleware(rateVideo)
|
||||
)
|
||||
|
||||
|
@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
|
|||
const body: UserVideoRateUpdate = req.body
|
||||
const rateType = body.rating
|
||||
const videoInstance: VideoModel = res.locals.video
|
||||
const userAccount: AccountModel = res.locals.oauth.token.User.Account
|
||||
|
||||
await sequelizeTypescript.transaction(async 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)
|
||||
|
||||
let likesToIncrement = 0
|
||||
|
@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
|
|||
// There was a previous rate, update it
|
||||
if (previousRate) {
|
||||
// We will remove the previous rate, so we will need to update the video count attribute
|
||||
if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement--
|
||||
else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
|
||||
if (previousRate.type === 'like') likesToIncrement--
|
||||
else if (previousRate.type === 'dislike') dislikesToIncrement--
|
||||
|
||||
if (rateType === 'none') { // Destroy previous rate
|
||||
await previousRate.destroy(sequelizeOptions)
|
||||
} else { // Update previous rate
|
||||
previousRate.type = rateType
|
||||
previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
|
||||
await previousRate.save(sequelizeOptions)
|
||||
}
|
||||
} else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
|
||||
const query = {
|
||||
accountId: accountInstance.id,
|
||||
videoId: videoInstance.id,
|
||||
type: rateType
|
||||
type: rateType,
|
||||
url: getRateUrl(rateType, userAccount.Actor, videoInstance)
|
||||
}
|
||||
|
||||
await AccountVideoRateModel.create(query, sequelizeOptions)
|
||||
|
|
|
@ -6,6 +6,7 @@ import { ACTIVITY_PUB } from '../initializers'
|
|||
import { ActorModel } from '../models/activitypub/actor'
|
||||
import { signJsonLDObject } from './peertube-crypto'
|
||||
import { pageToStartAndCount } from './core-utils'
|
||||
import { parse } from 'url'
|
||||
|
||||
function activityPubContextify <T> (data: T) {
|
||||
return Object.assign(data, {
|
||||
|
@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
|
|||
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 {
|
||||
checkUrlsSameHost,
|
||||
getActorUrl,
|
||||
activityPubContextify,
|
||||
activityPubCollectionPagination,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createWriteStream } from 'fs-extra'
|
|||
import * as request from 'request'
|
||||
import { ACTIVITY_PUB } from '../initializers'
|
||||
|
||||
function doRequest (
|
||||
function doRequest <T> (
|
||||
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
|
||||
): Bluebird<{ response: request.RequestResponse, body: any }> {
|
||||
if (requestOptions.activityPub === true) {
|
||||
|
@ -11,7 +11,7 @@ function doRequest (
|
|||
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 }))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
URL: { min: 3, max: 2000 } // Length
|
||||
},
|
||||
VIDEO_RATES: {
|
||||
URL: { min: 3, max: 2000 } // Length
|
||||
},
|
||||
VIDEOS: {
|
||||
NAME: { min: 3, max: 120 }, // Length
|
||||
LANGUAGE: { min: 1, max: 10 }, // Length
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -5,7 +5,7 @@ import * as url from 'url'
|
|||
import * as uuidv4 from 'uuid/v4'
|
||||
import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
|
||||
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 { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
|
||||
|
@ -65,8 +65,12 @@ async function getOrCreateActorAndServerAndModel (
|
|||
const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
|
||||
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 {
|
||||
// Assert we don't recurse another time
|
||||
// Don't recurse another time
|
||||
ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
|
||||
} catch (err) {
|
||||
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)
|
||||
|
||||
const actorJSON: ActivityPubActor = requestResult.body
|
||||
|
||||
if (isActorObjectValid(actorJSON) === false) {
|
||||
logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
|
||||
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 followingCount = await fetchActorTotalItems(actorJSON.following)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
|
|||
import { doRequest } from '../../helpers/requests'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
|
||||
|
||||
async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
|
||||
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
|
||||
}
|
||||
|
||||
const response = await doRequest(options)
|
||||
const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
|
||||
const firstBody = response.body
|
||||
|
||||
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) {
|
||||
options.uri = nextLink
|
||||
|
||||
const { body } = await doRequest(options)
|
||||
const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
|
||||
nextLink = body.next
|
||||
i++
|
||||
|
||||
|
|
|
@ -1,9 +1 @@
|
|||
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'
|
||||
|
|
|
@ -12,6 +12,8 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
|||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { Redis } from '../../redis'
|
||||
import { createOrUpdateCacheFile } from '../cache-file'
|
||||
import { immutableAssign } from '../../../tests/utils'
|
||||
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
|
||||
|
||||
async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
|
||||
const activityObject = activity.object
|
||||
|
@ -65,9 +67,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
|
|||
videoId: video.id,
|
||||
accountId: byAccount.id
|
||||
}
|
||||
|
||||
const [ , created ] = await AccountVideoRateModel.findOrCreate({
|
||||
where: rate,
|
||||
defaults: rate,
|
||||
defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
|
||||
transaction: t
|
||||
})
|
||||
if (created === true) await video.increment('dislikes', { transaction: t })
|
||||
|
|
|
@ -5,6 +5,8 @@ import { AccountVideoRateModel } from '../../../models/account/account-video-rat
|
|||
import { ActorModel } from '../../../models/activitypub/actor'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from '../videos'
|
||||
import { immutableAssign } from '../../../tests/utils'
|
||||
import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
|
||||
|
||||
async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
|
||||
return retryTransactionWrapper(processLikeVideo, byActor, activity)
|
||||
|
@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
|
|||
}
|
||||
const [ , created ] = await AccountVideoRateModel.findOrCreate({
|
||||
where: rate,
|
||||
defaults: rate,
|
||||
defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
|
||||
transaction: t
|
||||
})
|
||||
if (created === true) await video.increment('likes', { transaction: t })
|
||||
|
|
|
@ -55,7 +55,8 @@ async function processUndoLike (byActor: ActorModel, activity: ActivityUndo) {
|
|||
return sequelizeTypescript.transaction(async t => {
|
||||
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}.`)
|
||||
|
||||
await rate.destroy({ transaction: t })
|
||||
|
@ -78,7 +79,8 @@ async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo)
|
|||
return sequelizeTypescript.transaction(async t => {
|
||||
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}.`)
|
||||
|
||||
await rate.destroy({ transaction: t })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Activity, ActivityType } from '../../../../shared/models/activitypub'
|
||||
import { getActorUrl } from '../../../helpers/activitypub'
|
||||
import { checkUrlsSameHost, getActorUrl } from '../../../helpers/activitypub'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { ActorModel } from '../../../models/activitypub/actor'
|
||||
import { processAcceptActivity } from './process-accept'
|
||||
|
@ -25,11 +25,17 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
|
|||
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 } = {}
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
@ -37,12 +43,17 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
|
|||
const actorUrl = getActorUrl(activity.actor)
|
||||
|
||||
// When we fetch remote data, we don't have signature
|
||||
if (signatureActor && actorUrl !== signatureActor.url) {
|
||||
logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url)
|
||||
if (options.signatureActor && actorUrl !== options.signatureActor.url) {
|
||||
logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url)
|
||||
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
|
||||
|
||||
const activityProcessor = processActivity[activity.type]
|
||||
|
@ -52,7 +63,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
|
|||
}
|
||||
|
||||
try {
|
||||
await activityProcessor(activity, byActor, inboxActor)
|
||||
await activityProcessor(activity, byActor, options.inboxActor)
|
||||
} catch (err) {
|
||||
logger.warn('Cannot process activity %s.', activity.type, { err })
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
|
|||
logger.info('Creating job to send view of %s.', video.url)
|
||||
|
||||
const url = getVideoViewActivityPubUrl(byActor, video)
|
||||
const viewActivity = buildViewActivity(byActor, video)
|
||||
const viewActivity = buildViewActivity(url, byActor, video)
|
||||
|
||||
return sendVideoRelatedCreateActivity({
|
||||
// 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)
|
||||
|
||||
const url = getVideoDislikeActivityPubUrl(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(url, byActor, video)
|
||||
|
||||
return sendVideoRelatedCreateActivity({
|
||||
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 {
|
||||
id: url,
|
||||
type: 'Dislike',
|
||||
actor: byActor.url,
|
||||
object: video.url
|
||||
}
|
||||
}
|
||||
|
||||
function buildViewActivity (byActor: ActorModel, video: VideoModel) {
|
||||
function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
|
||||
return {
|
||||
id: url,
|
||||
type: 'View',
|
||||
actor: byActor.url,
|
||||
object: video.url
|
||||
|
|
|
@ -24,8 +24,8 @@ function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel,
|
|||
|
||||
return audiencify(
|
||||
{
|
||||
type: 'Like' as 'Like',
|
||||
id: url,
|
||||
type: 'Like' as 'Like',
|
||||
actor: byActor.url,
|
||||
object: video.url
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
||||
const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(byActor, video)
|
||||
const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
|
||||
const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
|
||||
|
||||
return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t })
|
||||
|
|
|
@ -4,13 +4,14 @@ import { getServerActor } from '../../helpers/utils'
|
|||
import { VideoModel } from '../../models/video/video'
|
||||
import { VideoShareModel } from '../../models/video/video-share'
|
||||
import { sendUndoAnnounce, sendVideoAnnounce } from './send'
|
||||
import { getAnnounceActivityPubUrl } from './url'
|
||||
import { getVideoAnnounceActivityPubUrl } from './url'
|
||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { doRequest } from '../../helpers/requests'
|
||||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { logger } from '../../helpers/logger'
|
||||
import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
|
||||
import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
|
||||
|
||||
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
|
||||
if (video.privacy === VideoPrivacy.PRIVATE) return undefined
|
||||
|
@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
|
|||
json: 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 entry = {
|
||||
|
@ -72,7 +77,7 @@ export {
|
|||
async function shareByServer (video: VideoModel, t: Transaction) {
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor)
|
||||
const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
|
||||
return VideoShareModel.findOrCreate({
|
||||
defaults: {
|
||||
actorId: serverActor.id,
|
||||
|
@ -91,7 +96,7 @@ async function shareByServer (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({
|
||||
defaults: {
|
||||
actorId: video.VideoChannel.actorId,
|
||||
|
|
|
@ -33,14 +33,14 @@ function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) {
|
||||
function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
|
||||
return byActor.url + '/dislikes/' + video.id
|
||||
}
|
||||
|
||||
|
@ -74,8 +74,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
|
|||
return follower.url + '/accepts/follows/' + me.id
|
||||
}
|
||||
|
||||
function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) {
|
||||
return originalUrl + '/announces/' + byActor.id
|
||||
function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
|
||||
return video.url + '/announces/' + byActor.id
|
||||
}
|
||||
|
||||
function getDeleteActivityPubUrl (originalUrl: string) {
|
||||
|
@ -97,7 +97,7 @@ export {
|
|||
getVideoAbuseActivityPubUrl,
|
||||
getActorFollowActivityPubUrl,
|
||||
getActorFollowAcceptActivityPubUrl,
|
||||
getAnnounceActivityPubUrl,
|
||||
getVideoAnnounceActivityPubUrl,
|
||||
getUpdateActivityPubUrl,
|
||||
getUndoActivityPubUrl,
|
||||
getVideoViewActivityPubUrl,
|
||||
|
|
|
@ -9,6 +9,7 @@ import { VideoCommentModel } from '../../models/video/video-comment'
|
|||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { getOrCreateVideoAndAccountAndChannel } from './videos'
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { checkUrlsSameHost } from '../../helpers/activitypub'
|
||||
|
||||
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
|
||||
let originCommentId: number = null
|
||||
|
@ -61,6 +62,14 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
|
|||
const actorUrl = body.attributedTo
|
||||
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 entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
|
||||
if (!entry) return { created: false }
|
||||
|
@ -134,6 +143,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
|
|||
const actorUrl = body.attributedTo
|
||||
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 comment = new VideoCommentModel({
|
||||
url: body.id,
|
||||
|
|
|
@ -8,13 +8,35 @@ import { getOrCreateActorAndServerAndModel } from './actor'
|
|||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||
import { logger } from '../../helpers/logger'
|
||||
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
|
||||
|
||||
await Bluebird.map(actorUrls, async actorUrl => {
|
||||
await Bluebird.map(ratesUrl, async rateUrl => {
|
||||
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 [ , created ] = await AccountVideoRateModel
|
||||
.findOrCreate({
|
||||
where: {
|
||||
|
@ -24,13 +46,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
|
|||
defaults: {
|
||||
videoId: video.id,
|
||||
accountId: actor.Account.id,
|
||||
type: rate
|
||||
type: rate,
|
||||
url: body.id
|
||||
}
|
||||
})
|
||||
|
||||
if (created) rateCounts += 1
|
||||
} 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 })
|
||||
|
||||
|
@ -62,7 +85,12 @@ async function sendVideoRateChange (account: AccountModel,
|
|||
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 {
|
||||
getRateUrl,
|
||||
createRates,
|
||||
sendVideoRateChange
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { createRates } from './video-rates'
|
|||
import { addVideoShares, shareVideoByServerAndChannel } from './share'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
|
||||
import { checkUrlsSameHost } from '../../helpers/activitypub'
|
||||
|
||||
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
|
||||
// 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)
|
||||
|
||||
if (sanitizeAndCheckVideoTorrentObject(body) === false) {
|
||||
if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
|
||||
logger.debug('Remote video JSON is not valid.', { body })
|
||||
return { response, videoObject: undefined }
|
||||
}
|
||||
|
@ -107,6 +108,10 @@ function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject
|
|||
const channel = videoObject.attributedTo.find(a => a.type === 'Group')
|
||||
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')
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
|
|||
if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
|
||||
|
||||
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-dislikes': items => createRates(items, video, 'dislike'),
|
||||
'video-shares': items => addVideoShares(items, video),
|
||||
|
|
|
@ -5,4 +5,6 @@ export * from './video-channels'
|
|||
export * from './video-comments'
|
||||
export * from './video-imports'
|
||||
export * from './video-watch'
|
||||
export * from './video-rates'
|
||||
export * from './video-shares'
|
||||
export * from './videos'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -26,14 +26,12 @@ import {
|
|||
isVideoLicenceValid,
|
||||
isVideoNameValid,
|
||||
isVideoPrivacyValid,
|
||||
isVideoRatingTypeValid,
|
||||
isVideoSupportValid,
|
||||
isVideoTagsValid
|
||||
} from '../../../helpers/custom-validators/videos'
|
||||
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||
import { VideoShareModel } from '../../../models/video/video-share'
|
||||
import { authenticate } from '../../oauth'
|
||||
import { areValidationErrors } from '../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 = [
|
||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
|
@ -415,9 +378,6 @@ export {
|
|||
videosGetValidator,
|
||||
videosCustomGetValidator,
|
||||
videosRemoveValidator,
|
||||
videosShareValidator,
|
||||
|
||||
videoRateValidator,
|
||||
|
||||
videosChangeOwnershipValidator,
|
||||
videosTerminateChangeOwnershipValidator,
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { values } from 'lodash'
|
||||
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 { 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 { AccountModel } from './account'
|
||||
import { ActorModel } from '../activitypub/actor'
|
||||
import { throwIfNotValid } from '../utils'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
|
||||
/*
|
||||
Account rates per video.
|
||||
|
@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor'
|
|||
},
|
||||
{
|
||||
fields: [ 'videoId', 'type' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'url' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
@Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
|
||||
type: VideoRateType
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
|
||||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
|
||||
url: string
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
|
@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
})
|
||||
Account: AccountModel
|
||||
|
||||
static load (accountId: number, videoId: number, transaction: Transaction) {
|
||||
static load (accountId: number, videoId: number, transaction?: Transaction) {
|
||||
const options: IFindOptions<AccountVideoRateModel> = {
|
||||
where: {
|
||||
accountId,
|
||||
|
@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
|||
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) {
|
||||
const query = {
|
||||
offset: start,
|
||||
|
|
|
@ -47,7 +47,7 @@ enum ScopeNames {
|
|||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'id' ],
|
||||
attributes: [ 'id', 'url' ],
|
||||
model: () => ActorModel.unscoped(),
|
||||
required: true
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
|
|||
})
|
||||
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({
|
||||
where: {
|
||||
actorId,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
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 { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
|
||||
import * as chai from 'chai'
|
||||
|
@ -63,7 +63,7 @@ describe('Test ActivityPub security', function () {
|
|||
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)
|
||||
})
|
||||
|
@ -73,7 +73,7 @@ describe('Test ActivityPub security', function () {
|
|||
const headers = buildGlobalHeaders(body)
|
||||
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)
|
||||
})
|
||||
|
@ -85,7 +85,7 @@ describe('Test ActivityPub security', function () {
|
|||
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
|
||||
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)
|
||||
})
|
||||
|
@ -97,7 +97,7 @@ describe('Test ActivityPub security', function () {
|
|||
const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
|
||||
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)
|
||||
})
|
||||
|
@ -126,7 +126,7 @@ describe('Test ActivityPub security', function () {
|
|||
|
||||
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)
|
||||
})
|
||||
|
@ -147,7 +147,7 @@ describe('Test ActivityPub security', function () {
|
|||
|
||||
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)
|
||||
})
|
||||
|
@ -163,7 +163,7 @@ describe('Test ActivityPub security', function () {
|
|||
|
||||
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)
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ import { HTTP_SIGNATURE } from '../../../initializers'
|
|||
import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
|
||||
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 = {
|
||||
method: 'POST',
|
||||
uri: url,
|
||||
|
@ -34,10 +34,10 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
|
|||
}
|
||||
const headers = buildGlobalHeaders(body)
|
||||
|
||||
return makeAPRequest(to.url, body, httpSignature, headers)
|
||||
return makePOSTAPRequest(to.url, body, httpSignature, headers)
|
||||
}
|
||||
|
||||
export {
|
||||
makeAPRequest,
|
||||
makePOSTAPRequest,
|
||||
makeFollowRequest
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export interface DislikeObject {
|
||||
type: 'Dislike',
|
||||
id: string
|
||||
type: 'Dislike'
|
||||
actor: string
|
||||
object: string
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
export type VideoRateType = 'like' | 'dislike' | 'none'
|
||||
export type VideoRateType = 'like' | 'dislike'
|
||||
|
|
Loading…
Reference in New Issue