aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/shared/video/video.service.ts4
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts4
-rwxr-xr-xscripts/update-host.ts4
-rw-r--r--server/controllers/activitypub/client.ts36
-rw-r--r--server/controllers/activitypub/inbox.ts6
-rw-r--r--server/controllers/api/videos/rate.ts17
-rw-r--r--server/helpers/activitypub.ts9
-rw-r--r--server/helpers/requests.ts4
-rw-r--r--server/initializers/constants.ts5
-rw-r--r--server/initializers/migrations/0290-account-video-rate-url.ts46
-rw-r--r--server/lib/activitypub/actor.ts13
-rw-r--r--server/lib/activitypub/crawl.ts5
-rw-r--r--server/lib/activitypub/process/index.ts8
-rw-r--r--server/lib/activitypub/process/process-create.ts5
-rw-r--r--server/lib/activitypub/process/process-like.ts4
-rw-r--r--server/lib/activitypub/process/process-undo.ts6
-rw-r--r--server/lib/activitypub/process/process.ts25
-rw-r--r--server/lib/activitypub/send/send-create.ts10
-rw-r--r--server/lib/activitypub/send/send-like.ts2
-rw-r--r--server/lib/activitypub/send/send-undo.ts2
-rw-r--r--server/lib/activitypub/share.ts15
-rw-r--r--server/lib/activitypub/url.ts12
-rw-r--r--server/lib/activitypub/video-comments.ts17
-rw-r--r--server/lib/activitypub/video-rates.ts36
-rw-r--r--server/lib/activitypub/videos.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts2
-rw-r--r--server/middlewares/validators/videos/index.ts2
-rw-r--r--server/middlewares/validators/videos/video-rates.ts55
-rw-r--r--server/middlewares/validators/videos/video-shares.ts38
-rw-r--r--server/middlewares/validators/videos/videos.ts40
-rw-r--r--server/models/account/account-video-rate.ts60
-rw-r--r--server/models/oauth/oauth-token.ts2
-rw-r--r--server/models/video/video-share.ts2
-rw-r--r--server/tests/api/activitypub/security.ts16
-rw-r--r--server/tests/utils/requests/activitypub.ts6
-rw-r--r--shared/models/activitypub/objects/dislike-object.ts3
-rw-r--r--shared/models/videos/video-rate.type.ts2
37 files changed, 403 insertions, 127 deletions
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
6import { ResultList } from '../../../../../shared/models/result-list.model' 6import { ResultList } from '../../../../../shared/models/result-list.model'
7import { 7import {
8 UserVideoRate, 8 UserVideoRate,
9 UserVideoRateType,
9 UserVideoRateUpdate, 10 UserVideoRateUpdate,
10 VideoConstant, 11 VideoConstant,
11 VideoFilter, 12 VideoFilter,
12 VideoPrivacy, 13 VideoPrivacy,
13 VideoRateType,
14 VideoUpdate 14 VideoUpdate
15} from '../../../../../shared/models/videos' 15} from '../../../../../shared/models/videos'
16import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' 16import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
@@ -332,7 +332,7 @@ export class VideoService implements VideosProvider {
332 return privacies 332 return privacies
333 } 333 }
334 334
335 private setVideoRate (id: number, rateType: VideoRateType) { 335 private setVideoRate (id: number, rateType: UserVideoRateType) {
336 const url = VideoService.BASE_VIDEO_URL + id + '/rate' 336 const url = VideoService.BASE_VIDEO_URL + id + '/rate'
337 const body: UserVideoRateUpdate = { 337 const body: UserVideoRateUpdate = {
338 rating: rateType 338 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 {
450 this.checkUserRating() 450 this.checkUserRating()
451 } 451 }
452 452
453 private setRating (nextRating: VideoRateType) { 453 private setRating (nextRating: UserVideoRateType) {
454 let method 454 let method
455 switch (nextRating) { 455 switch (nextRating) {
456 case 'like': 456 case 'like':
@@ -476,7 +476,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
476 ) 476 )
477 } 477 }
478 478
479 private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { 479 private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
480 let likesToIncrement = 0 480 let likesToIncrement = 0
481 let dislikesToIncrement = 0 481 let dislikesToIncrement = 0
482 482
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'
4import { ActorModel } from '../server/models/activitypub/actor' 4import { ActorModel } from '../server/models/activitypub/actor'
5import { 5import {
6 getAccountActivityPubUrl, 6 getAccountActivityPubUrl,
7 getAnnounceActivityPubUrl, 7 getVideoAnnounceActivityPubUrl,
8 getVideoActivityPubUrl, getVideoChannelActivityPubUrl, 8 getVideoActivityPubUrl, getVideoChannelActivityPubUrl,
9 getVideoCommentActivityPubUrl 9 getVideoCommentActivityPubUrl
10} from '../server/lib/activitypub' 10} from '../server/lib/activitypub'
@@ -78,7 +78,7 @@ async function run () {
78 78
79 console.log('Updating video share ' + videoShare.url) 79 console.log('Updating video share ' + videoShare.url)
80 80
81 videoShare.url = getAnnounceActivityPubUrl(videoShare.Video.url, videoShare.Actor) 81 videoShare.url = getVideoAnnounceActivityPubUrl(videoShare.Actor, videoShare.Video)
82 await videoShare.save() 82 await videoShare.save()
83 } 83 }
84 84
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'
3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' 4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
5import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' 5import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers'
6import { buildAnnounceWithVideoAudience } from '../../lib/activitypub/send' 6import { buildAnnounceWithVideoAudience, buildDislikeActivity, buildLikeActivity } from '../../lib/activitypub/send'
7import { audiencify, getAudience } from '../../lib/activitypub/audience' 7import { audiencify, getAudience } from '../../lib/activitypub/audience'
8import { buildCreateActivity } from '../../lib/activitypub/send/send-create' 8import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
9import { 9import {
10 asyncMiddleware, 10 asyncMiddleware,
11 videosShareValidator,
11 executeIfActivityPub, 12 executeIfActivityPub,
12 localAccountValidator, 13 localAccountValidator,
13 localVideoChannelValidator, 14 localVideoChannelValidator,
14 videosCustomGetValidator 15 videosCustomGetValidator
15} from '../../middlewares' 16} from '../../middlewares'
16import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators' 17import {
18 getAccountVideoRateValidator,
19 videoCommentGetValidator,
20 videosGetValidator
21} from '../../middlewares/validators'
17import { AccountModel } from '../../models/account/account' 22import { AccountModel } from '../../models/account/account'
18import { ActorModel } from '../../models/activitypub/actor' 23import { ActorModel } from '../../models/activitypub/actor'
19import { ActorFollowModel } from '../../models/activitypub/actor-follow' 24import { ActorFollowModel } from '../../models/activitypub/actor-follow'
@@ -25,6 +30,7 @@ import { cacheRoute } from '../../middlewares/cache'
25import { activityPubResponse } from './utils' 30import { activityPubResponse } from './utils'
26import { AccountVideoRateModel } from '../../models/account/account-video-rate' 31import { AccountVideoRateModel } from '../../models/account/account-video-rate'
27import { 32import {
33 getRateUrl,
28 getVideoCommentsActivityPubUrl, 34 getVideoCommentsActivityPubUrl,
29 getVideoDislikesActivityPubUrl, 35 getVideoDislikesActivityPubUrl,
30 getVideoLikesActivityPubUrl, 36 getVideoLikesActivityPubUrl,
@@ -48,6 +54,14 @@ activityPubClientRouter.get('/accounts?/:name/following',
48 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 54 executeIfActivityPub(asyncMiddleware(localAccountValidator)),
49 executeIfActivityPub(asyncMiddleware(accountFollowingController)) 55 executeIfActivityPub(asyncMiddleware(accountFollowingController))
50) 56)
57activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
58 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))),
59 executeIfActivityPub(getAccountVideoRate('like'))
60)
61activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
62 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))),
63 executeIfActivityPub(getAccountVideoRate('dislike'))
64)
51 65
52activityPubClientRouter.get('/videos/watch/:id', 66activityPubClientRouter.get('/videos/watch/:id',
53 executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), 67 executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))),
@@ -62,7 +76,7 @@ activityPubClientRouter.get('/videos/watch/:id/announces',
62 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), 76 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))),
63 executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) 77 executeIfActivityPub(asyncMiddleware(videoAnnouncesController))
64) 78)
65activityPubClientRouter.get('/videos/watch/:id/announces/:accountId', 79activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
66 executeIfActivityPub(asyncMiddleware(videosShareValidator)), 80 executeIfActivityPub(asyncMiddleware(videosShareValidator)),
67 executeIfActivityPub(asyncMiddleware(videoAnnounceController)) 81 executeIfActivityPub(asyncMiddleware(videoAnnounceController))
68) 82)
@@ -133,6 +147,20 @@ async function accountFollowingController (req: express.Request, res: express.Re
133 return activityPubResponse(activityPubContextify(activityPubResult), res) 147 return activityPubResponse(activityPubContextify(activityPubResult), res)
134} 148}
135 149
150function getAccountVideoRate (rateType: VideoRateType) {
151 return (req: express.Request, res: express.Response) => {
152 const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate
153
154 const byActor = accountVideoRate.Account.Actor
155 const url = getRateUrl(rateType, byActor, accountVideoRate.Video)
156 const APObject = rateType === 'like'
157 ? buildLikeActivity(url, byActor, accountVideoRate.Video)
158 : buildCreateActivity(url, byActor, buildDislikeActivity(url, byActor, accountVideoRate.Video))
159
160 return activityPubResponse(activityPubContextify(APObject), res)
161 }
162}
163
136async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 164async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
137 const video: VideoModel = res.locals.video 165 const video: VideoModel = res.locals.video
138 166
@@ -276,7 +304,7 @@ function videoRates (req: express.Request, rateType: VideoRateType, video: Video
276 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) 304 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
277 return { 305 return {
278 total: result.count, 306 total: result.count,
279 data: result.rows.map(r => r.Account.Actor.url) 307 data: result.rows.map(r => r.url)
280 } 308 }
281 } 309 }
282 return activityPubCollectionPagination(url, handler, req.query.page) 310 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 {
43// --------------------------------------------------------------------------- 43// ---------------------------------------------------------------------------
44 44
45const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { 45const inboxQueue = queue<{ activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel }, Error>((task, cb) => {
46 processActivities(task.activities, task.signatureActor, task.inboxActor) 46 const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
47
48 processActivities(task.activities, options)
47 .then(() => cb()) 49 .then(() => cb())
48}) 50})
49 51
50function inboxController (req: express.Request, res: express.Response, next: express.NextFunction) { 52function inboxController (req: express.Request, res: express.Response) {
51 const rootActivity: RootActivity = req.body 53 const rootActivity: RootActivity = req.body
52 let activities: Activity[] = [] 54 let activities: Activity[] = []
53 55
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'
2import { UserVideoRateUpdate } from '../../../../shared' 2import { UserVideoRateUpdate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' 4import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers'
5import { sendVideoRateChange } from '../../../lib/activitypub' 5import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoRateValidator } from '../../../middlewares' 6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
7import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 8import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoModel } from '../../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
@@ -12,7 +12,7 @@ const rateVideoRouter = express.Router()
12 12
13rateVideoRouter.put('/:id/rate', 13rateVideoRouter.put('/:id/rate',
14 authenticate, 14 authenticate,
15 asyncMiddleware(videoRateValidator), 15 asyncMiddleware(videoUpdateRateValidator),
16 asyncRetryTransactionMiddleware(rateVideo) 16 asyncRetryTransactionMiddleware(rateVideo)
17) 17)
18 18
@@ -28,11 +28,12 @@ async function rateVideo (req: express.Request, res: express.Response) {
28 const body: UserVideoRateUpdate = req.body 28 const body: UserVideoRateUpdate = req.body
29 const rateType = body.rating 29 const rateType = body.rating
30 const videoInstance: VideoModel = res.locals.video 30 const videoInstance: VideoModel = res.locals.video
31 const userAccount: AccountModel = res.locals.oauth.token.User.Account
31 32
32 await sequelizeTypescript.transaction(async t => { 33 await sequelizeTypescript.transaction(async t => {
33 const sequelizeOptions = { transaction: t } 34 const sequelizeOptions = { transaction: t }
34 35
35 const accountInstance = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 36 const accountInstance = await AccountModel.load(userAccount.id, t)
36 const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t) 37 const previousRate = await AccountVideoRateModel.load(accountInstance.id, videoInstance.id, t)
37 38
38 let likesToIncrement = 0 39 let likesToIncrement = 0
@@ -44,20 +45,22 @@ async function rateVideo (req: express.Request, res: express.Response) {
44 // There was a previous rate, update it 45 // There was a previous rate, update it
45 if (previousRate) { 46 if (previousRate) {
46 // We will remove the previous rate, so we will need to update the video count attribute 47 // We will remove the previous rate, so we will need to update the video count attribute
47 if (previousRate.type === VIDEO_RATE_TYPES.LIKE) likesToIncrement-- 48 if (previousRate.type === 'like') likesToIncrement--
48 else if (previousRate.type === VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement-- 49 else if (previousRate.type === 'dislike') dislikesToIncrement--
49 50
50 if (rateType === 'none') { // Destroy previous rate 51 if (rateType === 'none') { // Destroy previous rate
51 await previousRate.destroy(sequelizeOptions) 52 await previousRate.destroy(sequelizeOptions)
52 } else { // Update previous rate 53 } else { // Update previous rate
53 previousRate.type = rateType 54 previousRate.type = rateType
55 previousRate.url = getRateUrl(rateType, userAccount.Actor, videoInstance)
54 await previousRate.save(sequelizeOptions) 56 await previousRate.save(sequelizeOptions)
55 } 57 }
56 } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate 58 } else if (rateType !== 'none') { // There was not a previous rate, insert a new one if there is a rate
57 const query = { 59 const query = {
58 accountId: accountInstance.id, 60 accountId: accountInstance.id,
59 videoId: videoInstance.id, 61 videoId: videoInstance.id,
60 type: rateType 62 type: rateType,
63 url: getRateUrl(rateType, userAccount.Actor, videoInstance)
61 } 64 }
62 65
63 await AccountVideoRateModel.create(query, sequelizeOptions) 66 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'
6import { ActorModel } from '../models/activitypub/actor' 6import { ActorModel } from '../models/activitypub/actor'
7import { signJsonLDObject } from './peertube-crypto' 7import { signJsonLDObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
9import { parse } from 'url'
9 10
10function activityPubContextify <T> (data: T) { 11function activityPubContextify <T> (data: T) {
11 return Object.assign(data, { 12 return Object.assign(data, {
@@ -111,9 +112,17 @@ function getActorUrl (activityActor: string | ActivityPubActor) {
111 return activityActor.id 112 return activityActor.id
112} 113}
113 114
115function checkUrlsSameHost (url1: string, url2: string) {
116 const idHost = parse(url1).host
117 const actorHost = parse(url2).host
118
119 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
120}
121
114// --------------------------------------------------------------------------- 122// ---------------------------------------------------------------------------
115 123
116export { 124export {
125 checkUrlsSameHost,
117 getActorUrl, 126 getActorUrl,
118 activityPubContextify, 127 activityPubContextify,
119 activityPubCollectionPagination, 128 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'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB } from '../initializers' 4import { ACTIVITY_PUB } from '../initializers'
5 5
6function doRequest ( 6function doRequest <T> (
7 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 7 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
8): Bluebird<{ response: request.RequestResponse, body: any }> { 8): Bluebird<{ response: request.RequestResponse, body: any }> {
9 if (requestOptions.activityPub === true) { 9 if (requestOptions.activityPub === true) {
@@ -11,7 +11,7 @@ function doRequest (
11 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 11 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
12 } 12 }
13 13
14 return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => { 14 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
15 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 15 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
16 }) 16 })
17} 17}
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')
16 16
17// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
18 18
19const LAST_MIGRATION_VERSION = 285 19const LAST_MIGRATION_VERSION = 290
20 20
21// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
22 22
@@ -336,6 +336,9 @@ const CONSTRAINTS_FIELDS = {
336 VIDEOS_REDUNDANCY: { 336 VIDEOS_REDUNDANCY: {
337 URL: { min: 3, max: 2000 } // Length 337 URL: { min: 3, max: 2000 } // Length
338 }, 338 },
339 VIDEO_RATES: {
340 URL: { min: 3, max: 2000 } // Length
341 },
339 VIDEOS: { 342 VIDEOS: {
340 NAME: { min: 3, max: 120 }, // Length 343 NAME: { min: 3, max: 120 }, // Length
341 LANGUAGE: { min: 1, max: 10 }, // Length 344 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 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.STRING(2000),
12 allowNull: true
13 }
14
15 await utils.queryInterface.addColumn('accountVideoRate', 'url', data)
16 }
17
18 {
19 const builtUrlQuery = `SELECT "actor"."url" || '/' || "accountVideoRate"."type" || 's/' || "videoId" ` +
20 'FROM "accountVideoRate" ' +
21 'INNER JOIN account ON account.id = "accountVideoRate"."accountId" ' +
22 'INNER JOIN actor ON actor.id = account."actorId" ' +
23 'WHERE "base".id = "accountVideoRate".id'
24
25 const query = 'UPDATE "accountVideoRate" base SET "url" = (' + builtUrlQuery + ') WHERE "url" IS NULL'
26 await utils.sequelize.query(query)
27 }
28
29 {
30 const data = {
31 type: Sequelize.STRING(2000),
32 allowNull: false,
33 defaultValue: null
34 }
35 await utils.queryInterface.changeColumn('accountVideoRate', 'url', data)
36 }
37}
38
39function down (options) {
40 throw new Error('Not implemented.')
41}
42
43export {
44 up,
45 down
46}
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'
5import * as uuidv4 from 'uuid/v4' 5import * as uuidv4 from 'uuid/v4'
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8import { getActorUrl } from '../../helpers/activitypub' 8import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor' 9import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 11import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
@@ -65,8 +65,12 @@ async function getOrCreateActorAndServerAndModel (
65 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') 65 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
66 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) 66 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
67 67
68 if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
69 throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
70 }
71
68 try { 72 try {
69 // Assert we don't recurse another time 73 // Don't recurse another time
70 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) 74 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false)
71 } catch (err) { 75 } catch (err) {
72 logger.error('Cannot get or create account attributed to video channel ' + actor.url) 76 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
297 normalizeActor(requestResult.body) 301 normalizeActor(requestResult.body)
298 302
299 const actorJSON: ActivityPubActor = requestResult.body 303 const actorJSON: ActivityPubActor = requestResult.body
300
301 if (isActorObjectValid(actorJSON) === false) { 304 if (isActorObjectValid(actorJSON) === false) {
302 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) 305 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
303 return { result: undefined, statusCode: requestResult.response.statusCode } 306 return { result: undefined, statusCode: requestResult.response.statusCode }
304 } 307 }
305 308
309 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
310 throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id)
311 }
312
306 const followersCount = await fetchActorTotalItems(actorJSON.followers) 313 const followersCount = await fetchActorTotalItems(actorJSON.followers)
307 const followingCount = await fetchActorTotalItems(actorJSON.following) 314 const followingCount = await fetchActorTotalItems(actorJSON.following)
308 315
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'
2import { doRequest } from '../../helpers/requests' 2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 4import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
5 6
6async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { 7async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
7 logger.info('Crawling ActivityPub data on %s.', uri) 8 logger.info('Crawling ActivityPub data on %s.', uri)
@@ -14,7 +15,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
14 timeout: JOB_REQUEST_TIMEOUT 15 timeout: JOB_REQUEST_TIMEOUT
15 } 16 }
16 17
17 const response = await doRequest(options) 18 const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
18 const firstBody = response.body 19 const firstBody = response.body
19 20
20 let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 21 let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -23,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
23 while (nextLink && i < limit) { 24 while (nextLink && i < limit) {
24 options.uri = nextLink 25 options.uri = nextLink
25 26
26 const { body } = await doRequest(options) 27 const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
27 nextLink = body.next 28 nextLink = body.next
28 i++ 29 i++
29 30
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 @@
1export * from './process' export * from './process'
2export * from './process-accept'
3export * from './process-announce'
4export * from './process-create'
5export * from './process-delete'
6export * from './process-follow'
7export * from './process-like'
8export * from './process-undo'
9export * 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'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { forwardVideoRelatedActivity } from '../send/utils'
13import { Redis } from '../../redis' 13import { Redis } from '../../redis'
14import { createOrUpdateCacheFile } from '../cache-file' 14import { createOrUpdateCacheFile } from '../cache-file'
15import { immutableAssign } from '../../../tests/utils'
16import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
15 17
16async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 18async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
17 const activityObject = activity.object 19 const activityObject = activity.object
@@ -65,9 +67,10 @@ async function processCreateDislike (byActor: ActorModel, activity: ActivityCrea
65 videoId: video.id, 67 videoId: video.id,
66 accountId: byAccount.id 68 accountId: byAccount.id
67 } 69 }
70
68 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 71 const [ , created ] = await AccountVideoRateModel.findOrCreate({
69 where: rate, 72 where: rate,
70 defaults: rate, 73 defaults: immutableAssign(rate, { url: getVideoDislikeActivityPubUrl(byActor, video) }),
71 transaction: t 74 transaction: t
72 }) 75 })
73 if (created === true) await video.increment('dislikes', { transaction: t }) 76 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
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { immutableAssign } from '../../../tests/utils'
9import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
8 10
9async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) { 11async function processLikeActivity (activity: ActivityLike, byActor: ActorModel) {
10 return retryTransactionWrapper(processLikeVideo, byActor, activity) 12 return retryTransactionWrapper(processLikeVideo, byActor, activity)
@@ -34,7 +36,7 @@ async function processLikeVideo (byActor: ActorModel, activity: ActivityLike) {
34 } 36 }
35 const [ , created ] = await AccountVideoRateModel.findOrCreate({ 37 const [ , created ] = await AccountVideoRateModel.findOrCreate({
36 where: rate, 38 where: rate,
37 defaults: rate, 39 defaults: immutableAssign(rate, { url: getVideoLikeActivityPubUrl(byActor, video) }),
38 transaction: t 40 transaction: t
39 }) 41 })
40 if (created === true) await video.increment('likes', { transaction: t }) 42 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) {
55 return sequelizeTypescript.transaction(async t => { 55 return sequelizeTypescript.transaction(async t => {
56 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 56 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
57 57
58 const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) 58 let rate = await AccountVideoRateModel.loadByUrl(likeActivity.id, t)
59 if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
59 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) 60 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
60 61
61 await rate.destroy({ transaction: t }) 62 await rate.destroy({ transaction: t })
@@ -78,7 +79,8 @@ async function processUndoDislike (byActor: ActorModel, activity: ActivityUndo)
78 return sequelizeTypescript.transaction(async t => { 79 return sequelizeTypescript.transaction(async t => {
79 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) 80 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
80 81
81 const rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t) 82 let rate = await AccountVideoRateModel.loadByUrl(dislike.id, t)
83 if (!rate) rate = await AccountVideoRateModel.load(byActor.Account.id, video.id, t)
82 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`) 84 if (!rate) throw new Error(`Unknown rate by account ${byActor.Account.id} for video ${video.id}.`)
83 85
84 await rate.destroy({ transaction: t }) 86 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 @@
1import { Activity, ActivityType } from '../../../../shared/models/activitypub' 1import { Activity, ActivityType } from '../../../../shared/models/activitypub'
2import { getActorUrl } from '../../../helpers/activitypub' 2import { checkUrlsSameHost, getActorUrl } from '../../../helpers/activitypub'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { ActorModel } from '../../../models/activitypub/actor' 4import { ActorModel } from '../../../models/activitypub/actor'
5import { processAcceptActivity } from './process-accept' 5import { processAcceptActivity } from './process-accept'
@@ -25,11 +25,17 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, byActor: Ac
25 Like: processLikeActivity 25 Like: processLikeActivity
26} 26}
27 27
28async function processActivities (activities: Activity[], signatureActor?: ActorModel, inboxActor?: ActorModel) { 28async function processActivities (
29 activities: Activity[],
30 options: {
31 signatureActor?: ActorModel
32 inboxActor?: ActorModel
33 outboxUrl?: string
34 } = {}) {
29 const actorsCache: { [ url: string ]: ActorModel } = {} 35 const actorsCache: { [ url: string ]: ActorModel } = {}
30 36
31 for (const activity of activities) { 37 for (const activity of activities) {
32 if (!signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) { 38 if (!options.signatureActor && [ 'Create', 'Announce', 'Like' ].indexOf(activity.type) === -1) {
33 logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type) 39 logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type)
34 continue 40 continue
35 } 41 }
@@ -37,12 +43,17 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
37 const actorUrl = getActorUrl(activity.actor) 43 const actorUrl = getActorUrl(activity.actor)
38 44
39 // When we fetch remote data, we don't have signature 45 // When we fetch remote data, we don't have signature
40 if (signatureActor && actorUrl !== signatureActor.url) { 46 if (options.signatureActor && actorUrl !== options.signatureActor.url) {
41 logger.warn('Signature mismatch between %s and %s.', actorUrl, signatureActor.url) 47 logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, options.signatureActor.url)
42 continue 48 continue
43 } 49 }
44 50
45 const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) 51 if (options.outboxUrl && checkUrlsSameHost(options.outboxUrl, actorUrl) !== true) {
52 logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', options.outboxUrl, actorUrl)
53 continue
54 }
55
56 const byActor = options.signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl)
46 actorsCache[actorUrl] = byActor 57 actorsCache[actorUrl] = byActor
47 58
48 const activityProcessor = processActivity[activity.type] 59 const activityProcessor = processActivity[activity.type]
@@ -52,7 +63,7 @@ async function processActivities (activities: Activity[], signatureActor?: Actor
52 } 63 }
53 64
54 try { 65 try {
55 await activityProcessor(activity, byActor, inboxActor) 66 await activityProcessor(activity, byActor, options.inboxActor)
56 } catch (err) { 67 } catch (err) {
57 logger.warn('Cannot process activity %s.', activity.type, { err }) 68 logger.warn('Cannot process activity %s.', activity.type, { err })
58 } 69 }
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
95 logger.info('Creating job to send view of %s.', video.url) 95 logger.info('Creating job to send view of %s.', video.url)
96 96
97 const url = getVideoViewActivityPubUrl(byActor, video) 97 const url = getVideoViewActivityPubUrl(byActor, video)
98 const viewActivity = buildViewActivity(byActor, video) 98 const viewActivity = buildViewActivity(url, byActor, video)
99 99
100 return sendVideoRelatedCreateActivity({ 100 return sendVideoRelatedCreateActivity({
101 // Use the server actor to send the view 101 // Use the server actor to send the view
@@ -111,7 +111,7 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
111 logger.info('Creating job to dislike %s.', video.url) 111 logger.info('Creating job to dislike %s.', video.url)
112 112
113 const url = getVideoDislikeActivityPubUrl(byActor, video) 113 const url = getVideoDislikeActivityPubUrl(byActor, video)
114 const dislikeActivity = buildDislikeActivity(byActor, video) 114 const dislikeActivity = buildDislikeActivity(url, byActor, video)
115 115
116 return sendVideoRelatedCreateActivity({ 116 return sendVideoRelatedCreateActivity({
117 byActor, 117 byActor,
@@ -136,16 +136,18 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud
136 ) 136 )
137} 137}
138 138
139function buildDislikeActivity (byActor: ActorModel, video: VideoModel) { 139function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel) {
140 return { 140 return {
141 id: url,
141 type: 'Dislike', 142 type: 'Dislike',
142 actor: byActor.url, 143 actor: byActor.url,
143 object: video.url 144 object: video.url
144 } 145 }
145} 146}
146 147
147function buildViewActivity (byActor: ActorModel, video: VideoModel) { 148function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
148 return { 149 return {
150 id: url,
149 type: 'View', 151 type: 'View',
150 actor: byActor.url, 152 actor: byActor.url,
151 object: video.url 153 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,
24 24
25 return audiencify( 25 return audiencify(
26 { 26 {
27 type: 'Like' as 'Like',
28 id: url, 27 id: url,
28 type: 'Like' as 'Like',
29 actor: byActor.url, 29 actor: byActor.url,
30 object: video.url 30 object: video.url
31 }, 31 },
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
64 logger.info('Creating job to undo a dislike of video %s.', video.url) 64 logger.info('Creating job to undo a dislike of video %s.', video.url)
65 65
66 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) 66 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
67 const dislikeActivity = buildDislikeActivity(byActor, video) 67 const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
68 const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity) 68 const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
69 69
70 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) 70 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'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { VideoShareModel } from '../../models/video/video-share' 5import { VideoShareModel } from '../../models/video/video-share'
6import { sendUndoAnnounce, sendVideoAnnounce } from './send' 6import { sendUndoAnnounce, sendVideoAnnounce } from './send'
7import { getAnnounceActivityPubUrl } from './url' 7import { getVideoAnnounceActivityPubUrl } from './url'
8import { VideoChannelModel } from '../../models/video/video-channel' 8import { VideoChannelModel } from '../../models/video/video-channel'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { doRequest } from '../../helpers/requests' 10import { doRequest } from '../../helpers/requests'
11import { getOrCreateActorAndServerAndModel } from './actor' 11import { getOrCreateActorAndServerAndModel } from './actor'
12import { logger } from '../../helpers/logger' 12import { logger } from '../../helpers/logger'
13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
14import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
14 15
15async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { 16async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
16 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 17 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@@ -38,9 +39,13 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
38 json: true, 39 json: true,
39 activityPub: true 40 activityPub: true
40 }) 41 })
41 if (!body || !body.actor) throw new Error('Body of body actor is invalid') 42 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
43
44 const actorUrl = getActorUrl(body.actor)
45 if (checkUrlsSameHost(shareUrl, actorUrl) !== true) {
46 throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
47 }
42 48
43 const actorUrl = body.actor
44 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 49 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
45 50
46 const entry = { 51 const entry = {
@@ -72,7 +77,7 @@ export {
72async function shareByServer (video: VideoModel, t: Transaction) { 77async function shareByServer (video: VideoModel, t: Transaction) {
73 const serverActor = await getServerActor() 78 const serverActor = await getServerActor()
74 79
75 const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) 80 const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
76 return VideoShareModel.findOrCreate({ 81 return VideoShareModel.findOrCreate({
77 defaults: { 82 defaults: {
78 actorId: serverActor.id, 83 actorId: serverActor.id,
@@ -91,7 +96,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
91} 96}
92 97
93async function shareByVideoChannel (video: VideoModel, t: Transaction) { 98async function shareByVideoChannel (video: VideoModel, t: Transaction) {
94 const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) 99 const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
95 return VideoShareModel.findOrCreate({ 100 return VideoShareModel.findOrCreate({
96 defaults: { 101 defaults: {
97 actorId: video.VideoChannel.actorId, 102 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) {
33} 33}
34 34
35function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { 35function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
36 return video.url + '/views/' + byActor.uuid + '/' + new Date().toISOString() 36 return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString()
37} 37}
38 38
39function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { 39function getVideoLikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
40 return byActor.url + '/likes/' + video.id 40 return byActor.url + '/likes/' + video.id
41} 41}
42 42
43function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel) { 43function getVideoDislikeActivityPubUrl (byActor: ActorModel, video: VideoModel | { id: number }) {
44 return byActor.url + '/dislikes/' + video.id 44 return byActor.url + '/dislikes/' + video.id
45} 45}
46 46
@@ -74,8 +74,8 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
74 return follower.url + '/accepts/follows/' + me.id 74 return follower.url + '/accepts/follows/' + me.id
75} 75}
76 76
77function getAnnounceActivityPubUrl (originalUrl: string, byActor: ActorModel) { 77function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
78 return originalUrl + '/announces/' + byActor.id 78 return video.url + '/announces/' + byActor.id
79} 79}
80 80
81function getDeleteActivityPubUrl (originalUrl: string) { 81function getDeleteActivityPubUrl (originalUrl: string) {
@@ -97,7 +97,7 @@ export {
97 getVideoAbuseActivityPubUrl, 97 getVideoAbuseActivityPubUrl,
98 getActorFollowActivityPubUrl, 98 getActorFollowActivityPubUrl,
99 getActorFollowAcceptActivityPubUrl, 99 getActorFollowAcceptActivityPubUrl,
100 getAnnounceActivityPubUrl, 100 getVideoAnnounceActivityPubUrl,
101 getUpdateActivityPubUrl, 101 getUpdateActivityPubUrl,
102 getUndoActivityPubUrl, 102 getUndoActivityPubUrl,
103 getVideoViewActivityPubUrl, 103 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'
9import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
10import { getOrCreateVideoAndAccountAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
11import * as Bluebird from 'bluebird' 11import * as Bluebird from 'bluebird'
12import { checkUrlsSameHost } from '../../helpers/activitypub'
12 13
13async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) { 14async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
14 let originCommentId: number = null 15 let originCommentId: number = null
@@ -61,6 +62,14 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
61 const actorUrl = body.attributedTo 62 const actorUrl = body.attributedTo
62 if (!actorUrl) return { created: false } 63 if (!actorUrl) return { created: false }
63 64
65 if (checkUrlsSameHost(commentUrl, actorUrl) !== true) {
66 throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${commentUrl}`)
67 }
68
69 if (checkUrlsSameHost(body.id, commentUrl) !== true) {
70 throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`)
71 }
72
64 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 73 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
65 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) 74 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
66 if (!entry) return { created: false } 75 if (!entry) return { created: false }
@@ -134,6 +143,14 @@ async function resolveThread (url: string, comments: VideoCommentModel[] = []) {
134 const actorUrl = body.attributedTo 143 const actorUrl = body.attributedTo
135 if (!actorUrl) throw new Error('Miss attributed to in comment') 144 if (!actorUrl) throw new Error('Miss attributed to in comment')
136 145
146 if (checkUrlsSameHost(url, actorUrl) !== true) {
147 throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
148 }
149
150 if (checkUrlsSameHost(body.id, url) !== true) {
151 throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
152 }
153
137 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 154 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
138 const comment = new VideoCommentModel({ 155 const comment = new VideoCommentModel({
139 url: body.id, 156 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'
8import { AccountVideoRateModel } from '../../models/account/account-video-rate' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
9import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
11import { doRequest } from '../../helpers/requests'
12import { checkUrlsSameHost, getActorUrl } from '../../helpers/activitypub'
13import { ActorModel } from '../../models/activitypub/actor'
14import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
11 15
12async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) { 16async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) {
13 let rateCounts = 0 17 let rateCounts = 0
14 18
15 await Bluebird.map(actorUrls, async actorUrl => { 19 await Bluebird.map(ratesUrl, async rateUrl => {
16 try { 20 try {
21 // Fetch url
22 const { body } = await doRequest({
23 uri: rateUrl,
24 json: true,
25 activityPub: true
26 })
27 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
28
29 const actorUrl = getActorUrl(body.actor)
30 if (checkUrlsSameHost(actorUrl, rateUrl) !== true) {
31 throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`)
32 }
33
34 if (checkUrlsSameHost(body.id, rateUrl) !== true) {
35 throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
36 }
37
17 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 38 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
39
18 const [ , created ] = await AccountVideoRateModel 40 const [ , created ] = await AccountVideoRateModel
19 .findOrCreate({ 41 .findOrCreate({
20 where: { 42 where: {
@@ -24,13 +46,14 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
24 defaults: { 46 defaults: {
25 videoId: video.id, 47 videoId: video.id,
26 accountId: actor.Account.id, 48 accountId: actor.Account.id,
27 type: rate 49 type: rate,
50 url: body.id
28 } 51 }
29 }) 52 })
30 53
31 if (created) rateCounts += 1 54 if (created) rateCounts += 1
32 } catch (err) { 55 } catch (err) {
33 logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err }) 56 logger.warn('Cannot add rate %s.', rateUrl, { err })
34 } 57 }
35 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 58 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
36 59
@@ -62,7 +85,12 @@ async function sendVideoRateChange (account: AccountModel,
62 if (dislikes > 0) await sendCreateDislike(actor, video, t) 85 if (dislikes > 0) await sendCreateDislike(actor, video, t)
63} 86}
64 87
88function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) {
89 return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video)
90}
91
65export { 92export {
93 getRateUrl,
66 createRates, 94 createRates,
67 sendVideoRateChange 95 sendVideoRateChange
68} 96}
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'
29import { addVideoShares, shareVideoByServerAndChannel } from './share' 29import { addVideoShares, shareVideoByServerAndChannel } from './share'
30import { AccountModel } from '../../models/account/account' 30import { AccountModel } from '../../models/account/account'
31import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 31import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
32import { checkUrlsSameHost } from '../../helpers/activitypub'
32 33
33async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
34 // If the video is not private and published, we federate it 35 // If the video is not private and published, we federate it
@@ -63,7 +64,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
63 64
64 const { response, body } = await doRequest(options) 65 const { response, body } = await doRequest(options)
65 66
66 if (sanitizeAndCheckVideoTorrentObject(body) === false) { 67 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
67 logger.debug('Remote video JSON is not valid.', { body }) 68 logger.debug('Remote video JSON is not valid.', { body })
68 return { response, videoObject: undefined } 69 return { response, videoObject: undefined }
69 } 70 }
@@ -107,6 +108,10 @@ function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject
107 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 108 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
108 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 109 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
109 110
111 if (checkUrlsSameHost(channel.id, videoObject.id) !== true) {
112 throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`)
113 }
114
110 return getOrCreateActorAndServerAndModel(channel.id, 'all') 115 return getOrCreateActorAndServerAndModel(channel.id, 'all')
111} 116}
112 117
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) {
23 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) 23 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
24 24
25 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { 25 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
26 'activity': items => processActivities(items), 26 'activity': items => processActivities(items, { outboxUrl: payload.uri }),
27 'video-likes': items => createRates(items, video, 'like'), 27 'video-likes': items => createRates(items, video, 'like'),
28 'video-dislikes': items => createRates(items, video, 'dislike'), 28 'video-dislikes': items => createRates(items, video, 'dislike'),
29 'video-shares': items => addVideoShares(items, video), 29 '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'
5export * from './video-comments' 5export * from './video-comments'
6export * from './video-imports' 6export * from './video-imports'
7export * from './video-watch' 7export * from './video-watch'
8export * from './video-rates'
9export * from './video-shares'
8export * from './videos' 10export * 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 @@
1import * as express from 'express'
2import 'express-validator'
3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from '../utils'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoRateType } from '../../../../shared/models/videos'
10import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
11
12const videoUpdateRateValidator = [
13 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
14 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
15
16 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 logger.debug('Checking videoRate parameters', { parameters: req.body })
18
19 if (areValidationErrors(req, res)) return
20 if (!await isVideoExist(req.params.id, res)) return
21
22 return next()
23 }
24]
25
26const getAccountVideoRateValidator = function (rateType: VideoRateType) {
27 return [
28 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
29 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
30
31 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
32 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
33
34 if (areValidationErrors(req, res)) return
35
36 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId)
37 if (!rate) {
38 return res.status(404)
39 .json({ error: 'Video rate not found' })
40 .end()
41 }
42
43 res.locals.accountVideoRate = rate
44
45 return next()
46 }
47 ]
48}
49
50// ---------------------------------------------------------------------------
51
52export {
53 videoUpdateRateValidator,
54 getAccountVideoRateValidator
55}
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 @@
1import * as express from 'express'
2import 'express-validator'
3import { param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger'
7import { VideoShareModel } from '../../../models/video/video-share'
8import { areValidationErrors } from '../utils'
9import { VideoModel } from '../../../models/video/video'
10
11const videosShareValidator = [
12 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
13 param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'),
14
15 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking videoShare parameters', { parameters: req.params })
17
18 if (areValidationErrors(req, res)) return
19 if (!await isVideoExist(req.params.id, res)) return
20
21 const video: VideoModel = res.locals.video
22
23 const share = await VideoShareModel.load(req.params.actorId, video.id)
24 if (!share) {
25 return res.status(404)
26 .end()
27 }
28
29 res.locals.videoShare = share
30 return next()
31 }
32]
33
34// ---------------------------------------------------------------------------
35
36export {
37 videosShareValidator
38}
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 {
26 isVideoLicenceValid, 26 isVideoLicenceValid,
27 isVideoNameValid, 27 isVideoNameValid,
28 isVideoPrivacyValid, 28 isVideoPrivacyValid,
29 isVideoRatingTypeValid,
30 isVideoSupportValid, 29 isVideoSupportValid,
31 isVideoTagsValid 30 isVideoTagsValid
32} from '../../../helpers/custom-validators/videos' 31} from '../../../helpers/custom-validators/videos'
33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
34import { logger } from '../../../helpers/logger' 33import { logger } from '../../../helpers/logger'
35import { CONSTRAINTS_FIELDS } from '../../../initializers' 34import { CONSTRAINTS_FIELDS } from '../../../initializers'
36import { VideoShareModel } from '../../../models/video/video-share'
37import { authenticate } from '../../oauth' 35import { authenticate } from '../../oauth'
38import { areValidationErrors } from '../utils' 36import { areValidationErrors } from '../utils'
39import { cleanUpReqFiles } from '../../../helpers/express-utils' 37import { cleanUpReqFiles } from '../../../helpers/express-utils'
@@ -188,41 +186,6 @@ const videosRemoveValidator = [
188 } 186 }
189] 187]
190 188
191const videoRateValidator = [
192 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
193 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
194
195 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
196 logger.debug('Checking videoRate parameters', { parameters: req.body })
197
198 if (areValidationErrors(req, res)) return
199 if (!await isVideoExist(req.params.id, res)) return
200
201 return next()
202 }
203]
204
205const videosShareValidator = [
206 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
207 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
208
209 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
210 logger.debug('Checking videoShare parameters', { parameters: req.params })
211
212 if (areValidationErrors(req, res)) return
213 if (!await isVideoExist(req.params.id, res)) return
214
215 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
216 if (!share) {
217 return res.status(404)
218 .end()
219 }
220
221 res.locals.videoShare = share
222 return next()
223 }
224]
225
226const videosChangeOwnershipValidator = [ 189const videosChangeOwnershipValidator = [
227 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 190 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
228 191
@@ -415,9 +378,6 @@ export {
415 videosGetValidator, 378 videosGetValidator,
416 videosCustomGetValidator, 379 videosCustomGetValidator,
417 videosRemoveValidator, 380 videosRemoveValidator,
418 videosShareValidator,
419
420 videoRateValidator,
421 381
422 videosChangeOwnershipValidator, 382 videosChangeOwnershipValidator,
423 videosTerminateChangeOwnershipValidator, 383 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 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' 4import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
5import { VideoRateType } from '../../../shared/models/videos' 5import { VideoRateType } from '../../../shared/models/videos'
6import { VIDEO_RATE_TYPES } from '../../initializers' 6import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers'
7import { VideoModel } from '../video/video' 7import { VideoModel } from '../video/video'
8import { AccountModel } from './account' 8import { AccountModel } from './account'
9import { ActorModel } from '../activitypub/actor' 9import { ActorModel } from '../activitypub/actor'
10import { throwIfNotValid } from '../utils'
11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10 12
11/* 13/*
12 Account rates per video. 14 Account rates per video.
@@ -26,6 +28,10 @@ import { ActorModel } from '../activitypub/actor'
26 }, 28 },
27 { 29 {
28 fields: [ 'videoId', 'type' ] 30 fields: [ 'videoId', 'type' ]
31 },
32 {
33 fields: [ 'url' ],
34 unique: true
29 } 35 }
30 ] 36 ]
31}) 37})
@@ -35,6 +41,11 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
35 @Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) 41 @Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
36 type: VideoRateType 42 type: VideoRateType
37 43
44 @AllowNull(false)
45 @Is('AccountVideoRateUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
46 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_RATES.URL.max))
47 url: string
48
38 @CreatedAt 49 @CreatedAt
39 createdAt: Date 50 createdAt: Date
40 51
@@ -65,7 +76,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
65 }) 76 })
66 Account: AccountModel 77 Account: AccountModel
67 78
68 static load (accountId: number, videoId: number, transaction: Transaction) { 79 static load (accountId: number, videoId: number, transaction?: Transaction) {
69 const options: IFindOptions<AccountVideoRateModel> = { 80 const options: IFindOptions<AccountVideoRateModel> = {
70 where: { 81 where: {
71 accountId, 82 accountId,
@@ -77,6 +88,49 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
77 return AccountVideoRateModel.findOne(options) 88 return AccountVideoRateModel.findOne(options)
78 } 89 }
79 90
91 static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
92 const options: IFindOptions<AccountVideoRateModel> = {
93 where: {
94 videoId,
95 type: rateType
96 },
97 include: [
98 {
99 model: AccountModel.unscoped(),
100 required: true,
101 include: [
102 {
103 attributes: [ 'id', 'url', 'preferredUsername' ],
104 model: ActorModel.unscoped(),
105 required: true,
106 where: {
107 preferredUsername: accountName
108 }
109 }
110 ]
111 },
112 {
113 model: VideoModel.unscoped(),
114 required: true
115 }
116 ]
117 }
118 if (transaction) options.transaction = transaction
119
120 return AccountVideoRateModel.findOne(options)
121 }
122
123 static loadByUrl (url: string, transaction: Transaction) {
124 const options: IFindOptions<AccountVideoRateModel> = {
125 where: {
126 url
127 }
128 }
129 if (transaction) options.transaction = transaction
130
131 return AccountVideoRateModel.findOne(options)
132 }
133
80 static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) { 134 static listAndCountAccountUrlsByVideoId (rateType: VideoRateType, videoId: number, start: number, count: number, t?: Transaction) {
81 const query = { 135 const query = {
82 offset: start, 136 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 {
47 required: true, 47 required: true,
48 include: [ 48 include: [
49 { 49 {
50 attributes: [ 'id' ], 50 attributes: [ 'id', 'url' ],
51 model: () => ActorModel.unscoped(), 51 model: () => ActorModel.unscoped(),
52 required: true 52 required: true
53 } 53 }
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<VideoShareModel> {
88 }) 88 })
89 Video: VideoModel 89 Video: VideoModel
90 90
91 static load (actorId: number, videoId: number, t: Sequelize.Transaction) { 91 static load (actorId: number, videoId: number, t?: Sequelize.Transaction) {
92 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({ 92 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
93 where: { 93 where: {
94 actorId, 94 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 @@
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { flushAndRunMultipleServers, flushTests, killallServers, makeAPRequest, makeFollowRequest, ServerInfo } from '../../utils' 5import { flushAndRunMultipleServers, flushTests, killallServers, makePOSTAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
6import { HTTP_SIGNATURE } from '../../../initializers' 6import { HTTP_SIGNATURE } from '../../../initializers'
7import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' 7import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
8import * as chai from 'chai' 8import * as chai from 'chai'
@@ -63,7 +63,7 @@ describe('Test ActivityPub security', function () {
63 Digest: buildDigest({ hello: 'coucou' }) 63 Digest: buildDigest({ hello: 'coucou' })
64 } 64 }
65 65
66 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) 66 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
67 67
68 expect(response.statusCode).to.equal(403) 68 expect(response.statusCode).to.equal(403)
69 }) 69 })
@@ -73,7 +73,7 @@ describe('Test ActivityPub security', function () {
73 const headers = buildGlobalHeaders(body) 73 const headers = buildGlobalHeaders(body)
74 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' 74 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
75 75
76 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) 76 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
77 77
78 expect(response.statusCode).to.equal(403) 78 expect(response.statusCode).to.equal(403)
79 }) 79 })
@@ -85,7 +85,7 @@ describe('Test ActivityPub security', function () {
85 const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) 85 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
86 const headers = buildGlobalHeaders(body) 86 const headers = buildGlobalHeaders(body)
87 87
88 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) 88 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
89 89
90 expect(response.statusCode).to.equal(403) 90 expect(response.statusCode).to.equal(403)
91 }) 91 })
@@ -97,7 +97,7 @@ describe('Test ActivityPub security', function () {
97 const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) 97 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
98 const headers = buildGlobalHeaders(body) 98 const headers = buildGlobalHeaders(body)
99 99
100 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) 100 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature, headers)
101 101
102 expect(response.statusCode).to.equal(204) 102 expect(response.statusCode).to.equal(204)
103 }) 103 })
@@ -126,7 +126,7 @@ describe('Test ActivityPub security', function () {
126 126
127 const headers = buildGlobalHeaders(signedBody) 127 const headers = buildGlobalHeaders(signedBody)
128 128
129 const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) 129 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
130 130
131 expect(response.statusCode).to.equal(403) 131 expect(response.statusCode).to.equal(403)
132 }) 132 })
@@ -147,7 +147,7 @@ describe('Test ActivityPub security', function () {
147 147
148 const headers = buildGlobalHeaders(signedBody) 148 const headers = buildGlobalHeaders(signedBody)
149 149
150 const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) 150 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
151 151
152 expect(response.statusCode).to.equal(403) 152 expect(response.statusCode).to.equal(403)
153 }) 153 })
@@ -163,7 +163,7 @@ describe('Test ActivityPub security', function () {
163 163
164 const headers = buildGlobalHeaders(signedBody) 164 const headers = buildGlobalHeaders(signedBody)
165 165
166 const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) 166 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature, headers)
167 167
168 expect(response.statusCode).to.equal(204) 168 expect(response.statusCode).to.equal(204)
169 }) 169 })
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'
3import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' 3import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../helpers/activitypub' 4import { activityPubContextify } from '../../../helpers/activitypub'
5 5
6function makeAPRequest (url: string, body: any, httpSignature: any, headers: any) { 6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = { 7 const options = {
8 method: 'POST', 8 method: 'POST',
9 uri: url, 9 uri: url,
@@ -34,10 +34,10 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
34 } 34 }
35 const headers = buildGlobalHeaders(body) 35 const headers = buildGlobalHeaders(body)
36 36
37 return makeAPRequest(to.url, body, httpSignature, headers) 37 return makePOSTAPRequest(to.url, body, httpSignature, headers)
38} 38}
39 39
40export { 40export {
41 makeAPRequest, 41 makePOSTAPRequest,
42 makeFollowRequest 42 makeFollowRequest
43} 43}
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 @@
1export interface DislikeObject { 1export interface DislikeObject {
2 type: 'Dislike', 2 id: string
3 type: 'Dislike'
3 actor: string 4 actor: string
4 object: string 5 object: string
5} 6}
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'