Browse Source

Check activities host

Chocobozzz 5 years ago
parent
commit
5c6d985fae
37 changed files with 403 additions and 127 deletions
  1. 2 2
      client/src/app/shared/video/video.service.ts
  2. 2 2
      client/src/app/videos/+video-watch/video-watch.component.ts
  3. 2 2
      scripts/update-host.ts
  4. 32 4
      server/controllers/activitypub/client.ts
  5. 4 2
      server/controllers/activitypub/inbox.ts
  6. 10 7
      server/controllers/api/videos/rate.ts
  7. 9 0
      server/helpers/activitypub.ts
  8. 2 2
      server/helpers/requests.ts
  9. 4 1
      server/initializers/constants.ts
  10. 46 0
      server/initializers/migrations/0290-account-video-rate-url.ts
  11. 10 3
      server/lib/activitypub/actor.ts
  12. 3 2
      server/lib/activitypub/crawl.ts
  13. 0 8
      server/lib/activitypub/process/index.ts
  14. 4 1
      server/lib/activitypub/process/process-create.ts
  15. 3 1
      server/lib/activitypub/process/process-like.ts
  16. 4 2
      server/lib/activitypub/process/process-undo.ts
  17. 18 7
      server/lib/activitypub/process/process.ts
  18. 6 4
      server/lib/activitypub/send/send-create.ts
  19. 1 1
      server/lib/activitypub/send/send-like.ts
  20. 1 1
      server/lib/activitypub/send/send-undo.ts
  21. 10 5
      server/lib/activitypub/share.ts
  22. 6 6
      server/lib/activitypub/url.ts
  23. 17 0
      server/lib/activitypub/video-comments.ts
  24. 32 4
      server/lib/activitypub/video-rates.ts
  25. 6 1
      server/lib/activitypub/videos.ts
  26. 1 1
      server/lib/job-queue/handlers/activitypub-http-fetcher.ts
  27. 2 0
      server/middlewares/validators/videos/index.ts
  28. 55 0
      server/middlewares/validators/videos/video-rates.ts
  29. 38 0
      server/middlewares/validators/videos/video-shares.ts
  30. 0 40
      server/middlewares/validators/videos/videos.ts
  31. 57 3
      server/models/account/account-video-rate.ts
  32. 1 1
      server/models/oauth/oauth-token.ts
  33. 1 1
      server/models/video/video-share.ts
  34. 8 8
      server/tests/api/activitypub/security.ts
  35. 3 3
      server/tests/utils/requests/activitypub.ts
  36. 2 1
      shared/models/activitypub/objects/dislike-object.ts
  37. 1 1
      shared/models/videos/video-rate.type.ts

+ 2 - 2
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

+ 2 - 2
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
 

+ 2 - 2
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()
   }
 

+ 32 - 4
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)

+ 4 - 2
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[] = []
 

+ 10 - 7
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)

+ 9 - 0
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 <T> (data: T) {
   return Object.assign(data, {
@@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
   return activityActor.id
 }
 
+function checkUrlsSameHost (url1: string, url2: string) {
+  const idHost = parse(url1).host
+  const actorHost = parse(url2).host
+
+  return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
+}
+
 // ---------------------------------------------------------------------------
 
 export {
+  checkUrlsSameHost,
   getActorUrl,
   activityPubContextify,
   activityPubCollectionPagination,

+ 2 - 2
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 <T> (
   requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
 ): Bluebird<{ response: request.RequestResponse, body: any }> {
   if (requestOptions.activityPub === true) {
@@ -11,7 +11,7 @@ function doRequest (
     requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
   }
 
-  return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => {
+  return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
     request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
   })
 }

+ 4 - 1
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

+ 46 - 0
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<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
+}

+ 10 - 3
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)
 

+ 3 - 2
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 <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
   logger.info('Crawling ActivityPub data on %s.', uri)
@@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
     timeout: JOB_REQUEST_TIMEOUT
   }
 
-  const response = await doRequest(options)
+  const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
   const firstBody = response.body
 
   let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
   while (nextLink && i < limit) {
     options.uri = nextLink
 
-    const { body } = await doRequest(options)
+    const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
     nextLink = body.next
     i++
 

+ 0 - 8
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'

+ 4 - 1
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 })

+ 3 - 1
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 })

+ 4 - 2
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 })

+ 18 - 7
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 })
     }

+ 6 - 4
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

+ 1 - 1
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
     },

+ 1 - 1
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 })

+ 10 - 5
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,

+ 6 - 6
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,

+ 17 - 0
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,

+ 32 - 4
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
 }

+ 6 - 1
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')
 }
 

+ 1 - 1
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<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),

+ 2 - 0
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'

+ 55 - 0
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
+}

+ 38 - 0
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
+}

+ 0 - 40
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,

+ 57 - 3
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<AccountVideoRateModel> {
   @Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
   type: VideoRateType
 
+  @AllowNull(false)
+  @Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
+  url: string
+
   @CreatedAt
   createdAt: Date
 
@@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
   })
   Account: AccountModel
 
-  static load (accountId: number, videoId: number, transaction: Transaction) {
+  static load (accountId: number, videoId: number, transaction?: Transaction) {
     const options: IFindOptions<AccountVideoRateModel> = {
       where: {
         accountId,
@@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
     return AccountVideoRateModel.findOne(options)
   }
 
+  static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
+    const options: IFindOptions<AccountVideoRateModel> = {
+      where: {
+        videoId,
+        type: rateType
+      },
+      include: [
+        {
+          model: AccountModel.unscoped(),
+          required: true,
+          include: [
+            {
+              attributes: [ 'id', 'url', 'preferredUsername' ],
+              model: ActorModel.unscoped(),
+              required: true,
+              where: {
+                preferredUsername: accountName
+              }
+            }
+          ]
+        },
+        {
+          model: VideoModel.unscoped(),
+          required: true
+        }
+      ]
+    }
+    if (transaction) options.transaction = transaction
+
+    return AccountVideoRateModel.findOne(options)
+  }
+
+  static loadByUrl (url: string, transaction: Transaction) {
+    const options: IFindOptions<AccountVideoRateModel> = {
+      where: {
+        url
+      }
+    }
+    if (transaction) options.transaction = transaction
+
+    return AccountVideoRateModel.findOne(options)
+  }
+
   static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) {
     const query = {
       offset: start,

+ 1 - 1
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
               }

+ 1 - 1
server/models/video/video-share.ts

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

+ 8 - 8
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)
     })

+ 3 - 3
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
 }

+ 2 - 1
shared/models/activitypub/objects/dislike-object.ts

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

+ 1 - 1
shared/models/videos/video-rate.type.ts

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