diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 65297d7a1..55844f988 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts @@ -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 diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index dda870905..d0151ceb1 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -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 diff --git a/scripts/update-host.ts b/scripts/update-host.ts index 1dc19664d..422a3c9a7 100755 --- a/scripts/update-host.ts +++ b/scripts/update-host.ts @@ -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() } diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 433186179..ffbf1ba19 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -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) diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index 738d155eb..f0e65015b 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts @@ -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[] = [] diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts index dc322bb0c..53952a0a2 100644 --- a/server/controllers/api/videos/rate.ts +++ b/server/controllers/api/videos/rate.ts @@ -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) diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index b0bcfe824..4bf6e387d 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -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 (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, diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index ee9e80404..51facc9e0 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts @@ -3,7 +3,7 @@ import { createWriteStream } from 'fs-extra' import * as request from 'request' import { ACTIVITY_PUB } from '../initializers' -function doRequest ( +function doRequest ( 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 })) }) } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 9aadbe824..ae3d671bb 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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 diff --git a/server/initializers/migrations/0290-account-video-rate-url.ts b/server/initializers/migrations/0290-account-video-rate-url.ts new file mode 100644 index 000000000..bdabf2929 --- /dev/null +++ b/server/initializers/migrations/0290-account-video-rate-url.ts @@ -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 { + { + 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 +} diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 45dd4443d..b16a00669 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -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) diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts index db9ce3293..1b9b14c2e 100644 --- a/server/lib/activitypub/crawl.ts +++ b/server/lib/activitypub/crawl.ts @@ -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 (uri: string, handler: (items: T[]) => Promise | Bluebird) { logger.info('Crawling ActivityPub data on %s.', uri) @@ -14,7 +15,7 @@ async function crawlCollectionPage (uri: string, handler: (items: T[]) => Pr timeout: JOB_REQUEST_TIMEOUT } - const response = await doRequest(options) + const response = await doRequest>(options) const firstBody = response.body let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT @@ -23,7 +24,7 @@ async function crawlCollectionPage (uri: string, handler: (items: T[]) => Pr while (nextLink && i < limit) { options.uri = nextLink - const { body } = await doRequest(options) + const { body } = await doRequest>(options) nextLink = body.next i++ diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts index db4980a72..5466739c1 100644 --- a/server/lib/activitypub/process/index.ts +++ b/server/lib/activitypub/process/index.ts @@ -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' diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index cefe89db0..920d02cd2 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -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 }) diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts index f7200db61..0dca17551 100644 --- a/server/lib/activitypub/process/process-like.ts +++ b/server/lib/activitypub/process/process-like.ts @@ -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 }) diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index ff019cd8c..438a013b6 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts @@ -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 }) diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts index b263f1ea2..b9b255ddf 100644 --- a/server/lib/activitypub/process/process.ts +++ b/server/lib/activitypub/process/process.ts @@ -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 }) } diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 285edba3b..e3fca0a17 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -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 diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index 89307acc6..35227887a 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts @@ -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 }, diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 5236d2cb3..bf1b6e117 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -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 }) diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index 3ff60a97c..d2649e2d5 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts @@ -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, diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index e792be698..38f15448c 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts @@ -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, diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index c8c17f4c4..5868e7297 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts @@ -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, diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index 1619251c3..1854b44c4 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts @@ -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 } diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 3da363c0a..5bd03c8c6 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts @@ -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') } diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts index 42217c27c..67ccfa995 100644 --- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts +++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts @@ -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 } = { - '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), diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index 294783d85..a0d585b93 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts @@ -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' diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts new file mode 100644 index 000000000..793354520 --- /dev/null +++ b/server/middlewares/validators/videos/video-rates.ts @@ -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 +} diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts new file mode 100644 index 000000000..646d7acb1 --- /dev/null +++ b/server/middlewares/validators/videos/video-shares.ts @@ -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 +} diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 9dc52a134..656d161d8 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -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, diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index c99e32012..18762f0c5 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts @@ -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 { @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 { }) Account: AccountModel - static load (accountId: number, videoId: number, transaction: Transaction) { + static load (accountId: number, videoId: number, transaction?: Transaction) { const options: IFindOptions = { where: { accountId, @@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model { return AccountVideoRateModel.findOne(options) } + static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) { + const options: IFindOptions = { + 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 = { + 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, diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index ef9592c04..ecf846821 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts @@ -47,7 +47,7 @@ enum ScopeNames { required: true, include: [ { - attributes: [ 'id' ], + attributes: [ 'id', 'url' ], model: () => ActorModel.unscoped(), required: true } diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index fa9a70d50..c87f71277 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts @@ -88,7 +88,7 @@ export class VideoShareModel extends Model { }) 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, diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts index c5428abbb..e7899bb14 100644 --- a/server/tests/api/activitypub/security.ts +++ b/server/tests/api/activitypub/security.ts @@ -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) }) diff --git a/server/tests/utils/requests/activitypub.ts b/server/tests/utils/requests/activitypub.ts index e3e08ce67..96fee60a8 100644 --- a/server/tests/utils/requests/activitypub.ts +++ b/server/tests/utils/requests/activitypub.ts @@ -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 } diff --git a/shared/models/activitypub/objects/dislike-object.ts b/shared/models/activitypub/objects/dislike-object.ts index 295175774..7218fb784 100644 --- a/shared/models/activitypub/objects/dislike-object.ts +++ b/shared/models/activitypub/objects/dislike-object.ts @@ -1,5 +1,6 @@ export interface DislikeObject { - type: 'Dislike', + id: string + type: 'Dislike' actor: string object: string } diff --git a/shared/models/videos/video-rate.type.ts b/shared/models/videos/video-rate.type.ts index 17aaba5a5..d48774a4b 100644 --- a/shared/models/videos/video-rate.type.ts +++ b/shared/models/videos/video-rate.type.ts @@ -1 +1 @@ -export type VideoRateType = 'like' | 'dislike' | 'none' +export type VideoRateType = 'like' | 'dislike'