aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-11-23 14:19:55 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-11-27 19:40:53 +0100
commit0032ebe94aa83fab761c7de3ceb6210ac4532824 (patch)
tree3ea407d7ea6de4c7f7bc66caba7e23c0cc4036e3
parentd52eb8f656242c7e34afdb2dee681861fb9bce35 (diff)
downloadPeerTube-0032ebe94aa83fab761c7de3ceb6210ac4532824.tar.gz
PeerTube-0032ebe94aa83fab761c7de3ceb6210ac4532824.tar.zst
PeerTube-0032ebe94aa83fab761c7de3ceb6210ac4532824.zip
Federate likes/dislikes
-rw-r--r--server/controllers/api/videos/rate.ts7
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts23
-rw-r--r--server/helpers/custom-validators/activitypub/announce.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/rate.ts20
-rw-r--r--server/helpers/custom-validators/activitypub/undo.ts9
-rw-r--r--server/initializers/constants.ts1
-rw-r--r--server/lib/activitypub/process/index.ts1
-rw-r--r--server/lib/activitypub/process/process-create.ts35
-rw-r--r--server/lib/activitypub/process/process-like.ts50
-rw-r--r--server/lib/activitypub/process/process-undo.ts99
-rw-r--r--server/lib/activitypub/process/process.ts6
-rw-r--r--server/lib/activitypub/send/index.ts2
-rw-r--r--server/lib/activitypub/send/misc.ts27
-rw-r--r--server/lib/activitypub/send/send-create.ts62
-rw-r--r--server/lib/activitypub/send/send-like.ts60
-rw-r--r--server/lib/activitypub/send/send-undo.ts71
-rw-r--r--server/lib/activitypub/url.ts12
-rw-r--r--server/lib/activitypub/videos.ts47
-rw-r--r--server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts9
-rw-r--r--server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts24
-rw-r--r--server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts9
-rw-r--r--server/models/account/account-follow-interface.ts6
-rw-r--r--server/models/account/account-follow.ts5
-rw-r--r--server/tests/api/index-slow.ts1
-rw-r--r--shared/models/activitypub/activity.ts14
-rw-r--r--shared/models/activitypub/objects/dislike-object.ts5
-rw-r--r--shared/models/activitypub/objects/index.ts1
27 files changed, 548 insertions, 62 deletions
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts
index 8216dffd2..134284df7 100644
--- a/server/controllers/api/videos/rate.ts
+++ b/server/controllers/api/videos/rate.ts
@@ -3,6 +3,7 @@ import { UserVideoRateUpdate } from '../../../../shared'
3import { logger, retryTransactionWrapper } from '../../../helpers' 3import { logger, retryTransactionWrapper } from '../../../helpers'
4import { VIDEO_RATE_TYPES } from '../../../initializers' 4import { VIDEO_RATE_TYPES } from '../../../initializers'
5import { database as db } from '../../../initializers/database' 5import { database as db } from '../../../initializers/database'
6import { sendVideoRateChangeToFollowers, sendVideoRateChangeToOrigin } from '../../../lib/activitypub/videos'
6import { asyncMiddleware, authenticate, videoRateValidator } from '../../../middlewares' 7import { asyncMiddleware, authenticate, videoRateValidator } from '../../../middlewares'
7import { AccountInstance } from '../../../models/account/account-interface' 8import { AccountInstance } from '../../../models/account/account-interface'
8import { VideoInstance } from '../../../models/video/video-interface' 9import { VideoInstance } from '../../../models/video/video-interface'
@@ -82,10 +83,10 @@ async function rateVideo (req: express.Request, res: express.Response) {
82 // It is useful for the user to have a feedback 83 // It is useful for the user to have a feedback
83 await videoInstance.increment(incrementQuery, sequelizeOptions) 84 await videoInstance.increment(incrementQuery, sequelizeOptions)
84 85
85 if (videoInstance.isOwned() === false) { 86 if (videoInstance.isOwned()) {
86 // TODO: Send a event to original server 87 await sendVideoRateChangeToFollowers(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t)
87 } else { 88 } else {
88 // TODO: Send update to followers 89 await sendVideoRateChangeToOrigin(accountInstance, videoInstance, likesToIncrement, dislikesToIncrement, t)
89 } 90 }
90 }) 91 })
91 92
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index 66e557d39..3a0e8197c 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -1,9 +1,9 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity'
3import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account' 3import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
4import { isAnnounceValid } from './announce' 4import { isAnnounceActivityValid } from './announce'
5import { isActivityPubUrlValid } from './misc' 5import { isActivityPubUrlValid } from './misc'
6import { isUndoValid } from './undo' 6import { isUndoActivityValid } from './undo'
7import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' 7import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
8import { 8import {
9 isVideoFlagValid, 9 isVideoFlagValid,
@@ -12,6 +12,7 @@ import {
12 isVideoTorrentUpdateActivityValid 12 isVideoTorrentUpdateActivityValid
13} from './videos' 13} from './videos'
14import { isViewActivityValid } from './view' 14import { isViewActivityValid } from './view'
15import { isDislikeActivityValid, isLikeActivityValid } from './rate'
15 16
16function isRootActivityValid (activity: any) { 17function isRootActivityValid (activity: any) {
17 return Array.isArray(activity['@context']) && 18 return Array.isArray(activity['@context']) &&
@@ -34,7 +35,8 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean
34 Follow: checkFollowActivity, 35 Follow: checkFollowActivity,
35 Accept: checkAcceptActivity, 36 Accept: checkAcceptActivity,
36 Announce: checkAnnounceActivity, 37 Announce: checkAnnounceActivity,
37 Undo: checkUndoActivity 38 Undo: checkUndoActivity,
39 Like: checkLikeActivity
38} 40}
39 41
40function isActivityValid (activity: any) { 42function isActivityValid (activity: any) {
@@ -55,9 +57,10 @@ export {
55// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
56 58
57function checkCreateActivity (activity: any) { 59function checkCreateActivity (activity: any) {
58 return isVideoChannelCreateActivityValid(activity) || 60 return isViewActivityValid(activity) ||
59 isVideoFlagValid(activity) || 61 isDislikeActivityValid(activity) ||
60 isViewActivityValid(activity) 62 isVideoChannelCreateActivityValid(activity) ||
63 isVideoFlagValid(activity)
61} 64}
62 65
63function checkAddActivity (activity: any) { 66function checkAddActivity (activity: any) {
@@ -84,9 +87,13 @@ function checkAcceptActivity (activity: any) {
84} 87}
85 88
86function checkAnnounceActivity (activity: any) { 89function checkAnnounceActivity (activity: any) {
87 return isAnnounceValid(activity) 90 return isAnnounceActivityValid(activity)
88} 91}
89 92
90function checkUndoActivity (activity: any) { 93function checkUndoActivity (activity: any) {
91 return isUndoValid(activity) 94 return isUndoActivityValid(activity)
95}
96
97function checkLikeActivity (activity: any) {
98 return isLikeActivityValid(activity)
92} 99}
diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts
index 4ba99d1ea..45f6b05a0 100644
--- a/server/helpers/custom-validators/activitypub/announce.ts
+++ b/server/helpers/custom-validators/activitypub/announce.ts
@@ -2,7 +2,7 @@ import { isBaseActivityValid } from './misc'
2import { isVideoTorrentAddActivityValid } from './videos' 2import { isVideoTorrentAddActivityValid } from './videos'
3import { isVideoChannelCreateActivityValid } from './video-channels' 3import { isVideoChannelCreateActivityValid } from './video-channels'
4 4
5function isAnnounceValid (activity: any) { 5function isAnnounceActivityValid (activity: any) {
6 return isBaseActivityValid(activity, 'Announce') && 6 return isBaseActivityValid(activity, 'Announce') &&
7 ( 7 (
8 isVideoChannelCreateActivityValid(activity.object) || 8 isVideoChannelCreateActivityValid(activity.object) ||
@@ -11,5 +11,5 @@ function isAnnounceValid (activity: any) {
11} 11}
12 12
13export { 13export {
14 isAnnounceValid 14 isAnnounceActivityValid
15} 15}
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts
new file mode 100644
index 000000000..e70bd94b8
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/rate.ts
@@ -0,0 +1,20 @@
1import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
2
3function isLikeActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Like') &&
5 isActivityPubUrlValid(activity.object)
6}
7
8function isDislikeActivityValid (activity: any) {
9 return isBaseActivityValid(activity, 'Create') &&
10 activity.object.type === 'Dislike' &&
11 isActivityPubUrlValid(activity.object.actor) &&
12 isActivityPubUrlValid(activity.object.object)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 isLikeActivityValid,
19 isDislikeActivityValid
20}
diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts
index a9a2a3a41..58043f8a1 100644
--- a/server/helpers/custom-validators/activitypub/undo.ts
+++ b/server/helpers/custom-validators/activitypub/undo.ts
@@ -1,13 +1,16 @@
1import { isAccountFollowActivityValid } from './account' 1import { isAccountFollowActivityValid } from './account'
2import { isBaseActivityValid } from './misc' 2import { isBaseActivityValid } from './misc'
3import { isDislikeActivityValid, isLikeActivityValid } from './rate'
3 4
4function isUndoValid (activity: any) { 5function isUndoActivityValid (activity: any) {
5 return isBaseActivityValid(activity, 'Undo') && 6 return isBaseActivityValid(activity, 'Undo') &&
6 ( 7 (
7 isAccountFollowActivityValid(activity.object) 8 isAccountFollowActivityValid(activity.object) ||
9 isLikeActivityValid(activity.object) ||
10 isDislikeActivityValid(activity.object)
8 ) 11 )
9} 12}
10 13
11export { 14export {
12 isUndoValid 15 isUndoActivityValid
13} 16}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 9e61f01aa..e7f668ee4 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -229,6 +229,7 @@ const ACTIVITY_PUB = {
229 PUBLIC: 'https://www.w3.org/ns/activitystreams#Public', 229 PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
230 COLLECTION_ITEMS_PER_PAGE: 10, 230 COLLECTION_ITEMS_PER_PAGE: 10,
231 FETCH_PAGE_LIMIT: 100, 231 FETCH_PAGE_LIMIT: 100,
232 MAX_HTTP_ATTEMPT: 5,
232 URL_MIME_TYPES: { 233 URL_MIME_TYPES: {
233 VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT 234 VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
234 TORRENT: [ 'application/x-bittorrent' ], 235 TORRENT: [ 'application/x-bittorrent' ],
diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts
index c68312053..e25c261cc 100644
--- a/server/lib/activitypub/process/index.ts
+++ b/server/lib/activitypub/process/index.ts
@@ -5,5 +5,6 @@ export * from './process-announce'
5export * from './process-create' 5export * from './process-create'
6export * from './process-delete' 6export * from './process-delete'
7export * from './process-follow' 7export * from './process-follow'
8export * from './process-like'
8export * from './process-undo' 9export * from './process-undo'
9export * from './process-update' 10export * from './process-update'
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 1777733a0..147bbd132 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -5,9 +5,10 @@ import { logger, retryTransactionWrapper } from '../../../helpers'
5import { database as db } from '../../../initializers' 5import { database as db } from '../../../initializers'
6import { AccountInstance } from '../../../models/account/account-interface' 6import { AccountInstance } from '../../../models/account/account-interface'
7import { getOrCreateAccountAndServer } from '../account' 7import { getOrCreateAccountAndServer } from '../account'
8import { sendCreateViewToVideoFollowers } from '../send/send-create' 8import { sendCreateDislikeToVideoFollowers, sendCreateViewToVideoFollowers } from '../send/send-create'
9import { getVideoChannelActivityPubUrl } from '../url' 9import { getVideoChannelActivityPubUrl } from '../url'
10import { videoChannelActivityObjectToDBAttributes } from './misc' 10import { videoChannelActivityObjectToDBAttributes } from './misc'
11import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object'
11 12
12async function processCreateActivity (activity: ActivityCreate) { 13async function processCreateActivity (activity: ActivityCreate) {
13 const activityObject = activity.object 14 const activityObject = activity.object
@@ -16,6 +17,8 @@ async function processCreateActivity (activity: ActivityCreate) {
16 17
17 if (activityType === 'View') { 18 if (activityType === 'View') {
18 return processCreateView(activityObject as ViewObject) 19 return processCreateView(activityObject as ViewObject)
20 } else if (activityType === 'Dislike') {
21 return processCreateDislike(account, activityObject as DislikeObject)
19 } else if (activityType === 'VideoChannel') { 22 } else if (activityType === 'VideoChannel') {
20 return processCreateVideoChannel(account, activityObject as VideoChannelObject) 23 return processCreateVideoChannel(account, activityObject as VideoChannelObject)
21 } else if (activityType === 'Flag') { 24 } else if (activityType === 'Flag') {
@@ -34,6 +37,36 @@ export {
34 37
35// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
36 39
40async function processCreateDislike (byAccount: AccountInstance, dislike: DislikeObject) {
41 const options = {
42 arguments: [ byAccount, dislike ],
43 errorMessage: 'Cannot dislike the video with many retries.'
44 }
45
46 return retryTransactionWrapper(createVideoDislike, options)
47}
48
49function createVideoDislike (byAccount: AccountInstance, dislike: DislikeObject) {
50 return db.sequelize.transaction(async t => {
51 const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object)
52
53 if (!video) throw new Error('Unknown video ' + dislike.object)
54
55 const rate = {
56 type: 'dislike' as 'dislike',
57 videoId: video.id,
58 accountId: byAccount.id
59 }
60 const [ , created ] = await db.AccountVideoRate.findOrCreate({
61 where: rate,
62 defaults: rate
63 })
64 await video.increment('dislikes')
65
66 if (video.isOwned() && created === true) await sendCreateDislikeToVideoFollowers(byAccount, video, undefined)
67 })
68}
69
37async function processCreateView (view: ViewObject) { 70async function processCreateView (view: ViewObject) {
38 const video = await db.Video.loadByUrlAndPopulateAccount(view.object) 71 const video = await db.Video.loadByUrlAndPopulateAccount(view.object)
39 72
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
new file mode 100644
index 000000000..d77b30f24
--- /dev/null
+++ b/server/lib/activitypub/process/process-like.ts
@@ -0,0 +1,50 @@
1import { ActivityLike } from '../../../../shared/models/activitypub/activity'
2import { database as db } from '../../../initializers'
3import { AccountInstance } from '../../../models/account/account-interface'
4import { getOrCreateAccountAndServer } from '../account'
5import { sendLikeToVideoFollowers } from '../send/send-like'
6import { retryTransactionWrapper } from '../../../helpers/database-utils'
7
8async function processLikeActivity (activity: ActivityLike) {
9 const account = await getOrCreateAccountAndServer(activity.actor)
10
11 return processLikeVideo(account, activity.object)
12}
13
14// ---------------------------------------------------------------------------
15
16export {
17 processLikeActivity
18}
19
20// ---------------------------------------------------------------------------
21
22async function processLikeVideo (byAccount: AccountInstance, videoUrl: string) {
23 const options = {
24 arguments: [ byAccount, videoUrl ],
25 errorMessage: 'Cannot like the video with many retries.'
26 }
27
28 return retryTransactionWrapper(createVideoLike, options)
29}
30
31function createVideoLike (byAccount: AccountInstance, videoUrl: string) {
32 return db.sequelize.transaction(async t => {
33 const video = await db.Video.loadByUrlAndPopulateAccount(videoUrl)
34
35 if (!video) throw new Error('Unknown video ' + videoUrl)
36
37 const rate = {
38 type: 'like' as 'like',
39 videoId: video.id,
40 accountId: byAccount.id
41 }
42 const [ , created ] = await db.AccountVideoRate.findOrCreate({
43 where: rate,
44 defaults: rate
45 })
46 await video.increment('likes')
47
48 if (video.isOwned() && created === true) await sendLikeToVideoFollowers(byAccount, video, undefined)
49 })
50}
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index 610b800fb..caa835714 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -1,20 +1,20 @@
1import { ActivityUndo } from '../../../../shared/models/activitypub/activity' 1import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { database as db } from '../../../initializers' 3import { database as db } from '../../../initializers'
4import { retryTransactionWrapper } from '../../../helpers/database-utils'
5import { DislikeObject } from '../../../../shared/models/activitypub/objects/dislike-object'
6import { sendUndoLikeToVideoFollowers } from '../send/send-undo'
7import { sendUndoDislikeToVideoFollowers } from '../index'
4 8
5async function processUndoActivity (activity: ActivityUndo) { 9async function processUndoActivity (activity: ActivityUndo) {
6 const activityToUndo = activity.object 10 const activityToUndo = activity.object
7 11
8 if (activityToUndo.type === 'Follow') { 12 if (activityToUndo.type === 'Like') {
9 const follower = await db.Account.loadByUrl(activity.actor) 13 return processUndoLike(activity.actor, activityToUndo)
10 const following = await db.Account.loadByUrl(activityToUndo.object) 14 } else if (activityToUndo.type === 'Create' && activityToUndo.object.type === 'Dislike') {
11 const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id) 15 return processUndoDislike(activity.actor, activityToUndo.object)
12 16 } else if (activityToUndo.type === 'Follow') {
13 if (!accountFollow) throw new Error(`'Unknown account follow ${follower.id} -> ${following.id}.`) 17 return processUndoFollow(activity.actor, activityToUndo)
14
15 await accountFollow.destroy()
16
17 return undefined
18 } 18 }
19 19
20 logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id }) 20 logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id })
@@ -29,3 +29,80 @@ export {
29} 29}
30 30
31// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
32
33function processUndoLike (actor: string, likeActivity: ActivityLike) {
34 const options = {
35 arguments: [ actor, likeActivity ],
36 errorMessage: 'Cannot undo like with many retries.'
37 }
38
39 return retryTransactionWrapper(undoLike, options)
40}
41
42function undoLike (actor: string, likeActivity: ActivityLike) {
43 return db.sequelize.transaction(async t => {
44 const byAccount = await db.Account.loadByUrl(actor, t)
45 if (!byAccount) throw new Error('Unknown account ' + actor)
46
47 const video = await db.Video.loadByUrlAndPopulateAccount(likeActivity.object)
48 if (!video) throw new Error('Unknown video ' + likeActivity.actor)
49
50 const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t)
51 if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`)
52
53 await rate.destroy({ transaction: t })
54 await video.decrement('likes')
55
56 if (video.isOwned()) await sendUndoLikeToVideoFollowers(byAccount, video, t)
57 })
58}
59
60function processUndoDislike (actor: string, dislikeCreateActivity: DislikeObject) {
61 const options = {
62 arguments: [ actor, dislikeCreateActivity ],
63 errorMessage: 'Cannot undo dislike with many retries.'
64 }
65
66 return retryTransactionWrapper(undoDislike, options)
67}
68
69function undoDislike (actor: string, dislike: DislikeObject) {
70 return db.sequelize.transaction(async t => {
71 const byAccount = await db.Account.loadByUrl(actor, t)
72 if (!byAccount) throw new Error('Unknown account ' + actor)
73
74 const video = await db.Video.loadByUrlAndPopulateAccount(dislike.object)
75 if (!video) throw new Error('Unknown video ' + dislike.actor)
76
77 const rate = await db.AccountVideoRate.load(byAccount.id, video.id, t)
78 if (!rate) throw new Error(`Unknown rate by account ${byAccount.id} for video ${video.id}.`)
79
80 await rate.destroy({ transaction: t })
81 await video.decrement('dislikes')
82
83 if (video.isOwned()) await sendUndoDislikeToVideoFollowers(byAccount, video, t)
84 })
85}
86
87function processUndoFollow (actor: string, followActivity: ActivityFollow) {
88 const options = {
89 arguments: [ actor, followActivity ],
90 errorMessage: 'Cannot undo follow with many retries.'
91 }
92
93 return retryTransactionWrapper(undoFollow, options)
94}
95
96function undoFollow (actor: string, followActivity: ActivityFollow) {
97 return db.sequelize.transaction(async t => {
98 const follower = await db.Account.loadByUrl(actor, t)
99 const following = await db.Account.loadByUrl(followActivity.object, t)
100 const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id, t)
101
102 if (!accountFollow) throw new Error(`'Unknown account follow ${follower.id} -> ${following.id}.`)
103
104 await accountFollow.destroy({ transaction: t })
105
106 return undefined
107 })
108}
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts
index 613597341..942bce0e6 100644
--- a/server/lib/activitypub/process/process.ts
+++ b/server/lib/activitypub/process/process.ts
@@ -1,4 +1,5 @@
1import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity' 1import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity'
2import { logger } from '../../../helpers/logger'
2import { AccountInstance } from '../../../models/account/account-interface' 3import { AccountInstance } from '../../../models/account/account-interface'
3import { processAcceptActivity } from './process-accept' 4import { processAcceptActivity } from './process-accept'
4import { processAddActivity } from './process-add' 5import { processAddActivity } from './process-add'
@@ -6,9 +7,9 @@ import { processAnnounceActivity } from './process-announce'
6import { processCreateActivity } from './process-create' 7import { processCreateActivity } from './process-create'
7import { processDeleteActivity } from './process-delete' 8import { processDeleteActivity } from './process-delete'
8import { processFollowActivity } from './process-follow' 9import { processFollowActivity } from './process-follow'
10import { processLikeActivity } from './process-like'
9import { processUndoActivity } from './process-undo' 11import { processUndoActivity } from './process-undo'
10import { processUpdateActivity } from './process-update' 12import { processUpdateActivity } from './process-update'
11import { logger } from '../../../helpers/logger'
12 13
13const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise<any> } = { 14const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise<any> } = {
14 Create: processCreateActivity, 15 Create: processCreateActivity,
@@ -18,7 +19,8 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccoun
18 Follow: processFollowActivity, 19 Follow: processFollowActivity,
19 Accept: processAcceptActivity, 20 Accept: processAcceptActivity,
20 Announce: processAnnounceActivity, 21 Announce: processAnnounceActivity,
21 Undo: processUndoActivity 22 Undo: processUndoActivity,
23 Like: processLikeActivity
22} 24}
23 25
24async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) { 26async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) {
diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts
index 5f15dd4b5..ee8f3ad7e 100644
--- a/server/lib/activitypub/send/index.ts
+++ b/server/lib/activitypub/send/index.ts
@@ -4,4 +4,6 @@ export * from './send-announce'
4export * from './send-create' 4export * from './send-create'
5export * from './send-delete' 5export * from './send-delete'
6export * from './send-follow' 6export * from './send-follow'
7export * from './send-like'
8export * from './send-undo'
7export * from './send-update' 9export * from './send-update'
diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts
index f3dc5c148..41a039b19 100644
--- a/server/lib/activitypub/send/misc.ts
+++ b/server/lib/activitypub/send/misc.ts
@@ -3,6 +3,7 @@ import { logger } from '../../../helpers/logger'
3import { ACTIVITY_PUB, database as db } from '../../../initializers' 3import { ACTIVITY_PUB, database as db } from '../../../initializers'
4import { AccountInstance } from '../../../models/account/account-interface' 4import { AccountInstance } from '../../../models/account/account-interface'
5import { activitypubHttpJobScheduler } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler' 5import { activitypubHttpJobScheduler } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler'
6import { VideoInstance } from '../../../models/video/video-interface'
6 7
7async function broadcastToFollowers ( 8async function broadcastToFollowers (
8 data: any, 9 data: any,
@@ -41,6 +42,27 @@ async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: s
41 return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) 42 return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload)
42} 43}
43 44
45function getOriginVideoAudience (video: VideoInstance) {
46 return {
47 to: [ video.VideoChannel.Account.url ],
48 cc: [ video.VideoChannel.Account.url + '/followers' ]
49 }
50}
51
52function getVideoFollowersAudience (video: VideoInstance) {
53 return {
54 to: [ video.VideoChannel.Account.url + '/followers' ],
55 cc: []
56 }
57}
58
59async function getAccountsToForwardVideoAction (byAccount: AccountInstance, video: VideoInstance) {
60 const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id)
61 accountsToForwardView.push(video.VideoChannel.Account)
62
63 return accountsToForwardView
64}
65
44async function getAudience (accountSender: AccountInstance, isPublic = true) { 66async function getAudience (accountSender: AccountInstance, isPublic = true) {
45 const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls() 67 const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls()
46 68
@@ -64,5 +86,8 @@ async function getAudience (accountSender: AccountInstance, isPublic = true) {
64export { 86export {
65 broadcastToFollowers, 87 broadcastToFollowers,
66 unicastTo, 88 unicastTo,
67 getAudience 89 getAudience,
90 getOriginVideoAudience,
91 getAccountsToForwardVideoAction,
92 getVideoFollowersAudience
68} 93}
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index e5fb212b7..6afe67ee6 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -1,11 +1,17 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityCreate } from '../../../../shared/models/activitypub/activity' 2import { ActivityCreate } from '../../../../shared/models/activitypub/activity'
3import { getServerAccount } from '../../../helpers/utils'
3import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' 4import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models'
4import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface' 5import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface'
5import { broadcastToFollowers, getAudience, unicastTo } from './misc' 6import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
6import { getVideoAbuseActivityPubUrl, getVideoViewActivityPubUrl } from '../url' 7import {
7import { getServerAccount } from '../../../helpers/utils' 8 broadcastToFollowers,
8import { database as db } from '../../../initializers' 9 getAccountsToForwardVideoAction,
10 getAudience,
11 getOriginVideoAudience,
12 getVideoFollowersAudience,
13 unicastTo
14} from './misc'
9 15
10async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { 16async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) {
11 const byAccount = videoChannel.Account 17 const byAccount = videoChannel.Account
@@ -29,7 +35,7 @@ async function sendCreateViewToOrigin (byAccount: AccountInstance, video: VideoI
29 const url = getVideoViewActivityPubUrl(byAccount, video) 35 const url = getVideoViewActivityPubUrl(byAccount, video)
30 const viewActivity = createViewActivityData(byAccount, video) 36 const viewActivity = createViewActivityData(byAccount, video)
31 37
32 const audience = { to: [ video.VideoChannel.Account.url ], cc: [ video.VideoChannel.Account.url + '/followers' ] } 38 const audience = getOriginVideoAudience(video)
33 const data = await createActivityData(url, byAccount, viewActivity, audience) 39 const data = await createActivityData(url, byAccount, viewActivity, audience)
34 40
35 return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) 41 return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
@@ -39,16 +45,35 @@ async function sendCreateViewToVideoFollowers (byAccount: AccountInstance, video
39 const url = getVideoViewActivityPubUrl(byAccount, video) 45 const url = getVideoViewActivityPubUrl(byAccount, video)
40 const viewActivity = createViewActivityData(byAccount, video) 46 const viewActivity = createViewActivityData(byAccount, video)
41 47
42 const audience = { to: [ video.VideoChannel.Account.url + '/followers' ], cc: [] } 48 const audience = getVideoFollowersAudience(video)
43 const data = await createActivityData(url, byAccount, viewActivity, audience) 49 const data = await createActivityData(url, byAccount, viewActivity, audience)
44 50
45 const serverAccount = await getServerAccount() 51 const serverAccount = await getServerAccount()
46 const accountsToForwardView = await db.VideoShare.loadAccountsByShare(video.id) 52 const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
47 accountsToForwardView.push(video.VideoChannel.Account) 53
54 const followersException = [ byAccount ]
55 return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
56}
57
58async function sendCreateDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
59 const url = getVideoDislikeActivityPubUrl(byAccount, video)
60 const dislikeActivity = createDislikeActivityData(byAccount, video)
61
62 const audience = getOriginVideoAudience(video)
63 const data = await createActivityData(url, byAccount, dislikeActivity, audience)
64
65 return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
66}
48 67
49 // Don't forward view to server that sent it to us 68async function sendCreateDislikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
50 const index = accountsToForwardView.findIndex(a => a.id === byAccount.id) 69 const url = getVideoDislikeActivityPubUrl(byAccount, video)
51 if (index) accountsToForwardView.splice(index, 1) 70 const dislikeActivity = createDislikeActivityData(byAccount, video)
71
72 const audience = getVideoFollowersAudience(video)
73 const data = await createActivityData(url, byAccount, dislikeActivity, audience)
74
75 const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
76 const serverAccount = await getServerAccount()
52 77
53 const followersException = [ byAccount ] 78 const followersException = [ byAccount ]
54 return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException) 79 return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
@@ -71,6 +96,16 @@ async function createActivityData (url: string, byAccount: AccountInstance, obje
71 return activity 96 return activity
72} 97}
73 98
99function createDislikeActivityData (byAccount: AccountInstance, video: VideoInstance) {
100 const obj = {
101 type: 'Dislike',
102 actor: byAccount.url,
103 object: video.url
104 }
105
106 return obj
107}
108
74// --------------------------------------------------------------------------- 109// ---------------------------------------------------------------------------
75 110
76export { 111export {
@@ -78,7 +113,10 @@ export {
78 sendVideoAbuse, 113 sendVideoAbuse,
79 createActivityData, 114 createActivityData,
80 sendCreateViewToOrigin, 115 sendCreateViewToOrigin,
81 sendCreateViewToVideoFollowers 116 sendCreateViewToVideoFollowers,
117 sendCreateDislikeToOrigin,
118 sendCreateDislikeToVideoFollowers,
119 createDislikeActivityData
82} 120}
83 121
84// --------------------------------------------------------------------------- 122// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
new file mode 100644
index 000000000..70a7d886f
--- /dev/null
+++ b/server/lib/activitypub/send/send-like.ts
@@ -0,0 +1,60 @@
1import { Transaction } from 'sequelize'
2import { ActivityLike } from '../../../../shared/models/activitypub/activity'
3import { getServerAccount } from '../../../helpers/utils'
4import { AccountInstance, VideoInstance } from '../../../models'
5import { getVideoLikeActivityPubUrl } from '../url'
6import {
7 broadcastToFollowers,
8 getAccountsToForwardVideoAction,
9 getAudience,
10 getOriginVideoAudience,
11 getVideoFollowersAudience,
12 unicastTo
13} from './misc'
14
15async function sendLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
16 const url = getVideoLikeActivityPubUrl(byAccount, video)
17
18 const audience = getOriginVideoAudience(video)
19 const data = await likeActivityData(url, byAccount, video, audience)
20
21 return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
22}
23
24async function sendLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
25 const url = getVideoLikeActivityPubUrl(byAccount, video)
26
27 const audience = getVideoFollowersAudience(video)
28 const data = await likeActivityData(url, byAccount, video, audience)
29
30 const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
31 const serverAccount = await getServerAccount()
32
33 const followersException = [ byAccount ]
34 return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
35}
36
37async function likeActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, audience?: { to: string[], cc: string[] }) {
38 if (!audience) {
39 audience = await getAudience(byAccount)
40 }
41
42 const activity: ActivityLike = {
43 type: 'Like',
44 id: url,
45 actor: byAccount.url,
46 to: audience.to,
47 cc: audience.cc,
48 object: video.url
49 }
50
51 return activity
52}
53
54// ---------------------------------------------------------------------------
55
56export {
57 sendLikeToOrigin,
58 sendLikeToVideoFollowers,
59 likeActivityData
60}
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index 77bee6639..53fddd0cb 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -1,10 +1,14 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityFollow, ActivityUndo } from '../../../../shared/models/activitypub/activity' 2import { ActivityCreate, ActivityFollow, ActivityLike, ActivityUndo } from '../../../../shared/models/activitypub/activity'
3import { AccountInstance } from '../../../models' 3import { AccountInstance } from '../../../models'
4import { AccountFollowInstance } from '../../../models/account/account-follow-interface' 4import { AccountFollowInstance } from '../../../models/account/account-follow-interface'
5import { unicastTo } from './misc' 5import { broadcastToFollowers, getAccountsToForwardVideoAction, unicastTo } from './misc'
6import { followActivityData } from './send-follow' 6import { followActivityData } from './send-follow'
7import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl } from '../url' 7import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
8import { VideoInstance } from '../../../models/video/video-interface'
9import { likeActivityData } from './send-like'
10import { createActivityData, createDislikeActivityData } from './send-create'
11import { getServerAccount } from '../../../helpers/utils'
8 12
9async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transaction) { 13async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transaction) {
10 const me = accountFollow.AccountFollower 14 const me = accountFollow.AccountFollower
@@ -19,15 +23,72 @@ async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transact
19 return unicastTo(data, me, following.inboxUrl, t) 23 return unicastTo(data, me, following.inboxUrl, t)
20} 24}
21 25
26async function sendUndoLikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
27 const likeUrl = getVideoLikeActivityPubUrl(byAccount, video)
28 const undoUrl = getUndoActivityPubUrl(likeUrl)
29
30 const object = await likeActivityData(likeUrl, byAccount, video)
31 const data = await undoActivityData(undoUrl, byAccount, object)
32
33 return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
34}
35
36async function sendUndoLikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
37 const likeUrl = getVideoLikeActivityPubUrl(byAccount, video)
38 const undoUrl = getUndoActivityPubUrl(likeUrl)
39
40 const object = await likeActivityData(likeUrl, byAccount, video)
41 const data = await undoActivityData(undoUrl, byAccount, object)
42
43 const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
44 const serverAccount = await getServerAccount()
45
46 const followersException = [ byAccount ]
47 return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
48}
49
50async function sendUndoDislikeToOrigin (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
51 const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video)
52 const undoUrl = getUndoActivityPubUrl(dislikeUrl)
53
54 const dislikeActivity = createDislikeActivityData(byAccount, video)
55 const object = await createActivityData(undoUrl, byAccount, dislikeActivity)
56
57 const data = await undoActivityData(undoUrl, byAccount, object)
58
59 return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t)
60}
61
62async function sendUndoDislikeToVideoFollowers (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
63 const dislikeUrl = getVideoDislikeActivityPubUrl(byAccount, video)
64 const undoUrl = getUndoActivityPubUrl(dislikeUrl)
65
66 const dislikeActivity = createDislikeActivityData(byAccount, video)
67 const object = await createActivityData(undoUrl, byAccount, dislikeActivity)
68
69 const data = await undoActivityData(undoUrl, byAccount, object)
70
71 const accountsToForwardView = await getAccountsToForwardVideoAction(byAccount, video)
72 const serverAccount = await getServerAccount()
73
74 const followersException = [ byAccount ]
75 return broadcastToFollowers(data, serverAccount, accountsToForwardView, t, followersException)
76}
77
78
22// --------------------------------------------------------------------------- 79// ---------------------------------------------------------------------------
23 80
24export { 81export {
25 sendUndoFollow 82 sendUndoFollow,
83 sendUndoLikeToOrigin,
84 sendUndoLikeToVideoFollowers,
85 sendUndoDislikeToOrigin,
86 sendUndoDislikeToVideoFollowers
26} 87}
27 88
28// --------------------------------------------------------------------------- 89// ---------------------------------------------------------------------------
29 90
30async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow) { 91async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow | ActivityLike | ActivityCreate) {
31 const activity: ActivityUndo = { 92 const activity: ActivityUndo = {
32 type: 'Undo', 93 type: 'Undo',
33 id: url, 94 id: url,
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index d98561e33..17395a99b 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -25,6 +25,14 @@ function getVideoViewActivityPubUrl (byAccount: AccountInstance, video: VideoIns
25 return video.url + '#views/' + byAccount.uuid + '/' + new Date().toISOString() 25 return video.url + '#views/' + byAccount.uuid + '/' + new Date().toISOString()
26} 26}
27 27
28function getVideoLikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
29 return byAccount.url + '#likes/' + video.id
30}
31
32function getVideoDislikeActivityPubUrl (byAccount: AccountInstance, video: VideoInstance) {
33 return byAccount.url + '#dislikes/' + video.id
34}
35
28function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) { 36function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) {
29 const me = accountFollow.AccountFollower 37 const me = accountFollow.AccountFollower
30 const following = accountFollow.AccountFollowing 38 const following = accountFollow.AccountFollowing
@@ -61,5 +69,7 @@ export {
61 getAnnounceActivityPubUrl, 69 getAnnounceActivityPubUrl,
62 getUpdateActivityPubUrl, 70 getUpdateActivityPubUrl,
63 getUndoActivityPubUrl, 71 getUndoActivityPubUrl,
64 getVideoViewActivityPubUrl 72 getVideoViewActivityPubUrl,
73 getVideoLikeActivityPubUrl,
74 getVideoDislikeActivityPubUrl
65} 75}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 944244893..acee4fe16 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,9 +1,20 @@
1import { join } from 'path' 1import { join } from 'path'
2import * as request from 'request' 2import * as request from 'request'
3import { Transaction } from 'sequelize'
3import { ActivityIconObject } from '../../../shared/index' 4import { ActivityIconObject } from '../../../shared/index'
4import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 5import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
5import { CONFIG, REMOTE_SCHEME, STATIC_PATHS } from '../../initializers/constants' 6import { CONFIG, REMOTE_SCHEME, STATIC_PATHS } from '../../initializers/constants'
7import { AccountInstance } from '../../models/account/account-interface'
6import { VideoInstance } from '../../models/video/video-interface' 8import { VideoInstance } from '../../models/video/video-interface'
9import { sendLikeToOrigin } from './index'
10import { sendCreateDislikeToOrigin, sendCreateDislikeToVideoFollowers } from './send/send-create'
11import { sendLikeToVideoFollowers } from './send/send-like'
12import {
13 sendUndoDislikeToOrigin,
14 sendUndoDislikeToVideoFollowers,
15 sendUndoLikeToOrigin,
16 sendUndoLikeToVideoFollowers
17} from './send/send-undo'
7 18
8function fetchRemoteVideoPreview (video: VideoInstance) { 19function fetchRemoteVideoPreview (video: VideoInstance) {
9 // FIXME: use url 20 // FIXME: use url
@@ -37,8 +48,42 @@ function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObjec
37 return doRequestAndSaveToFile(options, thumbnailPath) 48 return doRequestAndSaveToFile(options, thumbnailPath)
38} 49}
39 50
51function sendVideoRateChangeToFollowers (account: AccountInstance, video: VideoInstance, likes: number, dislikes: number, t: Transaction) {
52 const tasks: Promise<any>[] = []
53
54 // Undo Like
55 if (likes < 0) tasks.push(sendUndoLikeToVideoFollowers(account, video, t))
56 // Like
57 if (likes > 0) tasks.push(sendLikeToVideoFollowers(account, video, t))
58
59 // Undo Dislike
60 if (dislikes < 0) tasks.push(sendUndoDislikeToVideoFollowers(account, video, t))
61 // Dislike
62 if (dislikes > 0) tasks.push(sendCreateDislikeToVideoFollowers(account, video, t))
63
64 return Promise.all(tasks)
65}
66
67function sendVideoRateChangeToOrigin (account: AccountInstance, video: VideoInstance, likes: number, dislikes: number, t: Transaction) {
68 const tasks: Promise<any>[] = []
69
70 // Undo Like
71 if (likes < 0) tasks.push(sendUndoLikeToOrigin(account, video, t))
72 // Like
73 if (likes > 0) tasks.push(sendLikeToOrigin(account, video, t))
74
75 // Undo Dislike
76 if (dislikes < 0) tasks.push(sendUndoDislikeToOrigin(account, video, t))
77 // Dislike
78 if (dislikes > 0) tasks.push(sendCreateDislikeToOrigin(account, video, t))
79
80 return Promise.all(tasks)
81}
82
40export { 83export {
41 fetchRemoteVideoPreview, 84 fetchRemoteVideoPreview,
42 fetchRemoteVideoDescription, 85 fetchRemoteVideoDescription,
43 generateThumbnailFromUrl 86 generateThumbnailFromUrl,
87 sendVideoRateChangeToFollowers,
88 sendVideoRateChangeToOrigin
44} 89}
diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts
index 111fc88a4..5b4c65b81 100644
--- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts
+++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-broadcast-handler.ts
@@ -2,7 +2,7 @@ import { logger } from '../../../helpers'
2import { buildSignedActivity } from '../../../helpers/activitypub' 2import { buildSignedActivity } from '../../../helpers/activitypub'
3import { doRequest } from '../../../helpers/requests' 3import { doRequest } from '../../../helpers/requests'
4import { database as db } from '../../../initializers' 4import { database as db } from '../../../initializers'
5import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler' 5import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
6 6
7async function process (payload: ActivityPubHttpPayload, jobId: number) { 7async function process (payload: ActivityPubHttpPayload, jobId: number) {
8 logger.info('Processing ActivityPub broadcast in job %d.', jobId) 8 logger.info('Processing ActivityPub broadcast in job %d.', jobId)
@@ -20,7 +20,12 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) {
20 20
21 for (const uri of payload.uris) { 21 for (const uri of payload.uris) {
22 options.uri = uri 22 options.uri = uri
23 await doRequest(options) 23
24 try {
25 await doRequest(options)
26 } catch (err) {
27 await maybeRetryRequestLater(err, payload, uri)
28 }
24 } 29 }
25} 30}
26 31
diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts
index aef217ce7..ccf109935 100644
--- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts
+++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler.ts
@@ -4,12 +4,16 @@ import * as activitypubHttpBroadcastHandler from './activitypub-http-broadcast-h
4import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handler' 4import * as activitypubHttpUnicastHandler from './activitypub-http-unicast-handler'
5import * as activitypubHttpFetcherHandler from './activitypub-http-fetcher-handler' 5import * as activitypubHttpFetcherHandler from './activitypub-http-fetcher-handler'
6import { JobCategory } from '../../../../shared' 6import { JobCategory } from '../../../../shared'
7import { ACTIVITY_PUB } from '../../../initializers/constants'
8import { logger } from '../../../helpers/logger'
7 9
8type ActivityPubHttpPayload = { 10type ActivityPubHttpPayload = {
9 uris: string[] 11 uris: string[]
10 signatureAccountId?: number 12 signatureAccountId?: number
11 body?: any 13 body?: any
14 attemptNumber?: number
12} 15}
16
13const jobHandlers: { [ handlerName: string ]: JobHandler<ActivityPubHttpPayload, void> } = { 17const jobHandlers: { [ handlerName: string ]: JobHandler<ActivityPubHttpPayload, void> } = {
14 activitypubHttpBroadcastHandler, 18 activitypubHttpBroadcastHandler,
15 activitypubHttpUnicastHandler, 19 activitypubHttpUnicastHandler,
@@ -19,7 +23,25 @@ const jobCategory: JobCategory = 'activitypub-http'
19 23
20const activitypubHttpJobScheduler = new JobScheduler(jobCategory, jobHandlers) 24const activitypubHttpJobScheduler = new JobScheduler(jobCategory, jobHandlers)
21 25
26function maybeRetryRequestLater (err: Error, payload: ActivityPubHttpPayload, uri: string) {
27 logger.warn('Cannot make request to %s.', uri, err)
28
29 let attemptNumber = payload.attemptNumber || 1
30 attemptNumber += 1
31
32 if (attemptNumber < ACTIVITY_PUB.MAX_HTTP_ATTEMPT) {
33 logger.debug('Retrying request to %s (attempt %d/%d).', uri, attemptNumber, ACTIVITY_PUB.MAX_HTTP_ATTEMPT, err)
34
35 const newPayload = Object.assign(payload, {
36 uris: [ uri ],
37 attemptNumber
38 })
39 return activitypubHttpJobScheduler.createJob(undefined, 'activitypubHttpUnicastHandler', newPayload)
40 }
41}
42
22export { 43export {
23 ActivityPubHttpPayload, 44 ActivityPubHttpPayload,
24 activitypubHttpJobScheduler 45 activitypubHttpJobScheduler,
46 maybeRetryRequestLater
25} 47}
diff --git a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts
index 8d3b755ad..f7f3dabbd 100644
--- a/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts
+++ b/server/lib/jobs/activitypub-http-job-scheduler/activitypub-http-unicast-handler.ts
@@ -1,6 +1,6 @@
1import { logger } from '../../../helpers' 1import { logger } from '../../../helpers'
2import { doRequest } from '../../../helpers/requests' 2import { doRequest } from '../../../helpers/requests'
3import { ActivityPubHttpPayload } from './activitypub-http-job-scheduler' 3import { ActivityPubHttpPayload, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
4import { database as db } from '../../../initializers/database' 4import { database as db } from '../../../initializers/database'
5import { buildSignedActivity } from '../../../helpers/activitypub' 5import { buildSignedActivity } from '../../../helpers/activitypub'
6 6
@@ -18,7 +18,12 @@ async function process (payload: ActivityPubHttpPayload, jobId: number) {
18 json: signedBody 18 json: signedBody
19 } 19 }
20 20
21 await doRequest(options) 21 try {
22 await doRequest(options)
23 } catch (err) {
24 await maybeRetryRequestLater(err, payload, uri)
25 throw err
26 }
22} 27}
23 28
24function onError (err: Error, jobId: number) { 29function onError (err: Error, jobId: number) {
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts
index 6f228c790..a0d620dd0 100644
--- a/server/models/account/account-follow-interface.ts
+++ b/server/models/account/account-follow-interface.ts
@@ -5,7 +5,11 @@ import { ResultList } from '../../../shared/models/result-list.model'
5import { AccountInstance } from './account-interface' 5import { AccountInstance } from './account-interface'
6 6
7export namespace AccountFollowMethods { 7export namespace AccountFollowMethods {
8 export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird<AccountFollowInstance> 8 export type LoadByAccountAndTarget = (
9 accountId: number,
10 targetAccountId: number,
11 t?: Sequelize.Transaction
12 ) => Bluebird<AccountFollowInstance>
9 13
10 export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>> 14 export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
11 export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>> 15 export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
index 578bcda39..8e35c7d20 100644
--- a/server/models/account/account-follow.ts
+++ b/server/models/account/account-follow.ts
@@ -93,7 +93,7 @@ toFormattedJSON = function (this: AccountFollowInstance) {
93 return json 93 return json
94} 94}
95 95
96loadByAccountAndTarget = function (accountId: number, targetAccountId: number) { 96loadByAccountAndTarget = function (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
97 const query = { 97 const query = {
98 where: { 98 where: {
99 accountId, 99 accountId,
@@ -110,7 +110,8 @@ loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
110 required: true, 110 required: true,
111 as: 'AccountFollowing' 111 as: 'AccountFollowing'
112 } 112 }
113 ] 113 ],
114 transaction: t
114 } 115 }
115 116
116 return AccountFollow.findOne(query) 117 return AccountFollow.findOne(query)
diff --git a/server/tests/api/index-slow.ts b/server/tests/api/index-slow.ts
index da56398b1..2448147d8 100644
--- a/server/tests/api/index-slow.ts
+++ b/server/tests/api/index-slow.ts
@@ -1,4 +1,5 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2// import './multiple-servers' 2// import './multiple-servers'
3import './video-transcoder' 3import './video-transcoder'
4import './multiple-servers'
4import './follows' 5import './follows'
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
index ce150bc12..cbfd6157a 100644
--- a/shared/models/activitypub/activity.ts
+++ b/shared/models/activitypub/activity.ts
@@ -1,13 +1,14 @@
1import { ActivityPubSignature } from './activitypub-signature' 1import { ActivityPubSignature } from './activitypub-signature'
2import { VideoChannelObject, VideoTorrentObject } from './objects' 2import { VideoChannelObject, VideoTorrentObject } from './objects'
3import { DislikeObject } from './objects/dislike-object'
3import { VideoAbuseObject } from './objects/video-abuse-object' 4import { VideoAbuseObject } from './objects/video-abuse-object'
4import { ViewObject } from './objects/view-object' 5import { ViewObject } from './objects/view-object'
5 6
6export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | 7export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate |
7 ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce | 8 ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
8 ActivityUndo 9 ActivityUndo | ActivityLike
9 10
10export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' 11export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like'
11 12
12export interface BaseActivity { 13export interface BaseActivity {
13 '@context'?: any[] 14 '@context'?: any[]
@@ -21,7 +22,7 @@ export interface BaseActivity {
21 22
22export interface ActivityCreate extends BaseActivity { 23export interface ActivityCreate extends BaseActivity {
23 type: 'Create' 24 type: 'Create'
24 object: VideoChannelObject | VideoAbuseObject | ViewObject 25 object: VideoChannelObject | VideoAbuseObject | ViewObject | DislikeObject
25} 26}
26 27
27export interface ActivityAdd extends BaseActivity { 28export interface ActivityAdd extends BaseActivity {
@@ -55,5 +56,10 @@ export interface ActivityAnnounce extends BaseActivity {
55 56
56export interface ActivityUndo extends BaseActivity { 57export interface ActivityUndo extends BaseActivity {
57 type: 'Undo', 58 type: 'Undo',
58 object: ActivityFollow 59 object: ActivityFollow | ActivityLike | ActivityCreate
60}
61
62export interface ActivityLike extends BaseActivity {
63 type: 'Like',
64 object: string
59} 65}
diff --git a/shared/models/activitypub/objects/dislike-object.ts b/shared/models/activitypub/objects/dislike-object.ts
new file mode 100644
index 000000000..295175774
--- /dev/null
+++ b/shared/models/activitypub/objects/dislike-object.ts
@@ -0,0 +1,5 @@
1export interface DislikeObject {
2 type: 'Dislike',
3 actor: string
4 object: string
5}
diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts
index d92f772e2..f1f761e44 100644
--- a/shared/models/activitypub/objects/index.ts
+++ b/shared/models/activitypub/objects/index.ts
@@ -3,3 +3,4 @@ export * from './video-abuse-object'
3export * from './video-channel-object' 3export * from './video-channel-object'
4export * from './video-torrent-object' 4export * from './video-torrent-object'
5export * from './view-object' 5export * from './view-object'
6export * from './dislike-object'