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'
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
this.checkUserRating()
}
- private setRating (nextRating: VideoRateType) {
+ private setRating (nextRating: UserVideoRateType) {
let method
switch (nextRating) {
case 'like':
)
}
- private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
+ private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
let likesToIncrement = 0
let dislikesToIncrement = 0
import { ActorModel } from '../server/models/activitypub/actor'
import {
getAccountActivityPubUrl,
- getAnnounceActivityPubUrl,
+ getVideoAnnounceActivityPubUrl,
getVideoActivityPubUrl, getVideoChannelActivityPubUrl,
getVideoCommentActivityPubUrl
} from '../server/lib/activitypub'
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()
}
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'
import { activityPubResponse } from './utils'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import {
+ getRateUrl,
getVideoCommentsActivityPubUrl,
getVideoDislikesActivityPubUrl,
getVideoLikesActivityPubUrl,
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))),
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))
)
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
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)
// ---------------------------------------------------------------------------
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[] = []
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'
rateVideoRouter.put('/:id/rate',
authenticate,
- asyncMiddleware(videoRateValidator),
+ asyncMiddleware(videoUpdateRateValidator),
asyncRetryTransactionMiddleware(rateVideo)
)
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
// 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)
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, {
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,
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) {
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 }))
})
}
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 285
+const LAST_MIGRATION_VERSION = 290
// ---------------------------------------------------------------------------
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
--- /dev/null
+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
+}
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'
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)
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)
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)
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
while (nextLink && i < limit) {
options.uri = nextLink
- const { body } = await doRequest(options)
+ const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
nextLink = body.next
i++
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'
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
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 })
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)
}
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 })
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 })
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 })
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'
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
}
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]
}
try {
- await activityProcessor(activity, byActor, inboxActor)
+ await activityProcessor(activity, byActor, options.inboxActor)
} catch (err) {
logger.warn('Cannot process activity %s.', activity.type, { err })
}
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
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,
)
}
-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
return audiencify(
{
- type: 'Like' as 'Like',
id: url,
+ type: 'Like' as 'Like',
actor: byActor.url,
object: video.url
},
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 })
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
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 = {
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,
}
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,
}
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
}
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) {
getVideoAbuseActivityPubUrl,
getActorFollowActivityPubUrl,
getActorFollowAcceptActivityPubUrl,
- getAnnounceActivityPubUrl,
+ getVideoAnnounceActivityPubUrl,
getUpdateActivityPubUrl,
getUndoActivityPubUrl,
getVideoViewActivityPubUrl,
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
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 }
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,
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: {
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 })
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
}
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
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 }
}
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')
}
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),
export * from './video-comments'
export * from './video-imports'
export * from './video-watch'
+export * from './video-rates'
+export * from './video-shares'
export * from './videos'
--- /dev/null
+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
+}
--- /dev/null
+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
+}
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'
}
]
-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'),
videosGetValidator,
videosCustomGetValidator,
videosRemoveValidator,
- videosShareValidator,
-
- videoRateValidator,
videosChangeOwnershipValidator,
videosTerminateChangeOwnershipValidator,
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.
},
{
fields: [ 'videoId', 'type' ]
+ },
+ {
+ fields: [ 'url' ],
+ unique: true
}
]
})
@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
})
Account: AccountModel
- static load (accountId: number, videoId: number, transaction: Transaction) {
+ static load (accountId: number, videoId: number, transaction?: Transaction) {
const options: IFindOptions<AccountVideoRateModel> = {
where: {
accountId,
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,
required: true,
include: [
{
- attributes: [ 'id' ],
+ attributes: [ 'id', 'url' ],
model: () => ActorModel.unscoped(),
required: true
}
})
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,
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'
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)
})
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)
})
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)
})
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)
})
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)
})
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)
})
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)
})
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,
}
const headers = buildGlobalHeaders(body)
- return makeAPRequest(to.url, body, httpSignature, headers)
+ return makePOSTAPRequest(to.url, body, httpSignature, headers)
}
export {
- makeAPRequest,
+ makePOSTAPRequest,
makeFollowRequest
}
export interface DislikeObject {
- type: 'Dislike',
+ id: string
+ type: 'Dislike'
actor: string
object: string
}
-export type VideoRateType = 'like' | 'dislike' | 'none'
+export type VideoRateType = 'like' | 'dislike'